Що таке семантика copy-on-modify у R, і де канонічне джерело?


74

Час від часу я стикаюся з думкою, що R має семантику копіювання та модифікації , наприклад, у вікі devtools Хадлі .

Більшість об'єктів R мають семантику copy-on-modify, тому зміна аргументу функції не змінює вихідне значення

Я можу простежити цей термін до списку розсилки R-Help. Наприклад, Пітер Далгаард писав у липні 2003 року :

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

Подібним чином Пітер Далгаард писав у січні 2004 року :

R має семантику копіювання-модифікації (в принципі, а іноді і на практиці), тому, коли частина об'єкта змінюється, можливо, доведеться шукати в нових місцях все, що містило його, включаючи, можливо, сам об'єкт.

Ще далі, у лютому 2000 року Росс Іхака сказав:

Ми доклали досить багато роботи, щоб це відбулося. Я б охарактеризував семантику як "копіювати при зміні (якщо потрібно)". Копіювання виконується лише тоді, коли об'єкти модифікуються. Частина (за необхідності) означає, що якщо ми можемо довести, що модифікація не може змінити будь-які нелокальні змінні, ми просто продовжуємо і модифікуємо без копіювання.

Цього немає в посібнику

Скільки б я не шукав, я не можу знайти посилання на "copy-on-modify" в посібниках R , ні у визначенні мови R, ні в R Internals

Питання

Моє запитання складається з двох частин:

  1. Де це офіційно задокументовано?
  2. Як працює копіювання на зміну?

Наприклад, чи правильно говорити про "передачу-посилання", оскільки обіцянка передається функції?


Внутрішні органи можуть бути не задокументовані в деяких випадках, щоб дати розробникам вільну можливість змінити спосіб роботи. У цьому випадку не слід писати код, який залежить від внутрішньої роботи, оскільки він може зламатися в майбутньому.
Г. Гротендік,

Відповіді:


49

Виклик за значенням

Визначення мови R говорить це (у розділі 4.3.3 Оцінка аргументу )

Семантика виклику функції у аргументі R є викликом за значенням . Загалом, подані аргументи поводяться так, ніби вони є локальними змінними, ініціалізованими із вказаним значенням та ім'ям відповідного офіційного аргументу. Зміна значення поданого аргументу в межах функції не вплине на значення змінної у викличному кадрі . [Наголос додано]

Хоча це не описує механізм, за допомогою якого працює копіювання при зміні , воно згадує, що зміна об'єкта, переданого функції, не впливає на оригінал у викличному кадрі.

Додаткова інформація, зокрема щодо аспекту копіювання-зміни , наведена в описі SEXPs у посібнику R Internals , розділ 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 цитує у своєму коментарі до питання)


1
Важливий біт (на цьому слід наголосити) полягає в тому, що R - виклик за значенням
Хедлі

@hadley, але чи не використовується ця NAMEDконцепція також із викликами функцій, з додатковою видачею обіцянок?
Гевін Сімпсон,

@hadley додав новий акцент.
Гевін Сімпсон,

NAMED - це просто оптимізація. Без цього Р би поводився однаково.
Hadley

Цілком правильно @ JoshO'Brien +1. Я занадто багато перефразовував там і змінив намір того, що писав Петро. Редагуватиме відповідно.
Гевін Сімпсон,

27

Я зробив кілька експериментів над цим, і виявив, що 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 копіює об'єкт лише при першій модифікації.


5
+1. Дуже цікаво. Думаю, ви могли б опублікувати запитання щодо того, чому R копіює об’єкти при першій настройці модифікації / атрибуту.
Ferdinand.kraft
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.