R中的copy-on-modify语义是什么,哪个是规范的源代码?
每过一段时间,我都会遇到R具有copy-on-modify语义的概念,例如在Hadley的devtools wiki中 。
大多数R对象具有copy-on-modify语义,因此修改函数参数不会改变原始值
我可以追溯到R-Help邮件列表。 例如,Peter Dalgaard在2003年7月写道:
R是一种function性语言,具有懒惰评估和弱dynamictypes(一个variables可以随意改变types:a <-1; a < – “a”是允许的)。 从语义上讲,尽pipe在实现中使用了一些优化技巧来避免最糟糕的低效率,但一切都是复制修改的。
同样,Peter Dalgaard在2004年1月写道:
R有复制修改语义(原则上有时在实践中),所以一旦一个对象的一部分发生变化,你可能不得不在新的地方寻找包含它的东西,包括可能的对象本身。
更进一步, 2000年2月, Ross Ihaka说:
我们付出了相当多的努力来实现这一目标。 我会将语义描述为“在修改时复制(如果需要)”。 仅在对象被修改时才进行复制。 (如果有必要)部分意味着如果我们能certificate修改不能改变任何非局部variables,那么我们就直接进行修改而不复制。
这不在手册中
无论我多么努力地search,我都无法在R手册中find对“copy-on-modify”的引用, R语言定义和R内部
题
我的问题有两个部分:
- 这个正式logging在哪里?
- 复制修改如何工作?
例如,说“传引用”是否合适,因为承诺会传递给函数?
呼叫按值
R语言定义说(在4.3.3参数评估 )
在R参数中调用函数的语义是按值调用的 。 通常,提供的参数的行为就好像是用提供的值和相应的forms参数的名称初始化的局部variables。 在函数内改变提供的参数值不会影响调用帧中variables的值 。 [强调添加]
虽然这并没有描述copy-on-modify的工作机制,但它确实提到了改变传递给函数的对象不会影响调用框架中的原始对象。
附加信息,特别是关于复制修改方面的内容在R内部手册中的SEXP
描述中给出, 1.1.2节其余的标题 。 具体说明[重点添加]
named
字段由SET_NAMED
和NAMED
macros设置和访问,并取值0
和2
。 R有一个“按价值调用”的幻想,所以像一个任务b <- a
似乎做了一个副本,并将其称为
b
。 但是, 如果a
和b
都没有被修改,就没有必要复制。 真正发生的是一个新的符号b
被绑定到与a相同的值,并且值对象上的named
字段被设置(在本例中为2
)。 当一个对象即将被改变时,被named
字段被查询。 值为2
意味着对象必须在被更改之前被复制。 (请注意,这并不是说需要复制,只是复制是否需要复制)值为0
意味着已知没有其他SEXP
与该对象共享数据,因此可以安全地被改变。 值为1
用于类似的情况dim(a) <- c(7, 2)
(原则上)在计算期间存在两个a的副本,
a <- `dim<-`(a, c(7, 2))
但不再使用,所以在这种情况下可以优化一些原始函数以避免复制。
虽然这并没有描述对象被作为parameter passing给函数的情况,但是我们可以推断出相同的过程在运行,尤其是在前面引用的R语言定义的信息的情况下。
承诺在function评估
我认为说一个承诺是通过这个函数是不正确的。 parameter passing给函数,实际使用的expression式被存储为promise(加上一个指向调用环境的指针)。 只有当参数被评估时,存储在promise中的expression式才会在指针所指示的环境中被检索和评估,这个过程被称为强制 。
因此,我不认为在这方面谈论引用传递是不正确的。 R具有按值的语义,但是尝试避免复制,除非传递给参数的值被评估和修改。
NAMED机制是一个优化(正如注释中的@hadley所指出的),它允许R跟踪修改后是否需要复制副本。 NAMED机制究竟是如何运作的,有一些微妙之处,正如Peter Dalgaard所讨论的( R Devel @ mnel在他们对这个问题的评论中引用的)
我做了一些实验,发现R总是在第一次修改时复制对象。
你可以在http://rpubs.com/wush978/5916我的机器上看到结果;
如果我犯了错,请告诉我,谢谢。
testing一个对象是否被复制
我用下面的C代码转储内存地址:
#define USE_RINTERNALS #include <Rh> #include <Rdefines.h> SEXP dump_address(SEXP src) { Rprintf("%16p %16p %d\n", &(src->u), INTEGER(src), INTEGER(src) - (int*)&(src->u)); return R_NilValue; }
它将打印2个地址:
-
SEXP
数据块的SEXP
-
integer
连续块的地址
我们编译并加载这个C函数。
Rcpp:::SHLIB("dump_address.c") dyn.load("dump_address.so")
会话信息
这里是testing环境的sessionInfo
。
sessionInfo()
在写上复制
首先,我在写入时testing拷贝的属性,这意味着R只在修改时才拷贝对象。
a <- 1L b <- a invisible(.Call("dump_address", a)) invisible(.Call("dump_address", b)) b <- b + 1 invisible(.Call("dump_address", b))
对象b
在修改时从b
复制。 R确实copy on write
属性copy on write
实现了copy on write
。
修改向量/matrix
然后我testing当我们修改一个向量/matrix元素时,R是否会复制对象。
vector与长度1
a <- 1L invisible(.Call("dump_address", a)) a <- 1L invisible(.Call("dump_address", a)) a[1] <- 1L invisible(.Call("dump_address", a)) a <- 2L invisible(.Call("dump_address", a))
地址每次都改变,这意味着R不重用内存。
长vector
system.time(a <- rep(1L, 10^7)) invisible(.Call("dump_address", a)) system.time(a[1] <- 1L) invisible(.Call("dump_address", a)) system.time(a[1] <- 1L) invisible(.Call("dump_address", a)) system.time(a[1] <- 2L) invisible(.Call("dump_address", a))
对于长向量,R在第一次修改后重用内存。
而且,上面的例子也显示了“就地修改”确实会影响对象很大时的性能。
matrix
system.time(a <- matrix(0L, 3162, 3162)) invisible(.Call("dump_address", a)) system.time(a[1,1] <- 0L) invisible(.Call("dump_address", a)) system.time(a[1,1] <- 1L) invisible(.Call("dump_address", a)) system.time(a[1] <- 2L) invisible(.Call("dump_address", a)) system.time(a[1] <- 2L) invisible(.Call("dump_address", a))
看起来R只在第一次修改时复制对象。
我不知道为什么。
更改属性
system.time(a <- vector("integer", 10^2)) invisible(.Call("dump_address", a)) system.time(names(a) <- paste(1:(10^2))) invisible(.Call("dump_address", a)) system.time(names(a) <- paste(1:(10^2))) invisible(.Call("dump_address", a)) system.time(names(a) <- paste(1:(10^2) + 1)) invisible(.Call("dump_address", a))
结果是一样的。 R只在第一次修改时复制对象。