Действительно ли данные действительно скопированы четыре раза в функции замены R?

Рассмотрим эту переменную

a = data.frame(x=1:5,y=2:6)

Когда я использую функцию замены для изменения первого элемента a, сколько раз память с таким же размером a скопирована?

tracemem(a)
"change_first_element<-" = function(x, value) {
  x[1,1] = value
  return(x)
}
change_first_element(a) = 3
# tracemem[0x7f86028f12d8 -> 0x7f86028f1498]: 
# tracemem[0x7f86028f1498 -> 0x7f86028f1508]: change_first_element<- 
# tracemem[0x7f86028f1508 -> 0x7f8605762678]: [<-.data.frame [<- change_first_element<- 
# tracemem[0x7f8605762678 -> 0x7f8605762720]: [<-.data.frame [<- change_first_element<- 

Существует четыре операции копирования. Я знаю, что R не мутирует объекты или не проходит по ссылке (да, есть исключения), но почему есть четыре копии? Должна ли быть недостаточно одной копии?

Часть 2:

Если я вызываю функцию замены по-разному, есть только три операции копирования?

tracemem(a)
a = `change_first_element<-`(a,3)
# tracemem[0x7f8611f1d9f0 -> 0x7f8607327640]: change_first_element<- 
# tracemem[0x7f8607327640 -> 0x7f8607327758]: [<-.data.frame [<- change_first_element<- 
# tracemem[0x7f8607327758 -> 0x7f8607327800]: [<-.data.frame [<- change_first_element<-

Ответы

Ответ 1

ПРИМЕЧАНИЕ. Если не указано иное, все пояснения ниже действительны для версий R < 3.1.0. В R v3.1.0 есть большие улучшения, о которых также кратко говорится здесь.

Чтобы ответить на ваш первый вопрос: "Почему четыре копии и их не должно быть достаточно?", мы начнем с цитирования соответствующей части из R- внутренности сначала:

Значение "named" 2, NAM (2) означает, что объект должен быть дублирован перед изменением. (Обратите внимание, что это не означает, что необходимо дублировать, только чтобы он дублировался независимо от того, нужно ли это или нет.) Значение 0 означает, что известно, что ни один другой SEXP не делится данными с этим объектом, и поэтому он может безопасно быть изменен.

Значение 1 используется для таких ситуаций, как dim(a) <- c(7, 2), где в принципе две копии a существуют на время вычисления как (в принципе) a <- dim < - (a, c(7, 2)), но больше не существует, и поэтому некоторые примитивные функции могут быть оптимизированы, чтобы избежать копирования в этом случае.

ДН (1):

Начнем с объектов NAM(1). Вот пример:

x <- 1:5 # (1)
.Internal(inspect(x))
# @10374ecc8 13 INTSXP g0c3 [NAM(1)] (len=5, tl=0) 1,2,3,4,5
tracemem(x)
# [1] "<0x10374ecc8>"

x[2L] <- 10L # (2)
.Internal(inspect(x))
# @10374ecc8 13 INTSXP g0c3 [MARK,NAM(1),TR] (len=5, tl=0) 1,10,3,4,5

Что здесь происходит? Мы создали целочисленный вектор, используя :, являющийся примитивным, в результате был создан объект NAM (1). И когда мы использовали [<- для этого объекта, значение было изменено на месте (обратите внимание, что указатели идентичны, (1) и (2)). Это связано с тем, что [<-, являющийся примитивом, хорошо знает, как обрабатывать свои входы и оптимизирован для копирования без копии в этом сценарии.

y = x # (3)
.Internal(inspect(x))
# @10374ecc8 13 INTSXP g0c3 [MARK,NAM(2),TR] (len=5, tl=0) 1,10,3,4,5

x[2L] <- 20L # (4)
.Internal(inspect(x))
# tracemem[0x10374ecc8 -> 0x10372f328]:
# @10372f328 13 INTSXP g0c3 [NAM(1),TR] (len=5, tl=0) 1,20,3,4,5

Теперь одно и то же задание приводит к копированию, почему? Делая (3), поле "named" увеличивается до NAM (2), так как несколько объектов указывают на одни и те же данные. Даже если [<- оптимизирован, тот факт, что он a NAM(2) означает, что объект должен быть дублирован. Вот почему теперь это снова объект NAM(1) после назначения. Это потому, что вызов duplicate устанавливает named в 0 и новое присваивание возвращает его к 1.

Примечание: Питер Далгаард прекрасно объясняет этот случай в связи с тем, почему x = 2L приводит к объекту NAM (2).


ДН (2):

Теперь вернемся к вашему вопросу о вызове *<- в data.frame, который является объектом NAM(2).

Первый вопрос заключается в том, почему объект data.frame() a NAM(2)? Почему не NAM (1), как предыдущий случай x <- 1:5? Duncan Murdoch очень хорошо отвечает на тот же пост:

data.frame() - простая функция R, поэтому она обрабатывается не иначе, как любая написанная пользователем функция. С другой стороны, внутренняя функция, реализующая оператор :, является примитивной, поэтому она имеет полный контроль над возвращаемым значением и может самым эффективным образом установить named.

Это означает, что любая попытка изменить значение приведет к запуску duplicate (глубокой копии). Из ?tracemem:

... любое копирование объекта с помощью функции C duplicate выводит сообщение на стандартный вывод.

Таким образом, сообщение из tracemem помогает понять количество копий. Чтобы понять первую строку вашего вывода tracemem, позвольте построить функцию f<-, которая не выполняет никакой реальной замены. Также построим a data.frame достаточно большую, чтобы мы могли измерить время, затраченное на одну копию этого data.frame.

## R v 3.0.3
`f<-` = function(x, value) {
    return(x) ## no actual replacement
}

df <- data.frame(x=1:1e8, y=1:1e8) # 762.9 Mb
tracemem(df) # [1] "<0x7fbccd2f4ae8>"

require(data.table)
system.time(copy(df)) 
# tracemem[0x7fbccd2f4ae8 -> 0x7fbccd2f4ff0]: copy system.time 
#   user  system elapsed 
#  0.609   0.484   1.106 

system.time(f(df) <- 3)
# tracemem[0x7fbccd2f4ae8 -> 0x7fbccd2f4f10]: system.time 
#   user  system elapsed 
#  0.608   0.480   1.101 

Я использовал функцию copy() из data.table (которая в основном вызывает функцию C duplicate). Время копирования более или менее идентично. Итак, первый шаг - это, безусловно, глубокая копия, даже если она ничего не сделала.

Это объясняет первые два подробных сообщения из tracemem в вашем сообщении:

(1) Из глобальной среды мы назвали f(df) <- 3). Вот один экземпляр.
(2) В рамках функции f<- выполняется другое назначение x[1,1] <- 3, которое вызывает функцию [<- (и, следовательно, [<-.data.frame). Это делает вторую копию немедленно.

Найти оставшиеся копии легко с помощью debugonce() на [<-.data.frame. То есть, делая:

debugonce(`[<-`)
df <- data.frame(x=1:1e8, y=1:1e8)
`f<-` = function(x, value) {
    x[1,1] = value
    return(x)
}
tracemem(df)
f(df) = 3

# first three lines:

# tracemem[0x7f8ba33d8a08 -> 0x7f8ba33d8d50]:      (1)
# tracemem[0x7f8ba33d8d50 -> 0x7f8ba33d8a78]: f<-  (2)
# debugging in: `[<-.data.frame`(`*tmp*`, 1L, 1L, value = 3L)

При нажатии кнопки ввода вы обнаружите, что две другие копии находятся внутри этой функции:

# debug: class(x) <- NULL
# tracemem[0x7f8ba33d8a78 -> 0x7f8ba3cd6078]: [<-.data.frame [<- f<-     (3)

# debug: x[[jj]][iseq] <- vjj
# tracemem[0x7f8ba3cd6078 -> 0x7f882c35ed40]: [<-.data.frame [<- f<-     (4)

Обратите внимание, что class является примитивным, но он вызывает объект NAM (2). Я подозреваю, что причина для копирования там. И последняя копия неизбежна, так как она изменяет столбец.

Итак, вы идете.


Теперь небольшая заметка на R v3.1.0:

Я также тестировал то же самое в R v3.1.0. tracemem предоставляет все четыре строки. Однако единственным трудоемким шагом является (4). IIUC, остальные случаи, все из-за [<-/class<- должны запускать мелкую копию вместо глубокой копии. Что удивительно в том, что даже в (4) только эта колонка, которая была изменена, кажется, сильно скопирована. R 3.1.0 имеет большие улучшения!

Это означает, что tracemem обеспечивает вывод из-за неглубокой копии тоже, что немного запутывает, поскольку в документации явно не указано это и затрудняет передачу между мелкой и глубокой копией, за исключением измерения времени. Возможно, это мое (неправильное) понимание. Не стесняйтесь меня исправлять.


С вашей стороны 2, я приведу Люка Тирни из здесь:

Вызов функции foo<- напрямую не является хорошей идеей, если вы действительно не понимаете, что происходит в механизме присваивания вообще и в конкретной функции foo<-. Это определенно не нужно делать в рутинном программировании, если вам не нравятся неприятные сюрпризы.

Но я не могу сказать, распространяются ли эти неприятные сюрпризы на объект, который уже NAM(2). Потому что Мэтт вызывал его на list, который является примитивным и, следовательно, NAM (1), а вызов foo<- напрямую не увеличивал значение "named" .

Но факт, что R v3.1.0 имеет большие улучшения, должен уже убедить вас, что такой вызов функции больше не нужен.

НТН.

PS: Не стесняйтесь исправлять меня (и помогите мне сократить этот ответ, если это возможно):).


Изменить: Я, кажется, пропустил мысль о сокращении копии при вызове f<- напрямую, как указано в комментарии. Это довольно легко увидеть, используя функцию Simon Urbanek, используемую в сообщении (которая связана несколько раз сейчас):

# rm(list=ls()) # to make sure there' no other object in your workspace
`f<-` <- function(x, value) {
    print(ls(env = parent.frame()))
}

df <- data.frame(x=1, y=2)
tracemem(df) # [1] "<0x7fce01a65358>"

f(df) = 3
# tracemem[0x7fce0359b2a0 -> 0x7fce0359ae08]: 
# [1] "*tmp*" "df"    "f<-"  

df <- data.frame(x=1, y=2)
tracemem(df) # [1] "<0x7fce03c505c0>"
df <- `f<-`(df, 3)
# [1] "df"  "f<-"

Как вы можете видеть, в первом методе создается объект *tmp*, который не является, во втором случае. И похоже, что создание объекта *tmp* для входного объекта NAM(2) запускает копию ввода до того, как *tmp* будет назначена аргументу функции. Но что касается моего понимания.