Was genau ist Copy-on-Modify-Semantik in R und wo ist die kanonische Quelle?

74

Hin und wieder stoße ich auf die Vorstellung, dass R eine Semantik zum Kopieren und Ändern hat , zum Beispiel in Hadleys Devtools-Wiki .

Die meisten R-Objekte verfügen über eine Semantik zum Kopieren und Ändern, sodass durch Ändern eines Funktionsarguments der ursprüngliche Wert nicht geändert wird

Ich kann diesen Begriff auf die R-Help-Mailingliste zurückführen. Zum Beispiel schrieb Peter Dalgaard im Juli 2003 :

R ist eine funktionale Sprache mit verzögerter Auswertung und schwacher dynamischer Typisierung (eine Variable kann den Typ nach Belieben ändern: a <- 1; a <- "a" ist zulässig). Semantisch ist alles Copy-on-Modify, obwohl bei der Implementierung einige Optimierungstricks verwendet werden, um die schlimmsten Ineffizienzen zu vermeiden.

Ebenso schrieb Peter Dalgaard im Januar 2004 :

R verfügt über eine Semantik zum Kopieren und Ändern (im Prinzip und manchmal in der Praxis). Sobald sich ein Teil eines Objekts ändert, müssen Sie möglicherweise an neuen Stellen nach allem suchen, was es enthält, einschließlich möglicherweise des Objekts selbst.

Noch weiter zurück, im Februar 2000, sagte Ross Ihaka:

Wir haben viel Arbeit investiert, um dies zu erreichen. Ich würde die Semantik als "Kopie beim Ändern (falls erforderlich)" beschreiben. Das Kopieren erfolgt nur, wenn Objekte geändert werden. Der (falls erforderlich) Teil bedeutet, dass wir, wenn wir nachweisen können, dass die Änderung keine nicht lokalen Variablen ändern kann, einfach ohne Kopieren Änderungen vornehmen.

Es ist nicht im Handbuch

Egal wie intensiv ich gesucht habe, ich kann in den R-Handbüchern weder in der R-Sprachdefinition noch in den R -Interna einen Verweis auf "Copy-on-Modify" finden

Frage

Meine Frage besteht aus zwei Teilen:

  1. Wo ist das formal dokumentiert?
  2. Wie funktioniert Copy-on-Modify?

Ist es zum Beispiel richtig, von "Pass-by-Reference" zu sprechen, da ein Versprechen an die Funktion weitergegeben wird?

Andrie
quelle
In einigen Fällen werden die Interna möglicherweise nicht dokumentiert, um den Entwicklern die Möglichkeit zu geben, ihre Funktionsweise zu ändern. In diesem Fall sollte man keinen Code schreiben, der von der internen Operation abhängt, da er in Zukunft brechen könnte.
G. Grothendieck

Antworten:

49

Call-by-Value

Die R-Sprachdefinition sagt dies aus (in Abschnitt 4.3.3 Argumentbewertung )

Die Semantik des Aufrufs einer Funktion im R-Argument ist Call-by-Value . Im Allgemeinen verhalten sich die angegebenen Argumente so, als wären sie lokale Variablen, die mit dem angegebenen Wert und dem Namen des entsprechenden formalen Arguments initialisiert wurden. Das Ändern des Werts eines angegebenen Arguments innerhalb einer Funktion wirkt sich nicht auf den Wert der Variablen im aufrufenden Frame aus . [Betonung hinzugefügt]

Dies beschreibt zwar nicht den Mechanismus, nach dem Copy-on-Modify funktioniert, erwähnt jedoch, dass das Ändern eines an eine Funktion übergebenen Objekts keine Auswirkungen auf das Original im aufrufenden Frame hat.

Weitere Informationen, insbesondere zum Aspekt des Kopierens und Änderns, finden Sie in der Beschreibung von SEXPs im Handbuch R Internals , Abschnitt 1.1.2 Rest des Headers . Insbesondere heißt es [Hervorhebung hinzugefügt]

Das namedFeld wird eingestellt und durch die zugegriffen SET_NAMEDund NAMED Makros und Werte annehmen 0, 1und 2. R hat eine "Call by Value" -Illusion, also eine Zuordnung wie

b <- a

scheint eine Kopie davon zu machen aund darauf zu verweisen alsb . Doch wenn weder anoch bnachträglich verändert werden , gibt es keine Notwendigkeit , zu kopieren. Was wirklich passiert, ist, dass ein neues Symbol ban denselben Wert gebunden ist wie aund das namedFeld auf dem Wertobjekt gesetzt ist (in diesem Fall auf 2). Wenn ein Objekt geändert werden soll, wird das namedFeld konsultiert. Ein Wert von 2bedeutet, dass das Objekt vor dem Ändern dupliziert werden muss. (Beachten Sie, dass dies nicht bedeutet, dass ein Duplizieren erforderlich ist, sondern nur, dass es dupliziert werden sollte, ob dies erforderlich ist oder nicht.) Ein Wert von 0bedeutet, dass bekannt ist, dass kein anderer vorhanden ist SEXPteilt Daten mit diesem Objekt und kann daher sicher geändert werden. Ein Wert von 1wird für Situationen wie verwendet

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

wo im Prinzip zwei Kopien von a für die Dauer der Berechnung existieren als (im Prinzip)

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

aber nicht mehr, und so können einige primitive Funktionen optimiert werden, um in diesem Fall eine Kopie zu vermeiden.

Dies beschreibt zwar nicht die Situation, in der Objekte als Argumente an Funktionen übergeben werden, wir können jedoch den Schluss ziehen, dass derselbe Prozess abläuft, insbesondere angesichts der Informationen aus der zuvor zitierten Definition der R-Sprache.

Versprechen bei der Funktionsbewertung

Ich glaube nicht , es ganz richtig ist zu sagen , dass ein Versprechen ist vergangen an die Funktion. Die Argumente werden an die Funktion übergeben und die tatsächlich verwendeten Ausdrücke werden als Versprechen gespeichert (plus einem Zeiger auf die aufrufende Umgebung). Nur wenn ein Argument ausgewertet wird, wird der in dem Versprechen gespeicherte Ausdruck in der durch den Zeiger angegebenen Umgebung abgerufen und ausgewertet. Dieser Vorgang wird als Forcen bezeichnet .

Insofern halte ich es nicht für richtig, diesbezüglich über Pass-by-Reference zu sprechen . R hat eine Call-by-Value- Semantik, versucht jedoch, das Kopieren zu vermeiden, es sei denn, ein an ein Argument übergebener Wert wird ausgewertet und geändert.

Der NAMED-Mechanismus ist eine Optimierung (wie von @hadley in den Kommentaren angegeben), mit der R verfolgen kann, ob bei Änderungen eine Kopie erstellt werden muss. Es gibt einige Feinheiten, die genau mit der Funktionsweise des NAMED-Mechanismus zusammenhängen, wie von Peter Dalgaard erörtert (im R Devel-Thread zitiert @mnel in seinem Kommentar zur Frage)

Gavin Simpson
quelle
1
Das wichtige Bit (das hervorgehoben werden sollte) ist, dass R Call-by-Value ist
Hadley
@ Hadley, aber wird das NAMEDKonzept nicht auch bei Funktionsaufrufen verwendet, mit der zusätzlichen Ausgabe von Versprechungen?
Gavin Simpson
@ Hadley fügte neue Betonung hinzu.
Gavin Simpson
NAMED ist nur eine Optimierung. R würde sich ohne es identisch verhalten.
Hadley
Ganz richtig @ JoshO'Brien +1. Ich habe dort viel zu viel para-formuliert und die Absicht von Peter geändert. Wird entsprechend bearbeitet.
Gavin Simpson
27

Ich habe einige Experimente damit durchgeführt und festgestellt, dass R das Objekt bei der ersten Änderung immer kopiert.

Sie können das Ergebnis auf meinem Computer unter http://rpubs.com/wush978/5916 sehen

Bitte lassen Sie mich wissen, wenn ich einen Fehler gemacht habe, danke.


Um zu testen, ob ein Objekt kopiert wurde oder nicht

Ich speichere die Speicheradresse mit dem folgenden C-Code:

#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;
}

Es werden 2 Adressen gedruckt:

  • Die Adresse des Datenblocks von SEXP
  • Die Adresse des fortlaufenden Blocks von integer

Lassen Sie uns diese C-Funktion kompilieren und laden.

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

Sitzungsinfo

Hier ist die sessionInfoTestumgebung.

sessionInfo()

Beim Schreiben kopieren

Zuerst teste ich die Eigenschaft von copy beim Schreiben , was bedeutet, dass R das Objekt nur kopiert, wenn es geändert wird.

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

Das Objekt bkopiert abei der Änderung von. R implementiert die copy on writeEigenschaft.

Ändern Sie den Vektor / die Matrix an Ort und Stelle

Dann teste ich, ob R das Objekt kopiert, wenn wir ein Element eines Vektors / einer Matrix ändern.

Vektor mit Länge 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))

Die Adresse ändert sich jedes Mal, was bedeutet, dass R den Speicher nicht wiederverwendet.

Langer Vektor

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))

Für lange Vektoren verwendet R den Speicher nach der ersten Änderung wieder.

Darüber hinaus zeigt das obige Beispiel auch, dass "an Ort und Stelle ändern" die Leistung beeinträchtigt, wenn das Objekt sehr groß ist.

Matrix

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))

Es scheint, dass R das Objekt nur bei den ersten Änderungen kopiert.

Ich weiß nicht warum.

Attribut ändern

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))

Das Ergebnis ist das gleiche. R kopiert das Objekt nur bei der ersten Änderung.

wush978
quelle
5
+1. Sehr interessant. Ich denke, Sie könnten eine Frage stellen, warum R Objekte bei der ersten Änderung / Attributeinstellung kopiert.
Ferdinand.kraft