向量上的修改时复制语义不会追加到循环中。为什么?

Copy-on-modify semantic on a vector does not append in a loop. Why?

这个问题听起来只有部分答案 here 但这对我来说还不够具体。我想更好地理解何时通过引用更新对象以及何时复制对象。

更简单的例子是矢量增长。以下代码在 R 中效率极低,因为在循环之前没有分配内存,并且在每次迭代时都创建了一个副本。

  x = runif(10)
  y = c() 

  for(i in 2:length(x))
    y = c(y, x[i] - x[i-1])

分配内存可以保留一些内存,而无需在每次迭代时重新分配内存。因此,这段代码速度要快得多,尤其是对于长向量。

  x = runif(10)
  y = numeric(length(x))

  for(i in 2:length(x))
    y[i] = x[i] - x[i-1]

我的问题来了。实际上,当矢量更新时,它 移动。有一个复制出来如下图

a = 1:10
pryr::tracemem(a)
[1] "<0xf34a268>"
a[1] <- 0L
tracemem[0xf34a268 -> 0x4ab0c3f8]:
a[3] <-0L
tracemem[0x4ab0c3f8 -> 0xf2b0a48]:  

但是在一个循环中这个副本不会发生

y = numeric(length(x))
for(i in 2:length(x))
{
   y[i] = x[i] - x[i-1]
   print(address(y))
}

给予

[1] "0xe849dc0"
[1] "0xe849dc0"
[1] "0xe849dc0"
[1] "0xe849dc0"
[1] "0xe849dc0"
[1] "0xe849dc0"
[1] "0xe849dc0"
[1] "0xe849dc0"
[1] "0xe849dc0" 

我理解为什么代码根据内存分配变慢或变快,但我不理解 R 逻辑。为什么以及如何,对于同一条语句,在一种情况下更新是通过引用进行的,而在另一种情况下更新是通过复制进行的。在一般情况下,我们怎么知道会发生什么。

Hadley 的 Advanced R 书中对此进行了介绍。他在其中说(在这里解释)每当 2 个或更多变量指向同一个对象时,R 将制作一个副本,然后修改该副本。在进入示例之前,Hadley 的书中也提到了一个重要的注意事项,即当您使用 RStudio

the environment browser makes a reference to every object you create on the command line.

鉴于您观察到的行为,我假设您正在使用 RStudio,我们将看到这将解释为什么实际上有 2 个变量指向 a 而不是您可能期望的 1 个。

我们将用来检查有多少变量指向一个对象的函数是 refs()。在您发布的第一个示例中,您可以看到:

library(pryr)
a = 1:10
refs(x)
#[1] 2

这表明(这是您发现的)2 个变量指向 a,因此对 a 的任何修改都会导致 R 复制它,然后修改该副本。

检查 for loop 我们可以看到 y 总是有相同的地址,并且 refs(y) = 1 在 for 循环中。 y 未被复制,因为在您的函数 y[i] = x[i] - x[i-1]:

中没有其他引用指向 y
for(i in 2:length(x))
{
  y[i] = x[i] - x[i-1]
  print(c(address(y), refs(y)))
}

#[1] "0x19c3a230" "1"         
#[1] "0x19c3a230" "1"         
#[1] "0x19c3a230" "1"         
#[1] "0x19c3a230" "1"         
#[1] "0x19c3a230" "1"         
#[1] "0x19c3a230" "1"         
#[1] "0x19c3a230" "1"         
#[1] "0x19c3a230" "1"         
#[1] "0x19c3a230" "1" 

另一方面,如果在 for loop 中引入 y 非原始 函数,您会看到 y 的地址每次都更改,更符合我们的预期:

is.primitive(lag)
#[1] FALSE

for(i in 2:length(x))
{
  y[i] = lag(y)[i]
  print(c(address(y), refs(y)))
}

#[1] "0x19b31600" "1"         
#[1] "0x19b31948" "1"         
#[1] "0x19b2f4a8" "1"         
#[1] "0x19b2d2f8" "1"         
#[1] "0x19b299d0" "1"         
#[1] "0x19b1bf58" "1"         
#[1] "0x19ae2370" "1"         
#[1] "0x19a649e8" "1"         
#[1] "0x198cccf0" "1"  

注意 非原始 的强调。如果你的 y 函数是原始的,比如 - 像: y[i] = y[i] - y[i-1] R 可以优化这个以避免复制。

感谢@duckmayr 帮助解释 for 循环内的行为。

我完成了@MikeH。使用此代码的遮阳篷

library(pryr)

x = runif(10)
y = numeric(length(x))
print(c(address(y), refs(y)))

for(i in 2:length(x))
{
  y[i] = x[i] - x[i-1]
  print(c(address(y), refs(y)))
}

print(c(address(y), refs(y)))

输出清楚地显示了发生了什么

[1] "0x7872180" "2"        
[1] "0x765b860" "1"        
[1] "0x765b860" "1"        
[1] "0x765b860" "1"        
[1] "0x765b860" "1"        
[1] "0x765b860" "1"        
[1] "0x765b860" "1"        
[1] "0x765b860" "1"        
[1] "0x765b860" "1"        
[1] "0x765b860" "1" 
[1] "0x765b860" "2"  

第一次迭代有一个副本。确实因为 Rstudio 有 2 个参考。但是在第一个副本之后 y 属于循环并且在全局环境中不可用。然后,Rstudio 不会创建任何额外的引用,因此在下一次更新期间不会创建任何副本。 y 通过引用更新。循环退出 y 在全局环境中变得可用。 Rstudio 创建了一个额外的引用,但此操作不会明显更改地址。