Méritos de la semántica de copia en escritura

10

Me pregunto qué méritos posibles tiene la copia en escritura. Naturalmente, no espero opiniones personales, sino escenarios prácticos del mundo real en los que pueda ser técnica y prácticamente beneficiosa de una manera tangible. Y por tangible me refiero a algo más que ahorrarte la escritura de un &personaje.

Para aclarar, esta pregunta es en el contexto de los tipos de datos, donde la construcción de asignación o copia crea una copia superficial implícita, pero las modificaciones a la misma crean una copia profunda implícita y le aplican los cambios en lugar del objeto original.

La razón por la que pregunto es que no parece encontrar ningún mérito de tener COW como un comportamiento implícito predeterminado. Utilizo Qt, que tiene COW implementado para muchos de los tipos de datos, prácticamente todos los cuales tienen un almacenamiento subyacente asignado dinámicamente. Pero, ¿cómo beneficia realmente al usuario?

Un ejemplo:

QString s("some text");
QString s1 = s; // now both s and s1 internally use the same resource

qDebug() << s1; // const operation, nothing changes
s1[o] = z; // s1 "detaches" from s, allocates new storage and modifies first character
           // s is still "some text"

¿Qué ganamos usando COW en este ejemplo?

Si todo lo que pretendemos hacer es usar operaciones constantes, s1es redundante, también podríamos usarlo s.

Si tenemos la intención de cambiar el valor, entonces COW solo retrasa la copia del recurso hasta la primera operación no constante, al costo (aunque mínimo) de incrementar el recuento de referencias para el uso compartido implícito y la separación del almacenamiento compartido. Parece que todos los gastos generales involucrados en COW no tienen sentido.

No es muy diferente en el contexto del paso de parámetros: si no tiene la intención de modificar el valor, pase como referencia constante, si desea modificar, haga una copia profunda implícita si no desea modificar el objeto original, o pase por referencia si desea modificarlo. Una vez más, COW parece una sobrecarga innecesaria que no logra nada, y solo agrega una limitación de que no puede modificar el valor original incluso si lo desea, ya que cualquier cambio se separará del objeto original.

Por lo tanto, dependiendo de si sabe acerca de COW o si no lo tiene en cuenta, puede generar un código con intenciones oscuras y gastos generales innecesarios, o un comportamiento completamente confuso que no coincide con las expectativas y lo deja rascándose la cabeza.

Para mí, parece que hay soluciones más eficientes y más legibles, ya sea que desee evitar una copia profunda innecesaria o si tiene la intención de hacer una. Entonces, ¿dónde está el beneficio práctico de COW? Supongo que debe haber algún beneficio ya que se usa en un marco tan popular y poderoso.

Además, por lo que he leído, COW ahora está explícitamente prohibido en la biblioteca estándar de C ++. No sé si las estafas que veo en él tienen algo que ver con eso, pero de cualquier manera, debe haber una razón para esto.

dtech
fuente

Respuestas:

15

Copiar al escribir se usa en situaciones en las que muy a menudo creará una copia del objeto y no lo modificará. En esas situaciones, se paga solo.

Como mencionó, puede pasar un objeto constante, y en muchos casos es suficiente. Sin embargo, const solo garantiza que la persona que llama no puede mutarlo (a menos que ellos const_cast, por supuesto). No maneja casos de subprocesos múltiples y no maneja casos donde hay devoluciones de llamada (que pueden mutar el objeto original). Pasar un objeto COW por valor plantea los desafíos de administrar estos detalles en el desarrollador de la API, en lugar del usuario de la API.

Las nuevas reglas para C + 11 prohíben la VACA std::stringen particular. Los iteradores en una cadena deben invalidarse si el búfer de respaldo está separado. Si el iterador se estaba implementando como un char*(A diferencia de string*ay un índice), estos iteradores ya no son válidos. La comunidad de C ++ tuvo que decidir con qué frecuencia se podían invalidar los iteradores, y la decisión fue que ese operator[]no debería ser uno de esos casos. operator[]en a std::stringdevuelve a char&, que puede modificarse. Por operator[]lo tanto, necesitaría separar la cadena, invalidando los iteradores. Se consideró que era un comercio pobre, y a diferencia de las funciones como end()y cend(), no hay forma de pedir la versión constante de operator[]short of const que arroja la cadena. ( relacionado )

VACA todavía está vivo y bien fuera del STL. En particular, lo he encontrado muy útil en los casos en que no es razonable que un usuario de mis API espere que haya algún objeto pesado detrás de lo que parece ser un objeto muy liviano. Es posible que desee utilizar COW en segundo plano para garantizar que nunca tengan que preocuparse por dichos detalles de implementación.

Cort Ammon
fuente
La mutación de la misma cadena en varios subprocesos parece un diseño muy malo, independientemente de si usa iteradores o el []operador. Entonces, COW permite un mal diseño, eso no parece un gran beneficio :) El punto en el último párrafo parece válido, pero yo mismo no soy un gran admirador del comportamiento implícito: las personas tienden a darlo por sentado, y luego tienen es difícil descubrir por qué el código no funciona como se esperaba, y seguir preguntándose hasta que descubran qué oculta detrás del comportamiento implícito.
dtech
En cuanto al punto de uso, const_castparece que puede romper la VACA tan fácilmente como puede romper el paso por referencia constante. Por ejemplo, QString::constData()devuelve un const QChar *- const_casteso y COW se colapsa - mutará los datos del objeto original.
dtech
Si puede devolver datos de un COW, debe separarlos antes de hacerlo, o devolver los datos en un formulario que todavía sea COW ( char*obviamente, no lo es). En cuanto al comportamiento implícito, creo que tienes razón, hay problemas con él. El diseño de API es un equilibrio constante entre los dos extremos. Demasiado implícito, y la gente comienza a confiar en un comportamiento especial como si fuera parte de facto de la especificación. Demasiado explícito, y la API se vuelve demasiado difícil de manejar al exponer demasiados detalles subyacentes que no eran realmente importantes, y de repente se escriben en las especificaciones de su API.
Cort Ammon
Creo que las stringclases obtuvieron un comportamiento COW porque los diseñadores del compilador notaron que un gran cuerpo de código estaba copiando cadenas en lugar de usar const-reference. Si agregaran COW, podrían optimizar este caso y hacer felices a más personas (y era legal, hasta C ++ 11). Aprecio su posición: aunque siempre paso mis cadenas por referencia constante, vi toda esa basura sintáctica que solo resta valor a la legibilidad. ¡Odio escribir const std::shared_ptr<const std::string>&solo para capturar la semántica correcta!
Cort Ammon
5

Para las cadenas y similares, parece que pesimizaría los casos de uso más comunes que no, ya que el caso común de las cadenas es a menudo cadenas pequeñas, y allí la sobrecarga de COW tenderá a superar con creces el costo de simplemente copiar la cadena pequeña. Una pequeña optimización del búfer tiene mucho más sentido para mí para evitar la asignación del montón en tales casos en lugar de las copias de cadena.

Sin embargo, si tiene un objeto más pesado, como un Android, y desea copiarlo y simplemente reemplazar su brazo cibernético, COW parece bastante razonable como una forma de mantener una sintaxis mutable y evitar la necesidad de copiar en profundidad todo el Android solo para dale a la copia un brazo único. Hacerlo simplemente inmutable como una estructura de datos persistente en ese punto podría ser superior, pero una "VACA parcial" aplicada en partes individuales de Android parece razonable para estos casos.

En tal caso, las dos copias del androide compartirían / ​​instanciarían el mismo torso, piernas, pies, cabeza, cuello, hombros, pelvis, etc. Los únicos datos que serían diferentes entre ellos y no compartidos es el brazo que se hizo. único para el segundo Android al sobrescribir su brazo.


fuente
Todo esto es bueno, pero no exige VACA, y todavía está sujeto a mucha implicidad dañina. Además, hay un inconveniente: a menudo es posible que desee hacer instancias de objetos, y no me refiero a instancias de tipo, pero copie un objeto como una instancia, por lo tanto, cuando modifica el objeto de origen, las copias también se actualizan. COW simplemente excluye esa posibilidad, ya que cualquier cambio en un objeto "compartido" lo separa.
dtech
Corrección La OMI no debe ser "fácil" de lograr, no con un comportamiento implícito. Un buen ejemplo de corrección es la corrección CONST, ya que es explícita y no deja lugar a ambigüedades o efectos secundarios invisibles. Tener algo como esto "fácil" y automático nunca acumula ese nivel adicional de comprensión de cómo funcionan las cosas, lo que no solo es importante para la productividad general, sino que elimina la posibilidad de un comportamiento no deseado, la razón por la cual puede ser difícil de identificar . Todo lo que es posible implícitamente con COW también es fácil de lograr explícitamente, y es más claro.
dtech
Mi pregunta fue motivada por el dilema de si proporcionar COW por defecto en el idioma en el que estoy trabajando. Después de ponderar los pros y los contras, decidí no tenerlo por defecto, sino como un modificador que se puede aplicar a los tipos nuevos o ya existentes. Parece lo mejor de ambos mundos, aún puedes tener la implícita de VACA cuando eres explícito acerca de quererlo.
dtech
@ddriver Lo que tenemos es algo similar a un lenguaje de programación con el paradigma nodal, excepto por la simplicidad del tipo de nodos semántica de valor de uso y sin semántica de tipo de referencia (tal vez algo similar a std::vector<std::string>antes de tener emplace_backy mover semántica en C ++ 11) . Pero también estamos básicamente usando instancias. El sistema de nodos puede o no modificar los datos. Tenemos cosas como nodos de transferencia que no hacen nada con la entrada, sino que solo generan una copia (están ahí para la organización del usuario de su programa). En esos casos, todos los datos se copian superficialmente para tipos complejos ...
@ddriver Nuestra copia en escritura es efectivamente un tipo de proceso de copia "hacer que la instancia sea única implícitamente en el cambio" . Hace que sea imposible modificar el original. Si Ase copia un objeto y no se le hace nada para que se oponga B, es una copia superficial barata para tipos de datos complejos como mallas. Ahora, si modificamos B, los datos que modificamos se Bvuelven únicos a través de COW, pero Ano se modifican (a excepción de algunos recuentos de referencia atómica).