首次括号内的分配与完整分配一样耗时?

时间:2013-05-07 16:47:19

标签: r copy lazy-evaluation

关于这个答案: 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提供深入的解决方案!

2 个答案:

答案 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不涉及副本。我们可以在不使用

人工增加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 

答案 1 :(得分:3)

编辑以下约书亚的评论:下面显示的行为仅限于R-studio !!

要回答OP的问题,复制的根本原因(如@MartinMorgan解释的)是由a的NAM(2)SEXP对象引起的。如果第一个命令不包含system.time(.),则a <- rep(1, 10^8)会返回NAM(1)类型,这会导致两个分配都没有副本。

在R-studio观察:

然而,要指出另一个有趣的观察/差异,如果你在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对象。因此,每次都会制作副本。