¿Qué es el Copy On Write?

Tutorial Swift – ¿Qué es el Copy On Write?

En este Tutorial Swift aprenderemos acerca de una característica bien interesante del lenguaje llamada Copy On Write (copia al escribir) la cual complementa a los tipos por valor.

Antes de proseguir me gustaría comentar que este artículo es una continuación del anterior donde hablamos sobre los tipos por referencia y los tipos por valor en el lenguaje de programación Swift. Es altamente recomendable su lectura para lograr entender plenamente sobre otros comportamientos a los que haremos referencia.

La característica o comportamiento Copy On Write es muy ingeniosa y viene a reducir grandemente el costo en recursos de los tipos de datos por valor. Esta consiste en que al efectuar una asignación cada tipo por valor apunta a la misma dirección de memoria, cuando el valor de uno de estos sea modificado es realmente el momento donde Swift copia el valor a una nueva dirección de memoria y por ende se ejecuta el cambio.

Veamos a bajo nivel como funciona todo esto, analicemos el siguiente ejemplo:

En este ejemplo hemos creado 2 estructuras, la primera llamada Address y la segunda llamada AddressBits, esta segunda estructura es creada solamente como apoyo, ya que como estamos trabajando con tipos por valor no podemos hacer uso de la función unsafeAddress. Por este motivo en las líneas 29, 30 y 31 hacemos uso de la función unsafeBitCast la cual nos permite devolver los bits de datos que componen el primer parámetro (Address) interpretados como de tipo del segundo parámetro (AddressBits). Sería algo así como una fusión entre estas dos estructuras del mismo tamaño (este es el objetivo de las dos constantes Int y las tres String más el puntero de tipo UnsafeMutablePointer) para al final hacer uso del puntero y lograr conocer así su dirección de memoria.

En la línea 25 creamos una instancia de la estructura Address, luego en las líneas 26 y 27 hemos creado dos variables de igual tipo a las cuales hemos asignado el valor de MyAddress1. Analizando esto último y lo ya dicho, en la línea 26 cuando igualamos MyAddress2 a MyAddress1, el compilador debería de asignar una nueva dirección de memoria a MyAddress2 e inicializar este segmento con los datos almacenados en MyAddress1, lo mismo debería de suceder en la línea 27, MyAddress3 debería de tener un nuevo segmento en memoria con una copia independiente de los datos de MyAddress2 que a su vez son una copia de MyAddress1.

Veamos la salida en pantalla para verificar si esto es así:

…mmm pues no! como podemos constatar luego de las asignaciones las direcciones de memoria no han cambiado, las variables MyAddress2 y MyAddress3 se encuentran apuntando a la misma dirección de memoria de MyAddress1.

¿Qué quiere decir esto?

Pues que no ha habido prácticamente gastos de recursos, nos más que si hubiéramos estado trabajando con tipos por referencia y si analizamos bien pues ¿qué sentido tendría si no hemos modificado los datos, que cada una de estas variables cuenten con un segmento de memoria propio y una copia del mismo valor, cuando el valor valga la redundancia es aún el mismo, no ha cambiado? En este punto comprendemos por qué hemos dicho anteriormente que el trabajo con tipos por valor no tiene que ser necesariamente más costoso que los tipos por referencia, así de óptimo puede quedar nuestro código gracias a la característica copy-on-write.

¿Qué sucedería si modificamos una de las propiedades de alguna de estas variables?

Pues veamos una variante del ejemplo anterior:

…la salida en pantalla sería:

…en la línea 39 hemos hecho la modificación y en la línea 8 de la salida en pantalla podemos ver como la dirección de MyAddress2 ha cambiado a una nueva debido a la modificación que esta ha sufrido, es decir, que en este punto es cuando único ha habido una operación de asignación e inicialización de memoria, algo realmente increíble y digno de un lenguaje de este siglo.

¿Quieren ver algo aún más sorprendente?

Seguramente que sí, agreguemos otro cambio, simple como el anterior, modifiquemos ahora MyAddress3 pero exactamente con el mismo cambio que hemos introducido en MyAddress2, el código quedaría así:

…la salida en pantalla sería:

Como podemos observar en la salida en pantalla, al modificar MyAddress3 en lugar de serle asignada otra dirección de memoria el compilador de Swift ha determinado que es más óptimo redireccionarlo a la dirección de memoria de MyAddress2 que ya ha sido asignada e inicializada y cuyo valor es el mismo.

Los comportamientos que hemos descrito es lo que hace tan poderoso a los tipos por valor en Swift, pero las mejoras en el desempeño no acaban aquí, ya que cuando tenemos dos tipos por valor comparten la misma dirección de memoria y el compilador se encuentra una sentencia donde comparamos la igualdad de sus valores con el operador == Swift ni se molesta en llevarla a cabo ya que lógicamente se infiere que los valores son los mismos, pequeños detalles como estos son los que hacen a Swift tan eficiente. No hay más que decir, sencillamente genial!!!

Espero que todo cuanto se ha dicho aquí, de una forma u otra le haya servido de aprendizaje, de referencia, que haya valido su preciado tiempo.

Este artículo, al igual que el resto, será revisado con cierta frecuencia en pos de mantener un contenido de calidad y actualizado.

Nota: Este artículo fue inspirado por las siguientes publicaciones:

Reference vs Value Types in Swift: Part 1/2
Reference vs Value Types in Swift: Part 2/2

…ambas escritas por Eric Cerney en idioma ingles y para el sitio web RayWenderlich.