向量上的修改时复制语义不会追加到循环中。为什么?
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 创建了一个额外的引用,但此操作不会明显更改地址。
这个问题听起来只有部分答案 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 创建了一个额外的引用,但此操作不会明显更改地址。