Семантика Copy-on-modify на векторе не добавляется в цикле. Почему?


На этот вопрос, кажется, можно частично ответить здесь, но это недостаточно конкретно для меня. Я хотел бы лучше понять, когда объект обновляется по ссылке и когда он копируется.

Более простой пример-векторное выращивание. Следующий код крайне неэффективен в 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. Почему и как, для того же утверждения, в случае обновление производится по ссылке, а в другом случае обновление производится по копии. В общем случае, как мы можем знать, что произойдет?
2 9

2 ответа:

Это описано в книге Хэдли "Advanced R book". В нем он говорит (перефразируя здесь), что всякий раз, когда 2 или более переменных указывают на один и тот же объект, R сделает копию, а затем изменит эту копию. Прежде чем перейти к примерам, одно важное замечание, которое также упоминается в книге Хэдли, заключается в том, что при использовании RStudio

Обозреватель среды делает ссылку на каждый объект, созданный в командной строке.

Учитывая ваше наблюдаемое поведение, я предполагаю, что вы использование 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" 

С другой стороны, если ввести непримитивную функцию y в вашем for loop, вы увидите, что адрес 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 создает дополнительные ссылки, но это действие явно не изменяет адрес.