在R的替换功能中,数据是否真的被复制了四次?

时间:2014-05-27 21:08:07

标签: r memory memory-management

考虑这个变量

a = data.frame(x=1:5,y=2:6)

当我使用替换函数更改a的第一个元素时,多少次 是复制a相同大小的内存吗?

tracemem(a)
"change_first_element<-" = function(x, value) {
  x[1,1] = value
  return(x)
}
change_first_element(a) = 3
# tracemem[0x7f86028f12d8 -> 0x7f86028f1498]: 
# tracemem[0x7f86028f1498 -> 0x7f86028f1508]: change_first_element<- 
# tracemem[0x7f86028f1508 -> 0x7f8605762678]: [<-.data.frame [<- change_first_element<- 
# tracemem[0x7f8605762678 -> 0x7f8605762720]: [<-.data.frame [<- change_first_element<- 

有四种复制操作。我知道R不会改变对象或通过引用传递(是的,有例外),但为什么有四个副本?难道一个副本不够吗?

第2部分:

如果我以不同方式调用替换函数,则只有三个复制操作?

tracemem(a)
a = `change_first_element<-`(a,3)
# tracemem[0x7f8611f1d9f0 -> 0x7f8607327640]: change_first_element<- 
# tracemem[0x7f8607327640 -> 0x7f8607327758]: [<-.data.frame [<- change_first_element<- 
# tracemem[0x7f8607327758 -> 0x7f8607327800]: [<-.data.frame [<- change_first_element<-

1 个答案:

答案 0 :(得分:24)

注意:除非另有说明,否则以下所有解释均适用于R版本&lt; 3.1.0。 R v3.1.0有了很大的改进,这里也简要介绍过。

要回答您的第一个问题,&#34;为什么四个副本并且不应该足够?&#34; ,我们首先引用相关部分来自R-internals首先:

  

A&#39;命名为&#39;值2,NAM(2),表示在更改之前必须复制对象。 (请注意,这并不表示有必要复制,只是必须重复它是否必须复制。)值为0意味着已知没有其他SEXP与此对象共享数据,因此可以安全地被改变。

     

值为1用于dim(a) <- c(7, 2)之类的情况,其中原则上两个a的副本在计算期间存在(原则上)   a <- dim&lt; - (a, c(7, 2))但不再是,因此可以优化一些原始函数以避免在这种情况下复制。

NAM(1):

让我们从NAM(1)个对象开始。这是一个例子:

x <- 1:5 # (1)
.Internal(inspect(x))
# @10374ecc8 13 INTSXP g0c3 [NAM(1)] (len=5, tl=0) 1,2,3,4,5
tracemem(x)
# [1] "<0x10374ecc8>"

x[2L] <- 10L # (2)
.Internal(inspect(x))
# @10374ecc8 13 INTSXP g0c3 [MARK,NAM(1),TR] (len=5, tl=0) 1,10,3,4,5

这里发生了什么?我们使用:创建了一个整数向量,它是一个基元,导致了一个NAM(1)对象。当我们在该对象上使用[<-时,该值就地更改(请注意指针是相同的,(1)和(2))。这是因为作为原语的[<-非常清楚如何处理其输入,并且在这种情况下针对无副本进行了优化。

y = x # (3)
.Internal(inspect(x))
# @10374ecc8 13 INTSXP g0c3 [MARK,NAM(2),TR] (len=5, tl=0) 1,10,3,4,5

x[2L] <- 20L # (4)
.Internal(inspect(x))
# tracemem[0x10374ecc8 -> 0x10372f328]:
# @10372f328 13 INTSXP g0c3 [NAM(1),TR] (len=5, tl=0) 1,20,3,4,5

现在相同的作业会产生副本,为什么?通过做(3),命名为&#39;当多个对象指向相同的数据时,字段会增加到NAM(2)。即使[<-已经过优化,但它是NAM(2)的事实意味着对象必须重复。这就是为什么它在分配后再次成为NAM(1)对象的原因。这是因为,呼叫duplicate会将named设置为0,新分配会将其恢复为1.

  

注意:Peter Dalgaard很好地解释了这个案例in this link,为什么x = 2L导致了NAM(2)对象。


NAM(2):

现在让我们回复您的问题,在*<- data.frame上调用NAM(2)

第一个问题是,为什么data.frame()NAM(2)个对象?为什么不像早期案例x <- 1:5那样使用NAM(1)? Duncan Murdoch在same post上非常好地回答了这个问题:

  

data.frame()是一个普通的R函数,因此它与任何用户编写的函数没有区别。另一方面,实现:运算符的内部函数是原语,因此它可以完全控制其返回值,并且可以设置NAMED最多有效的方式。

这意味着任何更改值的尝试都会导致触发duplicate深层副本)。来自?tracemem

  

... C函数duplicate对对象的任何复制都会向标准输出生成一条消息。

因此来自tracemem的消息有助于了解副本的数量。要理解tracemem输出的第一行,让我们构造函数f<-,它不会实际替换。另外,让我们构建一个足够大的data.frame,以便我们可以衡量data.frame的单个副本所花费的时间。

## R v 3.0.3
`f<-` = function(x, value) {
    return(x) ## no actual replacement
}

df <- data.frame(x=1:1e8, y=1:1e8) # 762.9 Mb
tracemem(df) # [1] "<0x7fbccd2f4ae8>"

require(data.table)
system.time(copy(df)) 
# tracemem[0x7fbccd2f4ae8 -> 0x7fbccd2f4ff0]: copy system.time 
#   user  system elapsed 
#  0.609   0.484   1.106 

system.time(f(df) <- 3)
# tracemem[0x7fbccd2f4ae8 -> 0x7fbccd2f4f10]: system.time 
#   user  system elapsed 
#  0.608   0.480   1.101 

我使用了来自copy()的函数data.table(它基本上调用了C duplicate函数)。复制的次数或多或少相同。所以,第一步显然是深层复制,即使它没有没有

这解释了帖子中来自tracemem的前两条详细消息:

  

(1)在全球环境中,我们称之为f(df) <- 3)。这是一个副本   (2)在函数f<-内,另一个作业x[1,1] <- 3,它将调用[<-(以及[<-.data.frame函数)。这使得第二个立即复制。

debugonce()上的[<-.data.frame可轻松查找其余副本。那就是:

debugonce(`[<-`)
df <- data.frame(x=1:1e8, y=1:1e8)
`f<-` = function(x, value) {
    x[1,1] = value
    return(x)
}
tracemem(df)
f(df) = 3

# first three lines:

# tracemem[0x7f8ba33d8a08 -> 0x7f8ba33d8d50]:      (1)
# tracemem[0x7f8ba33d8d50 -> 0x7f8ba33d8a78]: f<-  (2)
# debugging in: `[<-.data.frame`(`*tmp*`, 1L, 1L, value = 3L)

点击输入,您将在此功能中找到另外两个副本:

# debug: class(x) <- NULL
# tracemem[0x7f8ba33d8a78 -> 0x7f8ba3cd6078]: [<-.data.frame [<- f<-     (3)

# debug: x[[jj]][iseq] <- vjj
# tracemem[0x7f8ba3cd6078 -> 0x7f882c35ed40]: [<-.data.frame [<- f<-     (4)

请注意class是原始的,但它是在NAM(2)对象上调用的。我怀疑是那里的副本的原因。最后一个副本是不可避免的,因为它修改了列。

所以,你去吧。


现在关于R v3.1.0的小记:

  

我也在R V3.1.0中对其进行了测试。 tracemem提供所有四行。但是,唯一耗时的步骤是(4)。 IIUC,其余案例均由[<- / class<-引起,应触发浅拷贝而不是深拷贝。令人敬畏的是,即使在(4)中,只有被修改的列似乎深度复制。 R 3.1.0有很大的改进!

     

这意味着tracemem也会因浅拷贝而提供输出 - 这有点令人困惑,因为文档没有明确说明,并且很难说明浅和深拷贝,除了测量时间。也许这是我(不正确)的理解。随意纠正我。


在你的第2部分,我将引用来自here的Luke Tierney:

  

直接调用foo<-函数不是一个好主意,除非你真正了解一般的赋值机制和特定的foo<-函数中发生了什么。除非你喜欢令人不快的惊喜,否则绝对不能在日常编程中完成任务。

但是我无法判断这些令人不快的意外是否已扩展到已经NAM(2)的对象。因为,Matt在list上调用它,这是一个原始的,因此NAM(1),并且直接调用foo<-不会增加它的名字&#39; 39;值。

但是,R v3.1.0有很大改进的事实应该已经说服你不再需要这样的函数调用了。

HTH。

PS:请随意纠正我(如果可能的话,帮我缩短这个答案):)。


修改:我似乎错过了在评论时直接调用f<-时减少副本的问题。通过使用Simon Urbanek在帖子中使用的功能(现在连接多次)很容易看到:

# rm(list=ls()) # to make sure there' no other object in your workspace
`f<-` <- function(x, value) {
    print(ls(env = parent.frame()))
}

df <- data.frame(x=1, y=2)
tracemem(df) # [1] "<0x7fce01a65358>"

f(df) = 3
# tracemem[0x7fce0359b2a0 -> 0x7fce0359ae08]: 
# [1] "*tmp*" "df"    "f<-"  

df <- data.frame(x=1, y=2)
tracemem(df) # [1] "<0x7fce03c505c0>"
df <- `f<-`(df, 3)
# [1] "df"  "f<-"

正如您所看到的,在第一种方法中,有一个对象*tmp*正在创建,而在第二种情况下则不然。似乎*tmp*输入对象的NAM(2)对象的创建在*tmp*被赋值给函数参数之前触发了输入的副本。但就我的理解而言,那就是。