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

#r #function

#r #передача по ссылке #передача по значению

Вопрос:

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

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

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

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

Аналогично, Питер Далгаард написал в январе 2004 года:

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

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

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

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

Как бы я ни искал, я не могу найти ссылку на «копирование при изменении» в руководствах по R, ни в определении языка R, ни во внутренних компонентах R

Вопрос

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

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

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

Комментарии:

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

Ответ №1:

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

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

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

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

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

named Поле устанавливается и к нему обращаются SET_NAMED NAMED макросы and , и принимает значения 0 , 1 и 2 . R имеет иллюзию «вызова по значению», поэтому присваивание типа

 b <- a
 

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

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

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

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

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

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

Обещания при оценке функции

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

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

Именованный механизм — это оптимизация (как отметил @hadley в комментариях), которая позволяет R отслеживать, нужно ли делать копию при модификации. Есть некоторые тонкости, связанные с тем, как именно работает ИМЕНОВАННЫЙ механизм, о чем говорил Питер Далгаард (в ветке разработки R, которую @mnel цитирует в своем комментарии к вопросу)

Комментарии:

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

2. @hadley но разве эта NAMED концепция не используется также при вызовах функций с дополнительным выпуском обещаний?

3. @hadley добавил новый акцент.

4. NAMED — это просто оптимизация. R вел бы себя одинаково без него.

5. Совершенно верно @Josho’Brien 1. Я слишком много перефразировал там и изменил намерение того, что написал Питер. Отредактирует соответствующим образом.

Ответ №2:

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

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

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


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

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

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

SEXP dump_address(SEXP src) {
  Rprintf("p p %dn", amp;(src->u), INTEGER(src), INTEGER(src) - (int*)amp;(src->u));
  return R_NilValue;
}
 

Он выведет 2 адреса:

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

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

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

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

Вот sessionInfo среда тестирования.

 sessionInfo()
 

Копировать при записи

Сначала я проверяю свойство копировать при записи, что означает, что 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 копирует объект только при первом изменении.

Комментарии:

1. 1. Очень интересно. Я думаю, вы могли бы задать вопрос о том, почему R копирует объекты при первой настройке модификации / атрибута.