r pass-by-reference pass-by-value

¿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:

  1. ¿Dónde está esto formalmente documentado?
  2. ¿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 macros SET_NAMED y SET_NAMED , y toma los valores 0 , 1 y 2 . 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 como b . Sin embargo, si ni a ni b se modifican posteriormente, no es necesario copiar. Lo que realmente sucede es que un nuevo símbolo b está vinculado al mismo valor que a y el campo named en el objeto de valor está establecido (en este caso en 2 ). Cuando un objeto está a punto de ser alterado, se consulta el campo named . Un valor de 2 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 de 0 significa que se sabe que ningún otro SEXP comparte datos con este objeto, por lo que puede ser alterado. Un valor de 1 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.