首页 文章

准确理解data.table何时引用另一个data.table(与副本)

提问于
浏览
161

我在理解 data.table 的pass-by-reference属性时遇到了一些麻烦 . 有些操作似乎是'break'的参考,而我正在发生这种情况 .

在从另一个 data.table 创建 data.table (通过 <- ,然后通过 := 更新新表时,原始表也会被更改 . 这是预期的,按照:

?data.table::copystackoverflow: pass-by-reference-the-operator-in-the-data-table-package

这是一个例子:

library(data.table)

DT <- data.table(a=c(1,2), b=c(11,12))
print(DT)
#      a  b
# [1,] 1 11
# [2,] 2 12

newDT <- DT        # reference, not copy
newDT[1, a := 100] # modify new DT

print(DT)          # DT is modified too.
#        a  b
# [1,] 100 11
# [2,]   2 12

但是,如果我在 <- 赋值和上面的 := 行之间插入非 := 修改,则 DT 现在不再被修改:

DT = data.table(a=c(1,2), b=c(11,12))
newDT <- DT        
newDT$b[2] <- 200  # new operation
newDT[1, a := 100]

print(DT)
#      a  b
# [1,] 1 11
# [2,] 2 12

所以似乎 newDT$b[2] <- 200 行以某种方式'breaks'参考 . 我在我的代码中引入了潜在的错误 .

如果有人能向我解释这一点,我将非常感激 .

2 回答

  • 93

    是的,它是在R中使用 <- (或 =-> )进行子分配,它会复制整个对象 . 您可以使用 tracemem(DT).Internal(inspect(DT)) 跟踪它,如下所示 . data.table 的特征是 :=set() 通过引用它们传递的任何对象来分配 . 因此,如果该对象先前已被复制(通过子分配 <- 或显式 copy(DT) ),那么它就是通过引用修改的副本 .

    DT <- data.table(a = c(1, 2), b = c(11, 12)) 
    newDT <- DT 
    
    .Internal(inspect(DT))
    # @0000000003B7E2A0 19 VECSXP g0c7 [OBJ,NAM(2),ATT] (len=2, tl=100)
    #   @00000000040C2288 14 REALSXP g0c2 [NAM(2)] (len=2, tl=0) 1,2
    #   @00000000040C2250 14 REALSXP g0c2 [NAM(2)] (len=2, tl=0) 11,12
    # ATTRIB:  # ..snip..
    
    .Internal(inspect(newDT))   # precisely the same object at this point
    # @0000000003B7E2A0 19 VECSXP g0c7 [OBJ,NAM(2),ATT] (len=2, tl=100)
    #   @00000000040C2288 14 REALSXP g0c2 [NAM(2)] (len=2, tl=0) 1,2
    #   @00000000040C2250 14 REALSXP g0c2 [NAM(2)] (len=2, tl=0) 11,12
    # ATTRIB:  # ..snip..
    
    tracemem(newDT)
    # [1] "<0x0000000003b7e2a0"
    
    newDT$b[2] <- 200
    # tracemem[0000000003B7E2A0 -> 00000000040ED948]: 
    # tracemem[00000000040ED948 -> 00000000040ED830]: .Call copy $<-.data.table $<- 
    
    .Internal(inspect(DT))
    # @0000000003B7E2A0 19 VECSXP g0c7 [OBJ,NAM(2),TR,ATT] (len=2, tl=100)
    #   @00000000040C2288 14 REALSXP g0c2 [NAM(2)] (len=2, tl=0) 1,2
    #   @00000000040C2250 14 REALSXP g0c2 [NAM(2)] (len=2, tl=0) 11,12
    # ATTRIB:  # ..snip..
    
    .Internal(inspect(newDT))
    # @0000000003D97A58 19 VECSXP g0c7 [OBJ,NAM(2),ATT] (len=2, tl=100)
    #   @00000000040ED7F8 14 REALSXP g0c2 [NAM(2)] (len=2, tl=0) 1,2
    #   @00000000040ED8D8 14 REALSXP g0c2 [NAM(2)] (len=2, tl=0) 11,200
    # ATTRIB:  # ..snip..
    

    注意甚至 a 向量是如何被复制的(不同的十六进制值表示向量的新副本),即使 a 没有被更改 . 甚至整个 b 都被复制了,而不仅仅是改变了需要改变的元素 . 这对于避免大数据很重要,以及为什么 :=set() 被引入 data.table .

    现在,通过复制 newDT ,我们可以通过引用修改它:

    newDT
    #      a   b
    # [1,] 1  11
    # [2,] 2 200
    
    newDT[2, b := 400]
    #      a   b        # See FAQ 2.21 for why this prints newDT
    # [1,] 1  11
    # [2,] 2 400
    
    .Internal(inspect(newDT))
    # @0000000003D97A58 19 VECSXP g0c7 [OBJ,NAM(2),ATT] (len=2, tl=100)
    #   @00000000040ED7F8 14 REALSXP g0c2 [NAM(2)] (len=2, tl=0) 1,2
    #   @00000000040ED8D8 14 REALSXP g0c2 [NAM(2)] (len=2, tl=0) 11,400
    # ATTRIB:  # ..snip ..
    

    请注意,所有3个十六进制值(列点矢量和两列中的每一列)保持不变 . 因此它通过引用进行了真正的修改,完全没有副本 .

    或者,我们可以通过引用修改原始 DT

    DT[2, b := 600]
    #      a   b
    # [1,] 1  11
    # [2,] 2 600
    
    .Internal(inspect(DT))
    # @0000000003B7E2A0 19 VECSXP g0c7 [OBJ,NAM(2),ATT] (len=2, tl=100)
    #   @00000000040C2288 14 REALSXP g0c2 [NAM(2)] (len=2, tl=0) 1,2
    #   @00000000040C2250 14 REALSXP g0c2 [NAM(2)] (len=2, tl=0) 11,600
    #   ATTRIB:  # ..snip..
    

    这些十六进制值与我们在 DT 上面看到的原始值相同 . 输入 example(copy) 以获取更多使用 tracemem 的示例,并与 data.frame 进行比较 .

    顺便说一句,如果你 tracemem(DT) 然后 DT[2,b:=600] 你会看到一份报告 . 这是 print 方法执行的前10行的副本 . 当使用 invisible() 包装或在函数或脚本中调用时,不会调用 print 方法 .

    所有这些也适用于内部功能;即,即使在函数内, :=set() 也不会在写入时复制 . 如果需要修改本地副本,请在函数开头调用 x=copy(x) . 但是,请记住 data.table 适用于大数据(以及较小的数据编程优势) . 我们故意不需要允许通常的3 *工作记忆因素经验法则 . 我们尝试只需要一个大到一列的工作记忆(即工作记忆因子为1 / ncol而不是3) .

  • 123

    只是一个简单的总结 .

    <-data.table 就像基地一样;即,之后使用 <- 进行子分配(例如更改列名或更改 DT[i,j]<-v 之类的元素)之前不会复制 . 然后它就像基地一样获取整个对象的副本 . 这就是所谓的写时复制 . 我认为会更好地称为副本分配!当您使用特殊 := 运算符或 data.table 提供的 set* 函数时,它不会复制 . 如果你有大数据,你可能想要使用它们 . :=set* 将不会复制 data.table ,即使在函数内也是如此 .

    鉴于此示例数据:

    DT <- data.table(a=c(1,2), b=c(11,12))
    

    以下只是"binds"另一个名称 DT2 以相同的数据对象绑定当前绑定的名称 DT

    DT2 <- DT
    

    这永远不会复制,也永远不会复制到基地 . 它只标记数据对象,以便R知道两个不同的名称( DT2DT )指向同一个对象 . 因此,如果其中任何一个被分配给后来,R将需要复制该对象 .

    那也是 data.table 的完美之选 . := 不是这样做的 . 所以以下是故意的错误,因为 := 不仅仅是绑定对象名称:

    DT2 := DT    # not what := is for, not defined, gives a nice error
    

    := 用于通过引用进行子分配 . 但你不像在基地那样使用它:

    DT[3,"foo"] := newvalue    # not like this
    

    你这样使用它:

    DT[3,foo:=newvalue]    # like this
    

    这改变了 DT 作为参考 . 假设您通过引用数据对象添加新列 new ,则无需执行此操作:

    DT <- DT[,new:=1L]
    

    因为RHS已经通过引用改变了 DT . 额外的 DT <- 是误解 := 的作用 . 你可以在那里写,但它是多余的 .

    DT 由引用改变, := ,甚至在函数内:

    f <- function(X){
        X[,new2:=2L]
        return("something else")
    }
    f(DT)   # will change DT
    
    DT2 <- DT
    f(DT)   # will change both DT and DT2 (they're the same data object)
    

    data.table 适用于大型数据集,请记住 . 如果你的内存中有20GB data.table ,那么你需要一种方法来做到这一点 . 这是 data.table 的一个非常慎重的设计决定 .

    当然可以复制 . 您只需要告诉data.table您确定要复制20GB数据集,使用 copy() 功能:

    DT3 <- copy(DT)   # rather than DT3 <- DT
    DT3[,new3:=3L]     # now, this just changes DT3 because it's a copy, not DT too.
    

    要避免复制,请不要使用基本类型分配或更新:

    DT$new4 <- 1L                 # will make a copy so use :=
    attr(DT,"sorted") <- "a"      # will make a copy use setattr()
    

    如果您想确保通过引用更新,请使用 .Internal(inspect(x)) 并查看成分的内存地址值(请参阅Matthew Dowle的答案) .

    j 中编写 := 就可以通过组按引用进行子分配 . 您可以按组引用添加新列 . 这就是 :=[...] 内完成的原因:

    DT[, newcol:=mean(x), by=group]
    

相关问题