Что такое семантика копирования-на-изменении в R, и где находится канонический источник?

Время от времени я сталкиваюсь с мнением, что R имеет семантику копирования-на-изменении, например, в вики devtools Хэдли.

Большинство объектов R имеют семантику копирования при изменении, поэтому изменение аргумента функции не меняет исходное значение

Я могу проследить этот термин до списка рассылки R-Help. Например, Питер Далгаард написал в июле 2003 года:

R - функциональный язык с ленивой оценкой и слабой динамической типизацией (переменная может менять тип по желанию: a<- 1; a <- "a" разрешено). Семантически все копируется при модификации, хотя в реализации используются некоторые приемы оптимизации, чтобы избежать наихудшей неэффективности.

Точно так же Питер Далгаард написал в январе 2004 года:

R имеет семантику копирования при модификации (в принципе, а иногда и на практике), поэтому, как только часть объекта изменяется, вам, возможно, придется искать в новых местах все, что его содержит, включая, возможно, сам объект.

Еще дальше, в феврале 2000 года Росс Ихака сказал:

Мы приложили немало усилий, чтобы это произошло. Я бы описал семантику как "копировать при модификации (при необходимости)". Копирование выполняется только при изменении объектов. Часть (если необходимо) означает, что если мы можем доказать, что модификация не может изменить какие-либо нелокальные переменные, то мы просто продолжаем и модифицируем без копирования.

Это не в руководстве

Независимо от того, как сильно я искал, я не могу найти ссылку на "copy-on-modify" в руководствах R, ни в R Language Definition, ни в R Internals

Вопрос

Мой вопрос состоит из двух частей:

  1. Где это официально задокументировано?
  2. Как работает копирование на изменение?

Например, уместно ли говорить о "передаче по ссылке", поскольку обещание передается функции?

2 ответа

Решение

Вызов по значению

Определение языка R говорит об этом (в разделе 4.3.3 Оценка аргумента)

Семантика вызова функции в аргументе R определяется по значению. Как правило, предоставленные аргументы ведут себя так, как будто они являются локальными переменными, инициализированными предоставленным значением и именем соответствующего формального аргумента. Изменение значения предоставленного аргумента в функции не повлияет на значение переменной в вызывающем фрейме. [Акцент добавлен]

Хотя это не описывает механизм, с помощью которого работает метод копирования-на-изменении, в нем упоминается, что изменение объекта, переданного функции, не влияет на оригинал в вызывающем фрейме.

Дополнительная информация, в частности, об аспекте копирования при изменении, приведена в описании SEXP s в руководстве по внутренним компонентам R, раздел 1.1.2 Остальная часть заголовка. Конкретно в нем говорится [Акцент добавлен]

named поле установлено и доступно SET_NAMED а также NAMED макросы и принимать значения 0, 1 а также 2, R имеет иллюзию "вызова по значению", поэтому назначение как

b <- a

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

dim(a) <- c(7, 2)

где в принципе две копии существуют на время вычисления как (в принципе)

a <- `dim<-`(a, c(7, 2))

но уже не так, и поэтому некоторые примитивные функции могут быть оптимизированы, чтобы избежать копирования в этом случае.

Хотя это и не описывает ситуацию, когда объекты передаются функциям в качестве аргументов, мы можем сделать вывод, что работает тот же процесс, особенно учитывая информацию из определения языка R, которое цитировалось ранее.

Обещания в оценке функций

Я не думаю, что будет правильно сказать, что обещание передано функции. Аргументы передаются в функцию, а используемые выражения сохраняются как обещания (плюс указатель на вызывающую среду). Только когда аргумент оценен, выражение, сохраненное в обещании, извлекается и оценивается в среде, указанной указателем, процесс, известный как форсирование.

В связи с этим я не считаю правильным говорить об этом по ссылке. R имеет семантику вызова по значению, но пытается избежать копирования, если только значение, переданное аргументу, не будет оценено и изменено.

Механизм NAMED - это оптимизация (как отмечено @hadley в комментариях), которая позволяет R отслеживать, нужно ли делать копию после модификации. Есть некоторые тонкости, связанные с тем, как именно работает механизм NAMED, как обсуждалось Питером Далгаардом (в ветке R Devel @mnel цитирует в своем комментарии к вопросу)

Я провел несколько экспериментов и обнаружил, что R всегда копирует объект при первой модификации.

Вы можете увидеть результат на моей машине в http://rpubs.com/wush978/5916

Пожалуйста, дайте мне знать, если я сделал какую-либо ошибку, спасибо.


Чтобы проверить, скопирован ли объект или нет

Я сбрасываю адрес памяти с помощью следующего кода C:

#define USE_RINTERNALS
#include <R.h>
#include <Rdefines.h>

SEXP dump_address(SEXP src) {
  Rprintf("%16p %16p %d\n", &(src->u), INTEGER(src), INTEGER(src) - (int*)&(src->u));
  return R_NilValue;
}

Будет напечатано 2 адреса:

  • Адрес блока данных SEXP
  • Адрес непрерывного блока integer

Давайте скомпилируем и загрузим эту C-функцию.

Rcpp:::SHLIB("dump_address.c")
dyn.load("dump_address.so")

Информация о сессии

Здесь sessionInfo среды тестирования.

sessionInfo()

Копирование при записи

Сначала я проверяю свойство copy при записи, что означает, что R копирует объект только тогда, когда он модифицирован.

a <- 1L
b <- a
invisible(.Call("dump_address", a))
invisible(.Call("dump_address", b))
b <- b + 1
invisible(.Call("dump_address", b))

Предмет b копии с a в модификации. R реализует copy on write имущество.

Изменить вектор / матрицу на месте

Затем я проверяю, скопирует ли R объект, когда мы модифицируем элемент вектора / матрицы.

Вектор длиной 1

a <- 1L
invisible(.Call("dump_address", a))
a <- 1L
invisible(.Call("dump_address", a))
a[1] <- 1L
invisible(.Call("dump_address", a))
a <- 2L 
invisible(.Call("dump_address", a))

Адрес меняется каждый раз, что означает, что R не использует память повторно.

Длинный вектор

system.time(a <- rep(1L, 10^7))
invisible(.Call("dump_address", a))
system.time(a[1] <- 1L)
invisible(.Call("dump_address", a))
system.time(a[1] <- 1L)
invisible(.Call("dump_address", a))
system.time(a[1] <- 2L)
invisible(.Call("dump_address", a))

Для длинных векторов R повторно используют память после первой модификации.

Кроме того, приведенный выше пример также показывает, что "изменение на месте" влияет на производительность, когда объект огромен.

матрица

system.time(a <- matrix(0L, 3162, 3162))
invisible(.Call("dump_address", a))
system.time(a[1,1] <- 0L)
invisible(.Call("dump_address", a))
system.time(a[1,1] <- 1L)
invisible(.Call("dump_address", a))
system.time(a[1] <- 2L)
invisible(.Call("dump_address", a))
system.time(a[1] <- 2L)
invisible(.Call("dump_address", a))

Кажется, что R копирует объект только при первых модификациях.

Я не знаю почему.

Изменение атрибута

system.time(a <- vector("integer", 10^2))
invisible(.Call("dump_address", a))
system.time(names(a) <- paste(1:(10^2)))
invisible(.Call("dump_address", a))
system.time(names(a) <- paste(1:(10^2)))
invisible(.Call("dump_address", a))
system.time(names(a) <- paste(1:(10^2) + 1))
invisible(.Call("dump_address", a))

Результат тот же. R только копирует объект при первой модификации.

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