Семантика копирования-на-изменении в векторе не добавляется в цикл. Зачем?

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

Более простой пример - выращивание векторов. Следующий код невероятно неэффективен в 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 ответа

Решение

Я заканчиваю @MikeH. awnser с этим кодом

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 создает дополнительные ссылки, но это действие явно не меняет адрес.

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

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

Учитывая ваше наблюдаемое поведение, я предполагаю, что вы используете RStudio который мы увидим, объяснит, почему на самом деле есть две переменные, указывающие на 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 в вашей функции y[i] = x[i] - x[i-1]:

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.

Другие вопросы по тегам