kda*_*ria 22 memory memory-management r
考虑这个变量
a = data.frame(x=1:5,y=2:6)
Run Code Online (Sandbox Code Playgroud)
当我使用替换函数更改第一个元素时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<-
Run Code Online (Sandbox Code Playgroud)
有四种复制操作.我知道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<-
Run Code Online (Sandbox Code Playgroud)
Aru*_*run 26
注意:除非另有说明,否则以下所有解释均适用于R版本<3.1.0.R v3.1.0有很大的改进,这里也简要介绍了一下.
要回答你的第一个问题,"为什么四个副本,不应该一个就够了?" ,我们首先引用R-internals的相关部分:
"命名"值为2,NAM(2),表示在更改之前必须复制对象.(请注意,这并不表示有必要复制,只是必须重复它是否必须复制.)值为0意味着已知没有其他SEXP与此对象共享数据,因此可以安全地被改变.
值1用于这样的情况,例如
dim(a) <- c(7, 2)
原则上在计算期间存在两个副本a(原则上)a <-
dim < -(a, c(7, 2))
但不再存在,因此可以优化一些基本函数以避免复制案件.
让我们从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
Run Code Online (Sandbox Code Playgroud)
这里发生了什么事?我们创建了一个整数向量:
,它是一个基元,导致了一个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
Run Code Online (Sandbox Code Playgroud)
现在相同的作业会产生副本,为什么?通过执行(3),'named'字段增加到NAM(2),因为多个对象指向相同的数据.即使[<-
被优化,这也NAM(2)
意味着必须复制对象.这就是为什么它现在再次成为NAM(1)
作业之后的一个对象.那是因为,调用duplicate
设置named
为0并且新分配将其恢复为1.
注意:Peter Dalgaard 在这个链接中很好地解释了为什么
x = 2L
NAM(2)对象的结果.
现在,让我们回到你的问题上调用*<-
上data.frame
这是一个NAM(2)
对象.
那么第一个问题是,为什么是data.frame()
一个NAM(2)
对象?为什么不像之前的情况那样使用NAM(1)x <- 1:5
?Duncan Murdoch在同一篇文章中非常好地回答了这个问题:
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
Run Code Online (Sandbox Code Playgroud)
我使用了函数copy()
from 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)
Run Code Online (Sandbox Code Playgroud)
通过按Enter键,您将在此函数中找到另外两个副本:
# debug: class(x) <- NULL
# tracemem[0x7f8ba33d8a78 -> 0x7f8ba3cd6078]: [<-.data.frame [<- f<- (3)
# debug: x[[jj]][iseq] <- vjj
# tracemem[0x7f8ba3cd6078 -> 0x7f882c35ed40]: [<-.data.frame [<- f<- (4)
Run Code Online (Sandbox Code Playgroud)
请注意,它class
是原始的,但它是在NAM(2)对象上调用的.我怀疑那是副本的原因.最后一个副本是不可避免的,因为它修改了列.
所以,你去吧.
现在一个小小的说明R v3.1.0
:
我也测试过相同的
R V3.1.0
.tracemem
提供所有四条线.然而,唯一耗时的步骤是(4).IIUC,其余情况下,所有因[<-
/class<-
应该是触发浅拷贝,而不是深复制.令人敬畏的是,即使在(4)中,只有被修改的列似乎被深层复制.R 3.1.0有很大的改进!这意味着
tracemem
由于浅拷贝也提供了输出- 这有点令人困惑,因为文档没有明确说明这一点,并且除了通过测量时间之外,很难在浅拷贝和深拷贝之间分辨.也许这是我(不正确)的理解.随意纠正我.
在你的第2部分,我将从这里引用Luke Tierney :
foo<-
直接调用函数不是一个好主意,除非您真正了解一般的赋值机制和特定foo<-
函数中发生了什么.除非你喜欢令人不快的惊喜,否则绝对不能在日常编程中完成任务.
但我无法判断这些令人不快的意外是否扩展到已经存在的对象NAM(2)
.因为,Matt在a上调用它list
,这是一个原语,因此是NAM(1),并且foo<-
直接调用不会增加它的'named'值.
但是,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<-"
Run Code Online (Sandbox Code Playgroud)
如您所见,在第一种方法中,有一个*tmp*
正在创建的对象,在第二种情况下不是.似乎这个输入对象的*tmp*
对象创建NAM(2)
在*tmp*
被赋值给函数参数之前触发了输入的副本.但就我的理解而言.