为什么从data.table中选择列会产生副本?

时间:2017-08-25 01:56:07

标签: r data.table

似乎从带有[.data.table的data.table中选择列会产生基础向量的副本。我说的是非常简单的列选择,按名称,j中没有要计算的表达式,i中没有要子集的行。更奇怪的是,data.frame中的列子集似乎没有制作任何副本。我正在使用data.table版本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.framesetDT。看,向量ac不再被复制!基本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]

另一种方法是制作data.table的浅表副本,然后在新的data.table中通过引用将所有额外的列设为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

1 个答案:

答案 0 :(得分:5)

自从我想到这件事以来已经有一段时间了,但现在就去了。

好问题。但为什么你需要像这样对data.table进行子集化?我们真的需要看看你正在做什么 next :更大的图景。我们在data.table中可能有不同的方式,而不是基本的R习语。

粗略地说明可能是一个不好的例子:

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

而不是获取子集然后在结果外做下一步(此处为apply)的基本R习惯用法:

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

一般情况下,我们希望鼓励尽可能在[...]内进行操作,以便data.table将ijby放在一起{ {1}}操作并可以优化组合。当您对列进行子集,然后在外部执行下一项操作时,需要更多的软件复杂性来进行优化。在大多数情况下,大多数计算成本都在第一个[...]内,这会降低到相对不显着的大小。

话虽如此,除了弗兰克关于[...]的评论之外,我们还等着看ALTREP project如何发布。这改进了基本R中的引用计数,并且可以使shallow能够可靠地知道它正在运行的列是否需要首先进行写时复制。目前,:=始终通过引用进行更新,因此如果选择某些整列没有进行深层复制,它会更新data.table,因为它故意复制(因此) 。如果:=内未使用:=,则[...]始终会返回一个可以安全使用[...]的新结果,这是目前非常简单的规则。即使您所做的一切都是出于某种原因选择了几个完整的列。

我们真的需要看到更大的图片:您之后在列子集上做了什么。明确这一点将有助于提高调查ALTREP的优先级,或者为此案例做我们自己的引用计数。