8

关于这个答案: R中的copy-on-modify语义到底是什么,规范来源在哪里?

我们可以看到,在第一次用 更改向量时'[<-',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

请注意,type/storage.mode 没有变化。

所以问题是:为什么不能优化第一个括号分配呢?在什么情况下实际上需要这种行为(第一次修改时的完整副本)?

编辑:(剧透!)正如下面接受的答案中所解释的,这只不过是在system.time函数调用中包含第一个赋值的工件。这导致 R 将绑定到的内存空间标记a为可能引用多个符号,因此在更改时需要重复。如果我们删除封闭的调用,向量会从第一个括号赋值开始修改。

感谢马丁的深入解决!

4

2 回答 2

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()inside的返回值system.time及其值 outside的表示system.timef = function() { x <- rep(1L, 10); x }; a = f()而不是的道德等价物g = function() rep(1L, 10); a = g()

真实世界的代码a <- rep(1L, 10^8); a[123L] <- 231L不会涉及副本。我们可以在不人为地增加 NAMED 计数的情况下为分配计时

> 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 
于 2013-05-07T17:10:55.943 回答
3

按照 Joshua 的评论进行编辑:下面显示的行为仅限于 R-studio!

为了回答 OP 的问题,复制的根本原因(正如@MartinMorgan 解释的那样)是由于a. 如果第一个命令不包含 a system.time(.),则a <- rep(1, 10^8)返回一个NAM(1)类型,该类型将导致两个分配都没有副本。

R-studio 中的观察:

但是,要指出另一个有趣的观察/差异,如果您要在 R-studio 中运行,您可能不知道还有一个额外的行为差异(与 R64/R32 会话的差异)。

差异(在 R 工作室中)似乎源于您运行代码的方式。也就是说,如果您一次复制并粘贴所有内容(如下所示,包括输出):

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 秒。现在,复制/粘贴/执行同一组命令,但现在是一个接一个(在输入下一行之前在每一行之后按回车键)。结果如下:

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 中,对每一行按 Enter 会导致这些分配中的每一个都返回一个 NAM(2) SEXP 对象。因此,每次都在制作副本。

于 2013-05-07T17:21:15.110 回答