2017-08-25 10 views
10

Wygląda na to, że wybranie kolumn z tabeli data.table za pomocą [.data.table powoduje skopiowanie bazowego wektora (ów). Mówię o bardzo prostym wyborze kolumn, według nazwy, nie ma wyrażeń do obliczenia w j i nie ma żadnych wierszy do podzbioru w i. Co dziwniejsze, podzbiór kolumny w data.frame wydaje się nie wykonywać żadnych kopii. Używam danych data.table version data.table 1.10.4. Prosty przykład ze szczegółowymi informacjami i testami porównawczymi znajduje się poniżej. Moje pytania to:Dlaczego wybranie kolumn (y) z tabeli data.table powoduje wykonanie kopii?

  • Czy robię coś nie tak?
  • Czy to błąd, czy jest to zamierzone zachowanie?
  • Jeśli jest to zamierzone, jakie jest najlepsze podejście do podzbioru danych w tabelach i uniknięcia dodatkowej kopii?

Planowany przypadek użycia obejmuje duży zbiór danych, więc unikanie dodatkowych kopii jest koniecznością (zwłaszcza, że ​​baza R wydaje się już to wspierać).

library(data.table) 
set.seed(12345) 
cpp_dt <- data.table(a = runif(1e6), b = rnorm(1e6), c = runif(1e6)) 
cols=c("a","c") 

## naive/data.frame style of column selection 
## leads to a copy of the column vectors in cols 
subset_cols_1=function(dt,cols){ 
    return(dt[,cols,with=F]) 
} 

## alternative syntax, still results in a copy 
subset_cols_2=function(dt,cols){ 
    return(dt[,..cols]) 
} 

## work-around that uses data.frame column selection, 
## appears to avoid the copy 
subset_cols_3=function(dt,cols){ 
    setDF(dt) 
    subset=dt[,cols] 
    setDT(subset) 
    setDT(dt) 
    return(subset) 
} 

## another approach that makes a "shallow" copy of the data.table 
## then NULLs the not needed columns by reference 
## appears to also avoid the copy 
subset_cols_4=function(dt,cols){ 
    subset=dt[TRUE] 
    other_cols=setdiff(names(subset),cols) 
    set(subset,j=other_cols,value=NULL) 
    return(subset) 
} 

subset_1=subset_cols_1(cpp_dt,cols) 
subset_2=subset_cols_2(cpp_dt,cols) 
subset_3=subset_cols_3(cpp_dt,cols) 
subset_4=subset_cols_4(cpp_dt,cols) 

Teraz pozwala spojrzeć na alokację pamięci i porównać z oryginalnymi danymi.

.Internal(inspect(cpp_dt)) # original data, keep an eye on 1st and 3d vector 
# @7fe8ba278800 19 VECSXP g1c7 [OBJ,MARK,NAM(2),ATT] (len=3, tl=1027) 
# @10e2ce000 14 REALSXP g1c7 [MARK,NAM(2)] (len=1000000, tl=0) 0.720904,0.875773,0.760982,0.886125,0.456481,... 
# @10f1a3000 14 REALSXP g1c7 [MARK,NAM(2)] (len=1000000, tl=0) -0.947317,-0.636669,0.167872,-0.206986,0.411445,... 
# @10f945000 14 REALSXP g1c7 [MARK,NAM(2)] (len=1000000, tl=0) 0.717611,0.95416,0.191546,0.48525,0.539878,... 
# ATTRIB: [removed] 

Stosując [.data.table metody podzbiór kolumn:

.Internal(inspect(subset_1)) # looks like data.table is making a copy 
# @7fe8b9f3b800 19 VECSXP g0c7 [OBJ,NAM(1),ATT] (len=2, tl=1026) 
# @114cb0000 14 REALSXP g0c7 [MARK,NAM(2)] (len=1000000, tl=0) 0.720904,0.875773,0.760982,0.886125,0.456481,... 
# @1121ca000 14 REALSXP g0c7 [NAM(2)] (len=1000000, tl=0) 0.717611,0.95416,0.191546,0.48525,0.539878,... 
# ATTRIB: [removed] 

innej wersji składnia nadal używa [.data.table a jeszcze tworzenia kopii:

.Internal(inspect(subset_2)) # same, still copy 
# @7fe8b6402600 19 VECSXP g0c7 [OBJ,NAM(1),ATT] (len=2, tl=1026) 
# @115452000 14 REALSXP g0c7 [NAM(2)] (len=1000000, tl=0) 0.720904,0.875773,0.760982,0.886125,0.456481,... 
# @1100e7000 14 REALSXP g0c7 [NAM(2)] (len=1000000, tl=0) 0.717611,0.95416,0.191546,0.48525,0.539878,... 
# ATTRIB: [removed] 

stosując kolejno setDF, a następnie [.data.frame i setDT. Zobacz, wektory a i c nie są już kopiowane! Wydaje się, że podstawowa metoda R jest bardziej wydajna/ma mniejszy ślad pamięci?

.Internal(inspect(subset_3)) # "[.data.frame" is not making a copy!! 
# @7fe8b633f400 19 VECSXP g0c7 [OBJ,NAM(2),ATT] (len=2, tl=1026) 
# @10e2ce000 14 REALSXP g1c7 [MARK,NAM(2)] (len=1000000, tl=0) 0.720904,0.875773,0.760982,0.886125,0.456481,... 
# @10f945000 14 REALSXP g1c7 [MARK,NAM(2)] (len=1000000, tl=0) 0.717611,0.95416,0.191546,0.48525,0.539878,... 
# ATTRIB: [removed] 

Innym podejściem jest, aby płytki kopię data.table, następnie NULL wszelkie dodatkowe kolumny odnośnik w nowej data.table. Ponownie nie są wykonywane kopie.

.Internal(inspect(subset_4)) # 4th approach seems to also avoid the copy 
# @7fe8b924d800 19 VECSXP g0c7 [OBJ,NAM(2),ATT] (len=2, tl=1027) 
# @10e2ce000 14 REALSXP g1c7 [MARK,NAM(2)] (len=1000000, tl=0) 0.720904,0.875773,0.760982,0.886125,0.456481,... 
# @10f945000 14 REALSXP g1c7 [MARK,NAM(2)] (len=1000000, tl=0) 0.717611,0.95416,0.191546,0.48525,0.539878,... 
# ATTRIB: [removed] 

Teraz przyjrzyjmy się benchmarkom tych czterech podejść. Wygląda na to, że "[.data.frame" (subset_cols_3) jest wyraźnym zwycięzcą.

microbenchmark({subset_cols_1(cpp_dt,cols)}, 
       {subset_cols_2(cpp_dt,cols)}, 
       {subset_cols_3(cpp_dt,cols)}, 
       {subset_cols_4(cpp_dt,cols)}, 
       times=100) 

# Unit: microseconds 
#         expr  min  lq  mean median  uq  max neval 
# {  subset_cols_1(cpp_dt, cols) } 4772.092 5128.7395 8956.7398 7149.447 10189.397 53117.358 100 
# {  subset_cols_2(cpp_dt, cols) } 4705.383 5107.1690 8977.1816 6680.666 9206.164 53523.191 100 
# {  subset_cols_3(cpp_dt, cols) } 148.659 177.9595 285.4926 250.620 283.414 4422.968 100 
# {  subset_cols_4(cpp_dt, cols) } 193.912 241.9010 531.8308 336.467 384.844 20061.864 100 
+1

Może po prostu czekać na aktualizacje tutaj: https://stackoverflow.com/a/26481429/ Funkcja 'shallow' nie została jeszcze wyeksportowana, ale prawdopodobnie pomoże w tym. – Frank

Odpowiedz

4

Minęło trochę czasu, odkąd myślałem o tym, ale tutaj idzie.

Dobre pytanie. Ale dlaczego musisz tak ustawić podzbiór data.table? Naprawdę potrzebujemy zobaczyć, co robisz następny: większy obraz. Jest to większy obraz, który prawdopodobnie mamy inny sposób w data.table niż podstawowy id r.

grubsza ilustrującą ze chyba zły przykład:

DT[region=="EU", lapply(.SD, sum), .SDcols=10:20] 

zamiast idiom baza R biorąc podzbiór a potem robi coś następny (tutaj apply) na skutek zewnętrzny:

apply(DT[DT$region=="EU", 10:20], 2, sum) 

Ogólnie rzecz biorąc, chcemy zachęcić do robienia jak najwięcej w jednym [...], aby data.table zobaczyła i, j i by razem w jednym [...] i może zoptymalizować połączenie. Po podzieleniu kolumn, a następnie wykonaniu następnej rzeczy na zewnątrz, optymalizacja wymaga złożoności oprogramowania. W większości przypadków większość kosztów obliczeniowych znajduje się wewnątrz pierwszego modelu [...], który zmniejsza się do stosunkowo niewielkiego rozmiaru.

Po tym, oprócz komentarza Franka o numerze shallow, czekamy również, aby zobaczyć, jak wygląda patelnia ALTREP project. Poprawia to liczenie odwołań w bazie R i może umożliwić niezawodne sprawdzenie, czy kolumna, na której działa, musi być najpierw kopiowana, czy też nie. Obecnie := zawsze aktualizuje się przez odniesienie, aby zaktualizować obie tabele data.table, jeśli wybieranie-niektóre-kolumny nie byłyby głęboką kopią (celowe jest to, że kopiuje, z tego powodu). Jeśli := nie jest używane wewnątrz [...], to zawsze [...] zwraca nowy wynik, który może być bezpiecznie użyty, co jest obecnie całkiem prostą regułą. Nawet jeśli z jakiegoś powodu wszystko, co robisz, to wybranie kilku całych kolumn.

Naprawdę potrzebujemy zobaczyć większy obraz, proszę: co robicie później w podzbiorze kolumn. Mając to na uwadze, pomogłoby to w podniesieniu priorytetu w badaniu ALTREP, a może w obliczaniu tej liczby w naszym przypadku.

Powiązane problemy