81

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

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

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

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

同样,Peter Dalgaard 在2004 年 1 月写道:

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

更早些,在2000 年2 月, Ross Ihaka 说:

为了实现这一点,我们付出了很多努力。我将语义描述为“修改时复制(如有必要)”。仅在修改对象时才进行复制。(如有必要)部分意味着如果我们可以证明修改不能更改任何非局部变量,那么我们只需继续修改而不复制。

说明书上没有

无论我多么努力地搜索,我都无法在R 手册中找到对“修改时复制”的引用,无论是在R 语言定义中还是在R 内部

问题

我的问题有两个部分:

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

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

4

2 回答 2

53

按值调用

R 语言定义说明了这一点(在第4.3.3 节参数评估中)

在 R 参数中调用函数的语义是按值调用。通常,提供的参数表现得好像它们是使用提供的值和相应形式参数的名称初始化的局部变量。更改函数内提供的参数的值不会影响调用框架中变量的值。[强调补充]

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

附加信息,特别是关于修改时复制方面的信息,请参见R Internals 手册1.1.2 Rest of Header部分的SEXPs描述。具体来说,它指出[强调添加]

named字段由SET_NAMED和宏设置和访问,NAMED 并取值和。R 有一个“按值调用”的 错觉,所以像这样的赋值012

b <- a

似乎制作了一份副本a并将其称为b. 但是,如果既a没有也没有b随后改变,则不需要复制。 真正发生的是新符号b绑定到与值对象上anamed字段相同的值(在本例中为2)。当一个对象即将被更改时,会查询该named字段。值2表示对象在更改之前必须被复制。(请注意,这并不是说必须复制,只是无论是否必要都应该复制。)值0表示已知没有其他 SEXP与该对象共享数据,因此可以安全地对其进行更改。的值1用于以下情况

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

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

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

但不再如此,因此可以优化一些原始函数以避免在这种情况下复制。

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

函数评估中的 Promise

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

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

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

于 2013-04-02T13:08:55.547 回答
29

我对它做了一些实验,发现 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()

写时复制

首先我测试了copy on write的属性,这意味着 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 仅在第一次修改时复制对象。

于 2013-05-04T03:18:26.750 回答