在R中,“copy-on-modify”语义是什么,它的规范来源在哪里?

86
偶尔我会遇到 R 有“复制-修改”语义的概念,例如在Hadley's devtools wiki中。

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

我可以追溯这个术语到 R-Help 邮件列表。例如,Peter Dalgaard 在July 2003中写道:

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

同样,Peter Dalgaard 在Jan 2004中也写道:
R具有按需复制的语义(在原则上和有时在实践中),因此一旦对象的一部分发生更改,您可能需要查找任何包含它的新位置,包括可能是对象本身。更进一步,在2000年2月,Ross Ihaka说:

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

手册中没有

无论我如何搜索,都找不到“copy-on-modify”的参考资料,无论是在R手册中还是在R语言定义R内部中。
问题:
我的问题有两个部分:
1. 这在哪里正式记录? 2. copy-on-modify是如何工作的?
例如,是否适合谈论“按引用传递”,因为一个承诺会被传递给函数?

1
有些情况下,内部操作可能没有记录,以便为开发人员提供更改其操作方式的灵活性。在这种情况下,不应编写依赖于内部操作的代码,因为它可能会在未来出现问题。 - G. Grothendieck
2个回答

56

按值调用

R语言定义中在第4.3.3 调用参数章节中提到:

在 R 中,调用函数的语义是按值调用。一般而言,提供的参数表现为本地变量,初始化为提供的值和对应正式参数的名称。在函数内部更改提供的参数的值不会影响调用帧中变量的值。[强调部分已添加]

虽然这并没有描述复制修改机制的工作原理,但它提到了更改传递给函数的对象不会影响调用帧中的原始对象。

有关复制修改方面的详细信息,可以在R internals 手册的第1.1.2 头文件下的其余部分中查看。具体而言,它说明如下 [强调部分已添加]

named字段由SET_NAMEDNAMED宏设置和访问,可取值为012。R有一个"按值调用"的假象,所以像以下这样的赋值语句

b <- a
a产生了一个副本,并将其称为b。然而,如果之后没有修改ab,则无需复制。实际发生的是,一个新符号b绑定到与a相同的值,并在该值对象上设置named字段(在这种情况下为2)。当要修改对象时,会咨询named字段。值为2表示在修改之前必须复制对象。请注意,这并不意味着必须复制,只是应该复制,无论是否必要。值为0表示已知没有其他SEXP共享此对象的数据,因此可以安全地更改它。值为1用于类似的情况。
dim(a) <- c(7, 2)

原则上,在计算过程中会存在两个副本,因为(在原则上)

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

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

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

函数求值中的Promise

我认为说一个promise被“传递”到函数中并不完全正确。实际上,参数被传递到函数中,并且所使用的实际表达式被存储为Promise(以及指向调用环境的指针)。只有在参数被求值时,才会检索存储在Promise中的表达式并在由指针指示的环境中进行求值,这个过程称为forcing

因此,在这方面谈论按引用传递是不正确的。R具有按值调用的语义,但除非对传递给参数的值进行求值和修改,否则会尝试避免复制。

NAMED机制是一种优化(正如@hadley在评论中所提到的),它允许R跟踪是否需要在修改时进行复制。关于NAMED机制的操作方式存在一些细微差别,正如Peter Dalgaard在(R Devel线程中所讨论的,@mnel在问题评论中引用)。


3
重要的一点(应该强调)是R是按值传递。 - hadley
@hadley 但是 NAMED 的概念是否也与函数调用一起使用,存在 Promise 的附加问题呢? - Gavin Simpson
@hadley 添加了新的强调。 - Gavin Simpson
1
NAMED只是一种优化方式。即使没有它,R的行为也是完全相同的。 - hadley
非常正确@JoshO'Brien +1。我在那里解释得太多了,改变了Peter写的意图。会相应地进行编辑。 - Gavin Simpson
简洁明了,解释得很好 - 其他人可能会写上五页的博客文章,却没有提供更多的信息。谢谢! - undefined

31

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

它会打印两个地址:

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

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

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

会话信息

这里是测试环境的sessionInfo

sessionInfo()

写时复制

首先测试写时复制属性,意味着只有在对象被修改时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实现了写时复制属性。

就地修改向量/矩阵

然后我测试了一下,当我们修改向量/矩阵元素时,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只在第一次修改时复制对象。


7
非常有趣。我认为你可以发布一个问题,探讨为什么R在第一次修改/属性设置时会复制对象。 - Ferdinand.kraft

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接