关于这个答案: What exactly is copy-on-modify semantics in R, and where is the canonical source?
我们可以看到,在第一次用'[<-'
改变向量时,即使只修改一个条目,R也会复制整个向量。然而,在第二次,矢量“就地”改变。如果我们测量创建和修改大型向量的时间,那么在不检查对象地址的情况下这是显而易见的:
> system.time(a <- rep(1L, 10^8))
user system elapsed
0.15 0.17 0.31
> system.time(a[222L] <- 111L)
user system elapsed
0.26 0.08 0.34
> system.time(a[333L] <- 111L)
user system elapsed
0 0 0
请注意,/ storage.mode类型没有变化。
所以问题是:为什么不能优化第一个括号分配呢?在什么情况下实际需要这种行为(第一次修改时的完整拷贝)?
编辑:(剧透!)正如下面接受的答案中所解释的,这只不过是在system.time
函数调用中包含第一个赋值的工件。这导致R将绑定到a
的内存空间标记为可能引用多个符号,因此在更改时需要重复。如果我们删除封闭的调用,则会从第一个括号分配中修改向量。
感谢Martin提供深入的解决方案!
答案 0 :(得分:9)
比较
的“NAM()”部分> a <- rep(1L, 10)
> .Internal(inspect(a))
@457b840 13 INTSXP g0c4 [NAM(1)] (len=10, tl=0) 1,1,1,1,1,...
与
> system.time(a <- rep(1L, 10))
[...]
> .Internal(inspect(a))
@4626f88 13 INTSXP g0c4 [NAM(2)] (len=10, tl=0) 1,1,1,1,1,...
第一个示例中的“1”表示R认为对a
有一个引用,因此可以就地更新。 “2”表示R认为至少有两个对a
的引用,因此如果修改则需要重复。粗略地说,我将此合理化为rep()
在system.time
内的返回值的表示,其值在system.time
之外;道德等同于f = function() { x <- rep(1L, 10); x }; a = f()
而不是g = function() rep(1L, 10); a = g()
。
现实世界代码a <- rep(1L, 10^8); a[123L] <- 231L
不涉及副本。我们可以在不使用
> a <- rep(1L, 10^8)
> .Internal(inspect(a))
@7f972b571010 13 INTSXP g0c7 [NAM(1)] (len=100000000, tl=0) 1,1,1,1,1,...
> system.time(a[123L] <- a[321L])
user system elapsed
0 0 0
答案 1 :(得分:3)
要回答OP的问题,复制的根本原因(如@MartinMorgan解释的)是由a
的NAM(2)SEXP对象引起的。如果第一个命令不包含system.time(.)
,则a <- rep(1, 10^8)
会返回NAM(1)
类型,这会导致两个分配都没有副本。
然而,要指出另一个有趣的观察/差异,如果你在R-studio中运行,你可能不会注意到另外的行为差异(来自R64 / R32会话)。
差异(在R studio中)似乎源于如何运行代码。也就是说,如果您一次复制并粘贴所有内容(如下所示,包括输出):
system.time(a <- rep(1L, 10^8))
# user system elapsed
# 0.256 0.263 0.526
.Internal(inspect(a))
# @10745d000 13 INTSXP g0c7 [NAM(2)] (len=100000000, tl=0) 1,1,1,1,1,...
system.time(a[222L] <- 111L)
# user system elapsed
# 0.299 0.199 0.498
.Internal(inspect(a))
# @11f1d6000 13 INTSXP g0c7 [NAM(1)] (len=100000000, tl=0) 1,1,1,1,1,...
system.time(a[333L] <- 111L)
# user system elapsed
# 0 0 0
.Internal(inspect(a))
# @11f1d6000 13 INTSXP g1c7 [MARK,NAM(1)] (len=100000000, tl=0) 1,1,1,1,1,...
您看到第二个分配不涉及内存复制,所需时间为0秒。现在,复制/粘贴/执行相同的命令集,但现在逐个(在键入下一行之前在每行之后按Enter键)。结果如下:
system.time(a <- rep(1L, 10^8))
# user system elapsed
# 0.256 0.265 0.588
>
.Internal(inspect(a))
# @10745d000 13 INTSXP g0c7 [NAM(2)] (len=100000000, tl=0) 1,1,1,1,1,...
system.time(a[222L] <- 111L)
# user system elapsed
# 0.302 0.204 0.559
.Internal(inspect(a))
# @11f1d6000 13 INTSXP g0c7 [NAM(2)] (len=100000000, tl=0) 1,1,1,1,1,...
system.time(a[333L] <- 111L)
# user system elapsed
# 0.296 0.208 0.504
>
.Internal(inspect(a))
# @10745d000 13 INTSXP g0c7 [NAM(2)] (len=100000000, tl=0) 1,1,1,1,1,...
对于相同的语法,这里正在制作副本,运行时为0.5秒。
现在解释一下差异(正如@MartinMorgan在他的回答中所解释的那样):
对于第一种情况,作为NAM(2)SEXP对象,它在赋值期间会重复。但是,当您一次运行所有行时,在第一种情况下只会发生一次。另外需要注意的是,第二个赋值有一个MARK(unsigned int),表示“使用中标记对象”(来自R-internals)。
在第二种情况下,在R-studio中,为每一行命中输入会导致这些分配中的每一个返回一个NAM(2)SEXP对象。因此,每次都会制作副本。