R中的“修改时复制”语义到底是什么?规范来源在哪里?


74

偶尔我会遇到R具有修改时复制语义的概念,例如在Hadley的devtools Wiki中

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

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

R是一种函数式语言,具有惰性评估和弱动态类型(变量可以随意更改类型:a <-1;允许a <-“ a”)。从语义上讲,尽管在实现过程中使用了一些优化技巧来避免最差的效率,但是一切都是按修改进行复制的。

同样,Peter Dalgaard在20041月写道:

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

再往前走,Ross Ihaka在20002月说:

我们投入了大量工作来实现这一目标。我将语义描述为“修改时复制(如有必要)”。仅在修改对象后才进行复制。(如有必要)部分意味着,如果我们可以证明修改不能更改任何非局部变量,那么我们就可以继续进行修改而无需复制。

它不在手册中

不管我多么努力,我都无法在R手册R语言定义R Internals)中找到“修改时复制”的参考。

我的问题分为两部分:

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

例如,既然将诺言传递给函数,那么谈论“通过引用传递”是否合适?


在某些情况下,可能不会记录内部结构,以使开发人员有改变其工作方式的余地。在那种情况下,不应编写依赖于内部操作的代码,因为它将来可能会中断。
G. Grothendieck

Answers:


49

按值致电

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

在R参数中调用函数的语义是按值调用。通常,提供的参数的行为就像是使用提供的值和相应形式参数的名称初始化的局部变量一样。更改函数中提供的参数的值不会影响调用框架中变量的值。[强调已添加]

尽管这里没有描述修改时复制的工作机制,但确实提到了更改传递给函数的对象不会影响调用框架中的原始对象。

附加信息,尤其是有关“修改时复制”的信息,SEXPR Internals手册的1.1.2“标题其余部分”中对s的说明中提供。具体来说,它指出[强调已添加]

named字段由SET_NAMEDNAMED 宏设置和访问,并使用01和值2。R有一个“按价值致电”的 错觉,所以分配像

b <- a

似乎制作了副本,a并称为b。然而,如果没有ab随后改变就没有必要复制。 实际发生的情况是,将一个新符号b绑定到与相同的值,a并且named将value对象上的字段设置为(在这种情况下为2)。当将要更改对象时,请查阅该named字段。值2表示在更改之前必须复制对象。(请注意,这并不是说有必要重复,而只是说是否有必要进行重复。)value的值0意味着没有其他 SEXP与该对象共享数据,因此可以安全地对其进行更改。的值1用于以下情况

dim(a) <- c(7, 2)

原则上在计算期间存在a的两个副本,如(原则上)

a <- `dim<-`(a, c(7, 2))

但不再这样做了,因此可以优化某些原始函数,以免出现这种情况下的复制。

尽管这并未描述将对象作为参数传递给函数的情况,但我们可能会推断出相同的过程在运行,特别是考虑到先前引用的R语言定义中的信息。

功能评估的承诺

我不认为这是非常正确的说,一个承诺传递给函数。参数被传递给函数,并且所使用的实际表达式存储为Promise(加上指向调用环境的指针)。仅当对参数进行求值时,才会在指针所指示的环境内检索并评估存储在promise中的表达式,该过程称为forcing

因此,我认为在这方面谈论通过引用是不正确的。R具有按值调用的语义,但会尝试避免复制,除非对传递给自变量的值进行了评估和修改。

NAMED机制是一种优化(如@hadley在评论中指出的那样),它允许R跟踪是否需要在修改时进行复制。正如Peter Dalgaard所讨论的那样,NAMED机制的确切运行方式涉及一些微妙之处(R Devel线程@mnel在对问题的评论中引用)


1
重要的一点(应该强调)是R是按值致电
hadley

@hadley,但是这个NAMED概念是否也与函数调用一起使用,以及其他的promises问题?
加文·辛普森

@hadley添加了新的重点。
加文·辛普森

NAMED只是一种优化。没有它,R的行为将相同。
hadley

非常正确@ JoshO'Brien +1。我在那里的措辞太多了,改变了彼得写的意图。将进行相应的编辑。
加文·辛普森

27

我对此进行了一些实验,发现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;
}

它将打印2个地址:

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

让我们编译并加载此C函数。

Rcpp:::SHLIB("dump_address.c")
dyn.load("dump_address.so")

会议信息

这是sessionInfo测试环境。

sessionInfo()

写时复制

首先,我在write上测试copy的属性,这意味着R仅在修改对象时才复制该对象。

a <- 1L
b <- a
invisible(.Call("dump_address", a))
invisible(.Call("dump_address", b))
b <- b + 1
invisible(.Call("dump_address", b))

对象在修改时b从复制a。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))

地址每次都会更改,这意味着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))

对于长向量,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))

似乎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仅在第一次修改时复制对象。


5
+1。很有意思。我想您可以提出一个问题,为什么R会在第一个修改/属性设置上复制对象。
Ferdinand.kraft
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.