Подтвердить что ты не робот

Почему выбор столбца (столбцов) из таблицы data.table приводит к копированию?

Похоже, что выбор столбца (ов) из таблицы данных с таблицей [.data.table приводит к копированию базового вектора (ов). Я говорю о очень простом выборе столбцов, по имени, нет выражений для вычисления в j и нет строк для подмножества в i. Еще более странно, что подмножество столбцов в data.frame, похоже, не делает никаких копий. Я использую data.table version data.table 1.10.4. Ниже приведен простой пример с подробностями и ориентирами. Мои вопросы:

  • Я делаю что-то неправильно?
  • Это ошибка или это предполагаемое поведение?
  • Если это предназначено, какой лучший подход для подмножества data.table по столбцам и избежать дополнительной копии?

Предполагаемый прецедент предполагает большой набор данных, поэтому избегать дополнительных копий является обязательным (особенно потому, что база R, похоже, уже поддерживает это).

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)

Теперь рассмотрим распределение памяти и сравним с исходными данными.

.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]

Использование метода [.data.table для подмножества столбцов:

.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]

Другая версия синтаксиса, которая все еще использует [.data.table и все еще делает копию:

.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]

Используя последовательность setDF, а затем [.data.frame и setDT. Посмотрите, векторы a и c больше не копируются! Похоже, что базовый метод R более эффективен/имеет меньший объем памяти?

.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]

Другой подход заключается в создании мелкой копии таблицы данных. Затем NULL добавляет все дополнительные столбцы по ссылке в новой таблице данных. Опять не копируются.

.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]

Теперь давайте посмотрим на тесты этих четырех подходов. Похоже, что "[.data.frame" (subset_cols_3) является явным победителем.

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
4b9b3361

Ответ 1

Прошло некоторое время с тех пор, как я подумал об этом, но здесь идет.

Хороший вопрос. Но зачем вам подмножать data.table? Нам действительно нужно посмотреть, что вы делаете дальше: большая картина. Это то, что большая картина, что у нас, вероятно, есть другой способ для data.table, чем базовый R-идиом.

Примерный пример с плохим примером:

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

а не базовый R-идентификатор взятия подмножества, а затем выполнение чего-то следующего (здесь apply) результата вне:

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

В общем, мы хотим поощрять как можно больше внутри одного [...], так что data.table видит i, j и by вместе в одной операции [...] и может оптимизировать комбинацию, Когда вы подмножите столбцы, а затем выполняете следующую вещь за пределами, это требует большей сложности программного обеспечения для оптимизации. В большинстве случаев большая часть вычислительной стоимости находится внутри первого [...], который уменьшается до относительно незначительного размера.

С учетом сказанного, помимо комментария Фрэнка о shallow, мы также ожидаем увидеть, как проект ALTREP выкладывает, Это улучшает подсчет ссылок в базе R и может позволить := достоверно знать, должен ли столбец, с которым он работает, вначале копировать-на-запись. В настоящее время := всегда обновляется по ссылке, поэтому он обновляет оба data.table, если select-some-whole-columns не принимает глубокую копию (поэтому для этого необходимо скопировать ее). Если := не используется внутри [...], то [...] всегда возвращает новый результат, безопасный для использования := on, что в настоящее время является довольно простым правилом. Даже если все, что вы делаете, по какой-то причине выбирает несколько целых столбцов.

Нам действительно нужно увидеть большую картину, пожалуйста: что вы делаете впоследствии в подмножестве столбцов. Если бы это было ясно, это помогло бы повысить приоритет при расследовании ALTREP или, возможно, в этом случае сделать наш собственный счетчик ссылок.