Avantages de la sémantique de copie sur écriture

10

Je me demande quels sont les avantages possibles de la copie sur écriture? Naturellement, je ne m'attends pas à des opinions personnelles, mais à des scénarios pratiques du monde réel où cela peut être techniquement et pratiquement bénéfique de manière tangible. Et par tangible, je veux dire quelque chose de plus que vous sauver la saisie d'un &caractère.

Pour clarifier, cette question se situe dans le contexte des types de données, où l'affectation ou la construction de copie crée une copie implicite superficielle, mais les modifications qui y sont apportées créent une copie profonde implicite et lui appliquent les modifications à la place de l'objet d'origine.

La raison pour laquelle je pose la question est que je ne trouve aucun avantage à avoir COW comme comportement implicite par défaut. J'utilise Qt, qui a implémenté COW pour beaucoup de types de données, pratiquement tous qui ont un stockage sous-jacent alloué dynamiquement. Mais en quoi cela profite-t-il vraiment à l'utilisateur?

Un exemple:

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"

Que gagnons-nous en utilisant COW dans cet exemple?

Si tout ce que nous avons l'intention de faire, c'est d'utiliser des opérations const, s1est redondant, autant l'utiliser s.

Si nous avons l'intention de changer la valeur, alors COW ne retarde la copie de ressource que jusqu'à la première opération non-const, au prix (quoique minime) de l'incrémentation du nombre de références pour le partage implicite et le détachement du stockage partagé. Il semble que tous les frais généraux impliqués dans COW soient inutiles.

Ce n'est pas très différent dans le contexte de la transmission de paramètres - si vous n'avez pas l'intention de modifier la valeur, passez comme référence const, si vous voulez modifier, vous faites soit une copie profonde implicite si vous ne voulez pas modifier l'objet d'origine, ou passez par référence si vous souhaitez le modifier. Encore une fois, COW semble être une surcharge inutile qui ne permet rien, et ajoute seulement une limitation selon laquelle vous ne pouvez pas modifier la valeur d'origine même si vous le souhaitez, car tout changement se détachera de l'objet d'origine.

Donc, selon que vous connaissez COW ou que vous n'en avez pas conscience, cela peut entraîner un code avec une intention obscure et des frais généraux inutiles, ou un comportement complètement déroutant qui ne correspond pas aux attentes et vous laisse vous gratter la tête.

Il me semble qu'il existe des solutions plus efficaces et plus lisibles, que vous souhaitiez éviter une copie en profondeur inutile ou que vous ayez l'intention d'en créer une. Alors, où est l'avantage pratique de la vache? Je suppose qu'il doit y avoir un certain avantage car il est utilisé dans un cadre aussi populaire et puissant.

De plus, d'après ce que j'ai lu, COW est désormais explicitement interdit dans la bibliothèque standard C ++. Je ne sais pas si les con que j'y vois ont quelque chose à voir avec ça, mais de toute façon, il doit y avoir une raison à cela.

dtech
la source

Réponses:

15

La copie lors de l'écriture est utilisée dans des situations où vous créerez très souvent une copie de l'objet et ne la modifierez pas. Dans ces situations, il est rentable.

Comme vous l'avez mentionné, vous pouvez passer un objet const, et dans de nombreux cas, cela suffit. Cependant, const garantit seulement que l'appelant ne peut pas le muter (sauf si const_cast, bien sûr). Il ne gère pas les cas de multithreading et il ne gère pas les cas où il y a des rappels (qui pourraient muter l'objet d'origine). Le passage d'un objet COW par valeur met les défis de la gestion de ces détails sur le développeur de l'API, plutôt que sur l'utilisateur de l'API.

Les nouvelles règles pour C + 11 interdisent notamment à COW std::string. Les itérateurs d'une chaîne doivent être invalidés si le tampon de sauvegarde est détaché. Si l'itérateur était implémenté en tant que char*(par opposition à a string*et à un index), ces itérateurs ne sont plus valides. La communauté C ++ devait décider de la fréquence à laquelle les itérateurs pouvaient être invalidés, et la décision était que cela operator[]ne devrait pas être l'un de ces cas. operator[]sur un std::stringretourne un char&, qui peut être modifié. Ainsi, operator[]aurait besoin de détacher la chaîne, invalider les itérateurs. Cela a été considéré comme un métier médiocre, et contrairement aux fonctions comme end()et cend(), il n'y a aucun moyen de demander la version const de operator[]short de const cast la chaîne. ( lié ).

COW est toujours vivant et bien en dehors de la STL. En particulier, je l'ai trouvé très utile dans les cas où il est déraisonnable pour un utilisateur de mes API de s'attendre à ce qu'il y ait un objet lourd derrière ce qui semble être un objet très léger. Je souhaiterai peut-être utiliser COW en arrière-plan pour m'assurer qu'ils n'auront jamais à se préoccuper de ces détails de mise en œuvre.

Cort Ammon
la source
La mutation de la même chaîne dans plusieurs threads semble être une très mauvaise conception, que vous utilisiez des itérateurs ou l' []opérateur. Donc, COW permet une mauvaise conception - cela ne ressemble pas à beaucoup d'avantages :) Le point dans le dernier paragraphe semble valide, mais je ne suis pas moi-même un grand fan du comportement implicite - les gens ont tendance à le prendre pour acquis, puis ont du mal à comprendre pourquoi le code ne fonctionne pas comme prévu, et continuez à vous demander jusqu'à ce qu'ils découvrent ce qui est caché derrière le comportement implicite.
dtech
Quant au point d'utilisation, il const_castsemble qu'il puisse casser COW aussi facilement qu'il peut casser en passant par référence const. Par exemple, QString::constData()renvoie un const QChar *- const_castcela et COW s'effondre - vous allez muter les données de l'objet d'origine.
dtech
Si vous pouvez renvoyer des données à partir d'une GC, vous devez soit vous détacher avant de le faire, soit renvoyer les données sous une forme qui est toujours à la connaissance de la GC (a char*évidemment pas au courant). Quant au comportement implicite, je pense que vous avez raison, il y a des problèmes avec cela. La conception de l'API est un équilibre constant entre les deux extrêmes. Trop implicite, et les gens commencent à se fier à un comportement spécial comme s'il faisait de facto partie de la spécification. Trop explicite et l'API devient trop compliquée car vous exposez trop de détails sous-jacents qui n'étaient pas vraiment importants et qui sont soudainement écrits dans vos spécifications d'API.
Cort Ammon
Je crois que les stringclasses ont un comportement COW parce que les concepteurs du compilateur ont remarqué qu'un grand corps de code copiait des chaînes plutôt que d'utiliser const-reference. S'ils ajoutaient COW, ils pourraient optimiser ce cas et rendre plus de gens heureux (et c'était légal, jusqu'à C ++ 11). J'apprécie leur position: alors que je passe toujours mes chaînes par référence const, j'ai vu toutes ces ordures syntaxiques qui nuisent à la lisibilité. Je déteste écrire const std::shared_ptr<const std::string>&juste pour capturer la bonne sémantique!
Cort Ammon
5

Pour les chaînes et autres, il semble que cela pessimiserait des cas d'utilisation plus courants qu'improbable, car le cas commun pour les chaînes est souvent de petites chaînes, et là, les frais généraux de COW auraient tendance à dépasser de loin le coût de la simple copie de la petite chaîne. Une petite optimisation de la mémoire tampon me semble beaucoup plus logique pour éviter l'allocation de tas dans de tels cas au lieu des copies de chaînes.

Cependant, si vous avez un objet plus lourd, comme un androïde, et que vous vouliez le copier et simplement remplacer son bras cybernétique, COW semble tout à fait raisonnable comme moyen de conserver une syntaxe mutable tout en évitant de copier en profondeur l'intégralité de l'androïde juste pour donner à la copie un bras unique. Le rendre juste immuable en tant que structure de données persistante à ce stade pourrait être supérieur, mais un "COW partiel" appliqué sur des pièces Android individuelles semble raisonnable dans ces cas.

Dans un tel cas, les deux copies de l'androïde partageraient / auraient par exemple le même torse, les jambes, les pieds, la tête, le cou, les épaules, le bassin, etc. Les seules données qui seraient différentes entre elles et non partagées sont le bras qui a été fait unique pour le deuxième androïde sur l'écrasement de son bras.


la source
Tout cela est bien, mais cela ne demande pas de vache et est toujours soumis à beaucoup d'implicité nuisible. De plus, il y a un inconvénient - vous pouvez souvent vouloir faire l'instanciation d'objet, et je ne parle pas d'instanciation de type, mais copiez un objet en tant qu'instance, donc lorsque vous modifiez l'objet source, les copies sont également mises à jour. COW exclut simplement cette possibilité, car toute modification d'un objet "partagé" le détache.
dtech
Exactitude IMO ne devrait pas être "facile" à réaliser, pas avec un comportement implicite. Un bon exemple de correction est la correction CONST, car elle est explicite et ne laisse aucune place aux ambiguïtés ou aux effets secondaires invisibles. Le fait d'avoir quelque chose comme ça «facile» et automatique n'améliore jamais ce niveau supplémentaire de compréhension de la façon dont les choses fonctionnent, ce qui est non seulement important pour la productivité globale, mais élimine à peu près la possibilité d'un comportement indésirable, dont la raison pourrait être difficile à cerner . Tout ce qui est rendu possible implicitement avec COW est également facile à réaliser de manière explicite, et c'est plus clair.
dtech
Ma question était motivée par un dilemme de fournir ou non COW par défaut dans la langue sur laquelle je travaille. Après avoir pondéré les avantages et les inconvénients, j'ai décidé de ne pas l'avoir par défaut, mais en tant que modificateur pouvant être appliqué à des types nouveaux ou déjà existants. On dirait que le meilleur des deux mondes, vous pouvez toujours avoir l'implicite de COW lorsque vous êtes explicite sur le vouloir.
dtech
@ddriver Ce que nous avons ressemble à un langage de programmation avec le paradigme nodal, à l'exception de la simplicité, le type de nœuds utilise la sémantique des valeurs et aucune sémantique de type référence (peut-être un peu semblable à std::vector<std::string>avant emplace_backet à déplacer la sémantique en C ++ 11) . Mais nous utilisons également essentiellement l'instanciation. Le système de nœuds peut ou non modifier les données. Nous avons des choses comme les nœuds pass-through qui ne font rien avec l'entrée mais juste la sortie d'une copie (ils sont là pour l'organisation des utilisateurs de son programme). Dans ces cas, toutes les données sont copiées superficiellement pour les types complexes ...
@ddriver Notre copie sur écriture est en fait un processus de copie «rendre l'instance unique implicitement lors d'un changement» . Il est impossible de modifier l'original. Si l'objet Aest copié et que rien n'est fait pour l'objecter B, il s'agit d'une copie superficielle bon marché pour les types de données complexes comme les maillages. Maintenant, si nous modifions B, les données dans lesquelles nous modifions Bdeviennent uniques via COW, mais Arestent intactes (à l'exception de certains comptages de références atomiques).