¿Qué es exactamente la semántica copy-on-modify en R, y dónde está la fuente canónica?
pass-by-reference pass-by-value (2)
De vez en cuando me cruzo con la idea de que R tiene una semántica de copia sobre modificación , por ejemplo, en la wiki devtools de Hadley .
La mayoría de los objetos R tienen semántica de copiar-modificar, por lo que modificar un argumento de función no cambia el valor original
Puedo rastrear este término hasta la lista de correo de R-Help. Por ejemplo, Peter Dalgaard escribió en julio de 2003 :
R es un lenguaje funcional, con evaluación diferida y tipado dinámico débil (una variable puede cambiar el tipo a voluntad: a <- 1; a <- "a" está permitido). Semánticamente, todo es copy-on-modify aunque algunos trucos de optimización se utilizan en la implementación para evitar las peores ineficiencias.
Del mismo modo, Peter Dalgaard escribió en enero de 2004 :
R tiene una semántica de copia sobre modificación (en principio y algunas veces en la práctica) por lo que una vez que una parte de un objeto cambia, es posible que tenga que buscar nuevos lugares para cualquier cosa que lo contenga, incluido posiblemente el objeto mismo.
Aún más atrás, en febrero de 2000 Ross Ihaka dijo:
Ponemos bastante trabajo para que esto suceda. Yo describiría la semántica como "copiar en modificar (si es necesario)". La copia se hace solo cuando los objetos son modificados. La parte (si es necesario) significa que si podemos probar que la modificación no puede cambiar ninguna variable no local, entonces simplemente avanzamos y modificamos sin copiar.
No está en el manual
No importa cuánto haya buscado, no puedo encontrar una referencia a "copiar-modificar" en los manuales de R , ni en R Language Definition ni en R Internals
Pregunta
Mi pregunta tiene dos partes:
- ¿Dónde está esto formalmente documentado?
- ¿Cómo funciona copy-on-modify?
Por ejemplo, ¿es apropiado hablar de "paso por referencia", ya que se pasa una promesa a la función?
Llamada por valor
La definición de lenguaje R dice esto (en la sección 4.3.3 Evaluación de argumentos )
La semántica de invocar una función en el argumento R es llamada por valor . En general, los argumentos suministrados se comportan como si fueran variables locales inicializadas con el valor proporcionado y el nombre del argumento formal correspondiente. Cambiar el valor de un argumento proporcionado dentro de una función no afectará el valor de la variable en el marco de llamada . [Énfasis añadido]
Si bien esto no describe el mecanismo por el cual funciona copy-on-modify , menciona que cambiar un objeto pasado a una función no afecta al original en el marco de llamada.
En la descripción de los SEXP
, en el manual de R Internals , sección 1.1.2 Resto del encabezado , se proporciona información adicional, particularmente sobre el aspecto de copiar en la modificación . Específicamente declara [Énfasis añadido]
El campo
named
se establece y se accede mediante las macrosSET_NAMED
ySET_NAMED
, y toma los valores0
,1
y2
. R tiene una ilusión de "llamar por valor" , por lo que una asignación como
b <- a
parece hacer una copia de
a
y referirse a ella comob
. Sin embargo, si nia
nib
se modifican posteriormente, no es necesario copiar. Lo que realmente sucede es que un nuevo símbolob
está vinculado al mismo valor quea
y el camponamed
en el objeto de valor está establecido (en este caso en2
). Cuando un objeto está a punto de ser alterado, se consulta el camponamed
. Un valor de2
significa que el objeto debe duplicarse antes de cambiarse. (Tenga en cuenta que esto no dice que es necesario duplicar, solo que debe duplicarse si es necesario o no). Un valor de0
significa que se sabe que ningún otroSEXP
comparte datos con este objeto, por lo que puede ser alterado. Un valor de1
se usa para situaciones como
dim(a) <- c(7, 2)
donde en principio existen dos copias de una durante la duración del cálculo como (en principio)
a <- `dim<-`(a, c(7, 2))
pero por más tiempo, por lo que algunas funciones primitivas pueden optimizarse para evitar una copia en este caso.
Si bien esto no describe la situación en la que los objetos se pasan a funciones como argumentos, podemos deducir que opera el mismo proceso, especialmente dada la información de la definición del lenguaje R citada anteriormente.
Promesas en la evaluación de funciones
No creo que sea del todo correcto decir que se pasa una promesa a la función. Los argumentos se pasan a la función y las expresiones reales utilizadas se almacenan como promesas (más un puntero al entorno de llamada). Solo cuando se evalúa un argumento, la expresión almacenada en la promesa se recupera y evalúa dentro del entorno indicado por el puntero, un proceso conocido como forzar .
Como tal, no creo que sea correcto hablar de referencia de paso en este sentido. R tiene una semántica llamada por valor, pero trata de evitar la copia a menos que un valor pasado a un argumento sea evaluado y modificado.
El mecanismo NAMED es una optimización (como lo señala @hadley en los comentarios) que permite a R rastrear si se debe hacer una copia después de la modificación. Hay algunas sutilezas involucradas en cómo funciona exactamente el mecanismo NAMED, como discutió Peter Dalgaard (en el hilo de R Devel @mnel cita en su comentario a la pregunta)
Hice algunos experimentos al respecto y descubrí que R siempre copia el objeto bajo la primera modificación.
Puedes ver el resultado en mi máquina en http://rpubs.com/wush978/5916
Por favor, avíseme si cometí un error, gracias.
Para probar si un objeto está copiado o no
Volcado la dirección de memoria con el siguiente código 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;
}
Imprimirá 2 direcciones:
- La dirección del bloque de datos de
SEXP
- La dirección de bloque continuo de
integer
Vamos a compilar y cargar esta función C.
Rcpp:::SHLIB("dump_address.c")
dyn.load("dump_address.so")
Información de la sesión
Aquí está la sessionInfo
del entorno de prueba.
sessionInfo()
Copiar en escrito
Primero pruebo la propiedad de copiar en escritura , lo que significa que R solo copia el objeto solo cuando se modifica.
a <- 1L
b <- a
invisible(.Call("dump_address", a))
invisible(.Call("dump_address", b))
b <- b + 1
invisible(.Call("dump_address", b))
El objeto b
copia de a
en la modificación. R implementa la copy on write
propiedad de copy on write
.
Modificar vector / matriz en su lugar
Luego pruebo si R copiará el objeto cuando modifiquemos un elemento de un vector / matriz.
Vector con longitud 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))
La dirección cambia cada vez, lo que significa que R no reutiliza la memoria.
Vector largo
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))
Para vector largo, R reutiliza la memoria después de la primera modificación.
Además, el ejemplo anterior también muestra que "modificar en el lugar" afecta el rendimiento cuando el objeto es enorme.
Matriz
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))
Parece que R copia el objeto solo en las primeras modificaciones.
No sé por qué.
Cambiar atributo
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))
El resultado es el mismo. R solo copia el objeto en la primera modificación.