Wat is copy-on-modify semantiek in R precies en waar is de canonieke bron?

Af en toe kom ik het idee tegen dat R copy-on-modify semantiekheeft, bijvoorbeeld in Devtools-wiki van Hadley.

De meeste R-objecten hebben een copy-on-modify-semantiek, dus het wijzigen van een functie
argument verandert de oorspronkelijke waarde niet

Ik kan deze term terugvinden in de R-Help mailinglijst. Peter Dalgaard schreef bijvoorbeeld in juli 2003:

R is een functionele taal, met luie evaluatie en zwakke dynamiek
typen (een variabele kan naar believen van type veranderen: a <- 1 ; a <- “a” is
toegestaan). Semantisch gezien is alles copy-on-modify, hoewel sommige
optimalisatietrucs worden gebruikt bij de implementatie om het ergste te voorkomen
inefficiënties.

Evenzo schreef Peter Dalgaard in januari 2004:

R heeft copy-on-modify semantiek (in principe en soms in
oefenen) dus als een deel van een object verandert, moet je misschien naar binnen kijken
nieuwe plaatsen voor alles wat het bevatte, inclusief mogelijk de
object zelf.

Nog verder terug, in Feb 2000Ross Ihaka zei:

We hebben behoorlijk wat werk gestoken om dit mogelijk te maken. ik zou beschrijven
de semantiek als “kopiëren bij wijzigen (indien nodig)”. Kopiëren is klaar
alleen wanneer objecten worden gewijzigd. Het (indien nodig) gedeelte betekent dat als
we kunnen bewijzen dat de wijziging geen niet-lokale
variabelen, dan gaan we gewoon door en passen we het aan zonder te kopiëren.

Het staat niet in de handleiding

Hoe hard ik ook heb gezocht, ik kan geen verwijzing naar “copy-on-modify” vinden in de R-handleidingen, noch in R Taaldefinitienoch in R Internals

Vraag

Mijn vraag bestaat uit twee delen:

  1. Waar is dit formeel gedocumenteerd?
  2. Hoe werkt kopiëren-op-wijzigen?

Is het bijvoorbeeld juist om te spreken over “pass-by-reference”, aangezien een beloftewordt doorgegeven aan de functie?


Antwoord 1, autoriteit 100%

Call-by-value

De R-taaldefinitiezegt dit (in sectie 4.3.3 Argumentevaluatie)

De semantiek van het aanroepen van een functie in het R-argument is call-by-value. In het algemeen gedragen argumenten zich alsof het lokale variabelen zijn die zijn geïnitialiseerd met de opgegeven waarde en de naam van het bijbehorende formele argument. Het wijzigen van de waarde van een opgegeven argument binnen een functie heeft geen invloed op de waarde van de variabele in het aanroepende frame. [Nadruk toegevoegd]

Hoewel dit niet het mechanisme beschrijft waarmee copy-on-modifywerkt, vermeldt het wel dat het wijzigen van een object dat aan een functie is doorgegeven, geen invloed heeft op het origineel in het aanroepende frame.

p>

Aanvullende informatie, met name over het aspect copy-on-modify, wordt gegeven in de beschrijving van SEXPs in het R Internals-handleiding, sectie 1.1.2 Rest van header. Er staat specifiek [Nadruk toegevoegd]

Het veld namedwordt ingesteld en geopend door de SET_NAMEDen named
macro’s, en neem de waarden 0, 1en 2. R heeft een ‘call by value’
illusie, dus een opdracht als

b <- a

lijkt een kopie te maken van aen verwijst ernaar als b. Echter, als
noch anoch bworden vervolgens gewijzigd, het is niet nodig om te kopiëren.

Wat er echt gebeurt, is dat een nieuw symbool baan hetzelfde is gebonden
waarde als aen het veld namedop het waardeobject is ingesteld (in dit
hoofdletters naar 2). Wanneer een object op het punt staat te worden gewijzigd, wordt het veld named
wordt geraadpleegd. Een waarde van 2betekent dat het object moet worden gedupliceerd
alvorens te worden gewijzigd. (Merk op dat dit niet zegt dat het zo is)
nodig om te dupliceren, alleen dat het moet worden gedupliceerd of
noodzakelijk of niet.) Een waarde van 0betekent dat het bekend is dat geen andere
SEXPdeelt gegevens met dit object en kan dus veilig worden gewijzigd.
Een waarde van 1wordt gebruikt voor situaties zoals

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

waar in principe twee exemplaren van a bestaan ​​voor de duur van de
berekening als (in principe)

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

maar niet langer, en dus kunnen sommige primitieve functies worden geoptimaliseerd om
vermijd in dit geval een kopie.

Hoewel dit niet de situatie beschrijft waarin objecten worden doorgegeven aan functies als argumenten, zouden we kunnen afleiden dat hetzelfde proces werkt, vooral gezien de informatie uit de eerder geciteerde R Language-definitie.

Beloftes in functie-evaluatie

Ik denk niet dat het helemaal correct is om te zeggen dat een beloftewordt doorgegevenaan de functie. De argumenten worden doorgegeven aan de functie en de daadwerkelijk gebruikte expressies worden opgeslagen als beloften (plus een verwijzing naar de aanroepende omgeving). Alleen wanneer een argument wordt geëvalueerd, wordt de uitdrukking die is opgeslagen in de belofte opgehaald en geëvalueerd binnen de omgeving die wordt aangegeven door de aanwijzer, een proces dat bekend staat als forceren.

Als zodanig geloof ik niet dat het in dit opzicht correct is om over pass-by-referencete praten. R heeft call-by-value-semantiek, maar probeert kopiëren te vermijden, tenzij een waarde die aan een argument is doorgegeven, wordt geëvalueerd en gewijzigd.

Het NAMED-mechanisme is een optimalisatie (zoals opgemerkt door @hadley in de opmerkingen) waarmee R kan volgen of er een kopie moet worden gemaakt bij wijziging. Er zijn enkele subtiliteiten betrokken bij de exacte werking van het NAMED-mechanisme, zoals besproken door Peter Dalgaard (in de R Devel-thread@mnel citeert in hun commentaar op de vraag)


Antwoord 2, autoriteit 55%

Ik heb er wat experimenten mee gedaan en ontdekte dat R altijd het object kopieert onder de eerste wijziging.

Je kunt het resultaat op mijn computer zien in http://rpubs.com/wush978/5916

Laat het me weten als ik een fout heb gemaakt, bedankt.


Te testen of een object is gekopieerd of niet

Ik dump het geheugenadres met de volgende 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;
}

Er worden 2 adressen afgedrukt:

  • Het adres van het gegevensblok van SEXP
  • Het adres van een doorlopend blok van integer

Laten we deze C-functie compileren en laden.

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

Sessie-info

Hier is de sessionInfovan de testomgeving.

sessionInfo()

Kopiëren op schrijven

Eerst test ik de eigenschap van copy on write,
wat betekent dat R het object alleen kopieert als het wordt gewijzigd.

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

Het object bkopieert van abij de wijziging. R implementeert de eigenschap copy on write.

Wijzig vector/matrix op zijn plaats

Vervolgens test ik of R het object zal kopiëren wanneer we een element van een vector/matrix wijzigen.

Vector met lengte 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))

Het adres verandert elke keer, wat betekent dat R het geheugen niet opnieuw gebruikt.

Lange vector

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

Voor long vector, R hergebruik het geheugen na de eerste wijziging.

Bovendien laat het bovenstaande voorbeeld ook zien dat “op zijn plaats wijzigen” de prestaties beïnvloedt wanneer het object enorm is.

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

Het lijkt erop dat R het object alleen bij de eerste wijzigingen kopieert.

Ik weet niet waarom.

Kenmerk wijzigen

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

Het resultaat is hetzelfde. R kopieert het object alleen bij de eerste wijziging.

Other episodes