R中的copy-on-modify语义到底是什么,以及规范源在哪里?

And*_*rie 71 r pass-by-reference pass-by-value

每隔一段时间我就会遇到R具有复制修改语义的概念,例如在Hadley的devtools wiki中.

大多数R对象具有复制修改语义,因此修改函数参数不会更改原始值

我可以将这个术语追溯到R-Help邮件列表.例如,Peter Dalgaard在20037月写道:

R是一种函数式语言,具有惰性求值和弱动态类型(变量可以随意更改类型:a < - 1; a < - "a"是允许的).从语义上讲,一切都是复制修改,尽管在实现中使用了一些优化技巧来避免最严重的低效率.

同样,Peter Dalgaard在20041月写道:

R具有复制修改语义(原则上,有时在实践中),因此一旦对象的一部分发生变化,您可能必须在新的位置查找包含它的任何内容,包括可能的对象本身.

更进一步,20002月, Ross Ihaka说:

我们付出了相当多的努力来实现这一目标.我会将语义描述为"复制修改(如有必要)".仅在修改对象时才进行复制.(如果需要)部分意味着如果我们可以证明修改不能改变任何非局部变量,那么我们就可以继续修改而不需要复制.

它不在手册中

无论我搜索多么努力,我都无法在R手册中找到"复制修改"的参考,无论是在R语言定义还是在R Internals中

我的问题有两个部分:

  1. 这在哪里正式记录?
  2. 复制修改如何工作?

例如,谈论"传递引用"是否合适,因为承诺传递给函数?

Rei*_*son 43

呼叫按值

R语言定义说,这(在第4.3.3论证评价)

在R参数中调用函数的语义是call-by-value.通常,提供的参数的行为就像它们是使用提供的值初始化的局部变量以及相应的形式参数的名称一样.更改函数内提供的参数的值不会影响调用帧中变量的值.[强调补充]

虽然这没有描述复制修改工作的机制,但它确实提到更改传递给函数的对象不会影响调用框架中的原始对象.

其他信息,特别是有关复制修改方面SEXP的信息,请参阅R Internals手册1.1.2节" 其余标题"中s 的说明.具体来说它指出[强调增加]

named字段由SET_NAMEDNAMED 宏设置和访问,并获取值0,12.R有一个'按价值呼叫'的 幻觉,所以一个如此的作业

b <- a
Run Code Online (Sandbox Code Playgroud)

似乎复制a并将其称为b.但是,如果既没有a也没有b随后改变,则无需复制. 真正发生的是新符号b绑定到相同的值,a并且named值对象上的字段被设置(在本例中为2).当一个物体即将被改变时,该named场地被咨询.值2表示在更改之前必须复制对象.(请注意,这并不表示有必要复制,只是必须复制它是否重复.)值0意味着已知没有其他人 SEXP与此对象共享数据,因此可以安全地更改.值的值1用于类似的情况

dim(a) <- c(7, 2)
Run Code Online (Sandbox Code Playgroud)

其中原则上在计算期间存在两个副本(原则上)

a <- `dim<-`(a, c(7, 2))
Run Code Online (Sandbox Code Playgroud)

但不再是,因此可以优化一些原始函数以避免在这种情况下的副本.

虽然这没有描述将对象作为参数传递给函数的情况,但我们可以推断出相同的进程在运行,特别是考虑到前面引用的R语言定义的信息.

功能评估的承诺

我不认为这是非常正确的说,一个承诺传递给函数.参数传递给函数,使用的实际表达式存储为promises(加上指向调用环境的指针).只有在参数被评估时,才会存储在由指针指示的环境中检索和评估的promise中的表达式,这个过程称为强制.

因此,我认为在这方面谈论传递参考是不正确的.R具有按值调用语义,但尝试避免复制,除非评估和修改传递给参数的值.

NAMED机制是一种优化(如评论中的@hadley所述),它允许R跟踪是否需要在修改时进行复制.正如Peter Dalgaard所讨论的那样,NAMED机制的运作方式确实存在一些细微之处(在R Devel线程 @mnel引用他们对该问题的评论中)

  • 重要的一点(应该强调)是 R 是 **call-by-value** (2认同)

wus*_*978 26

我做了一些实验,发现R总是在第一次修改时复制对象.

您可以在我的机器上查看结果,网址为http://rpubs.com/wush978/5916

如果我犯了任何错误,请告诉我,谢谢.


测试是否复制了对象

我使用以下C代码转储内存地址:

#define USE_RINTERNALS
#include <R.h>
#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;
}
Run Code Online (Sandbox Code Playgroud)

它会打印2个地址:

  • 数据块的地址 SEXP
  • 连续块的地址 integer

让我们编译并加载这个C函数.

Rcpp:::SHLIB("dump_address.c")
dyn.load("dump_address.so")
Run Code Online (Sandbox Code Playgroud)

会话信息

这是sessionInfo测试环境.

sessionInfo()
Run Code Online (Sandbox Code Playgroud)

写入时复制

首先,我在写入时测试copy的属性,这意味着R仅在修改时才复制对象.

a <- 1L
b <- a
invisible(.Call("dump_address", a))
invisible(.Call("dump_address", b))
b <- b + 1
invisible(.Call("dump_address", b))
Run Code Online (Sandbox Code Playgroud)

对象ba修改中复制.R确实实现了这个copy on write属性.

修改矢量/矩阵到位

然后我测试当我们修改矢量/矩阵的元素时R是否会复制对象.

矢量与长度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))
Run Code Online (Sandbox Code Playgroud)

地址每次都会改变,这意味着R不会重复使用内存.

长矢量

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))
Run Code Online (Sandbox Code Playgroud)

对于长向量,R在第一次修改后重用内存.

此外,上面的例子还表明,"就地修改"确实会影响对象庞大时的性能.

矩阵

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))
Run Code Online (Sandbox Code Playgroud)

似乎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))
Run Code Online (Sandbox Code Playgroud)

结果是一样的.R仅在第一次修改时复制对象.

  • +1.很有意思.我想你可以发一个关于为什么R在第一个修改/属性设置上复制对象的问题. (5认同)