在 R 中,复制-修改语义到底是什么,规范来源在哪里?

每隔一段时间,我都会遇到 R 有 基于修改的拷贝语义的概念,例如在 Hadley 的 Devtools 维基中。

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

我可以将这个术语追溯到 R-Help 邮件列表,例如,Peter Dalgaard 在 二零零三年七月中写道:

R 是一种函数式语言,具有惰性计算和弱动态性 类型(变量可以随意改变类型: a <-1; a <-“ a”是 从语义上讲,所有东西都是基于修改的拷贝 在实现中使用了优化技巧,以避免出现最坏的情况 效率低下。

同样,彼得•达尔加德(Peter Dalgaard)在 至2004年1月底中写道:

R 具有基于修饰的拷贝语义(原则上,有时在 所以一旦一个对象的一部分发生变化,你可能不得不查看 任何包含它的东西的新地方,可能包括 物体本身。

甚至在更早的时候,罗斯 · 伊哈卡(Ross Ihaka)在 二零零零年二月上说:

我们为此付出了很多努力 语义作为“复制修改(如果必要)”。复制完成 只有当对象被修改的时候。(如果必要的话)部分意味着如果 我们可以证明,修改不能改变任何非局部 变量,那么我们只需要继续修改,不需要复制。

手册里没有

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

提问

我的问题分为两部分:

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

例如,既然 我保证被传递给函数,那么讨论“通过引用传递”是否合适?

9340 次浏览

按价值打电话

语言定义是这么说的(在 4.3.3论点评估部分)

在 R 参数中调用函数的语义是 按价值计算的电话。通常,提供的参数的行为就好像它们是用提供的值和相应形式参数的名称初始化的局部变量。更改函数中提供的参数的值不会影响调用框架中变量的值.[加重语气]

虽然这没有描述 修改后的拷贝工作的机制,但是它提到了更改传递给函数的对象不会影响调用框架中的原始对象。

更多的信息,特别是关于 修改后的拷贝方面的信息,在 内部手册1.1.2标题的其余部分部分的 SEXP描述中给出。具体来说,它说明[强调添加]

named字段由 SET_NAMEDNAMED设置和访问 宏,并取值 012。 R 有一个 ‘按价值调用’ 幻觉,所以作业就像

b <- a

显示为 a的副本,并称为 b ab后来都没有改变,不需要复制。 实际发生的情况是,一个新的符号 b绑定到相同的 值设置为 a,值对象上的 named字段设置为(在 当一个对象即将被修改时,named字段 值 2意味着对象必须被复制 在被改变之前。(注意,这并没有说它是 必须复制,只是应该复制是否 0的值表示已知没有其他 SEXP与此对象共享数据,因此可以安全地对其进行更改。 值 1用于以下情况

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

原则上,在有效期内存在两份 (原则上)计算

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

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

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

功能评估的承诺

我不认为说 我保证是函数的 通过了是完全正确的。参数被传递给函数,实际使用的表达式作为承诺存储(加上一个指向调用环境的指针)。只有当一个参数被求值时,表达式才会存储在允诺中,并在指针指示的环境中被检索和求值,这个过程称为 强迫

因此,我不认为在这方面谈论 通过引用传递是正确的。R 具有 按价值计算的电话语义,但是试图避免复制,除非计算和修改传递给参数的值。

NAMED 机制是一种优化(如@hadley 在评论中提到的) ,它允许 R 跟踪是否需要在修改后进行复制。正如 Peter Dalgaard 所讨论的(在 发展线程@mnel 中引用了他们对这个问题的评论) ,NAMED 机制的确切运作方式涉及到一些微妙之处

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

抄写

首先测试 书面复印的性质, 这意味着 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 是否会复制对象。

长度为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 只在第一次修改时复制对象。