Vorteile der Copy-on-Write-Semantik

10

Ich frage mich, welche möglichen Vorteile Copy-on-Write hat. Natürlich erwarte ich keine persönlichen Meinungen, sondern reale praktische Szenarien, in denen dies auf greifbare Weise technisch und praktisch von Vorteil sein kann. Und mit greifbar meine ich etwas mehr, als Ihnen das Schreiben eines &Zeichens zu ersparen .

Zur Verdeutlichung steht diese Frage im Zusammenhang mit Datentypen, bei denen durch Zuweisung oder Kopierkonstruktion eine implizite flache Kopie erstellt wird. Durch Änderungen wird jedoch eine implizite tiefe Kopie erstellt, und die Änderungen werden anstelle des ursprünglichen Objekts darauf angewendet.

Der Grund, den ich frage, ist, dass ich anscheinend keine Vorteile darin finde, COW als implizites Standardverhalten zu haben. Ich verwende Qt, das COW für viele Datentypen implementiert hat, praktisch alle, denen dynamisch zugeordneter Speicher zugrunde liegt. Aber wie kommt es dem Benutzer wirklich zugute?

Ein Beispiel:

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"

Was gewinnen wir mit COW in diesem Beispiel?

Wenn wir nur const-Operationen verwenden wollen, s1ist dies redundant und kann genauso gut verwendet werden s.

Wenn wir beabsichtigen, den Wert zu ändern, verzögert COW die Ressourcenkopie nur bis zur ersten nicht konstanten Operation, und zwar auf (wenn auch minimale) Kosten für die Erhöhung der Ref-Anzahl für die implizite Freigabe und das Trennen vom gemeinsam genutzten Speicher. Es sieht so aus, als ob der gesamte Aufwand für COW sinnlos ist.

Im Kontext der Parameterübergabe ist dies nicht viel anders. Wenn Sie den Wert nicht ändern möchten, übergeben Sie ihn als const-Referenz. Wenn Sie ihn ändern möchten, erstellen Sie entweder eine implizite tiefe Kopie, wenn Sie ihn nicht ändern möchten das ursprüngliche Objekt oder übergeben Sie es als Referenz, wenn Sie es ändern möchten. Wiederum scheint COW unnötiger Overhead zu sein, der nichts bewirkt, und fügt nur eine Einschränkung hinzu, dass Sie den ursprünglichen Wert nicht ändern können, selbst wenn Sie möchten, da sich jede Änderung vom ursprünglichen Objekt löst.

Je nachdem, ob Sie über COW Bescheid wissen oder sich dessen nicht bewusst sind, kann dies entweder zu Code mit unklarer Absicht und unnötigem Overhead führen oder zu einem völlig verwirrenden Verhalten, das nicht den Erwartungen entspricht und Sie am Kopf kratzen lässt.

Mir scheint, dass es effizientere und lesbarere Lösungen gibt, unabhängig davon, ob Sie eine unnötig tiefe Kopie vermeiden möchten oder eine erstellen möchten. Wo liegt also der praktische Nutzen von COW? Ich gehe davon aus, dass es einen gewissen Nutzen geben muss, da es in einem so populären und mächtigen Rahmen verwendet wird.

Darüber hinaus ist COW nach dem, was ich gelesen habe, jetzt in der C ++ - Standardbibliothek ausdrücklich verboten. Ich weiß nicht, ob die Nachteile, die ich darin sehe, etwas damit zu tun haben, aber so oder so muss es einen Grund dafür geben.

dtech
quelle

Antworten:

15

Beim Schreiben kopieren wird in Situationen verwendet, in denen Sie sehr häufig eine Kopie des Objekts erstellen und nicht ändern. In solchen Situationen zahlt es sich aus.

Wie Sie bereits erwähnt haben, können Sie ein const-Objekt übergeben, was in vielen Fällen ausreicht. Const garantiert jedoch nur, dass der Anrufer es nicht mutieren kann (es sei denn, sie const_castnatürlich). Multithreading-Fälle werden nicht behandelt, und es werden keine Fälle behandelt, in denen Rückrufe vorliegen (die das ursprüngliche Objekt mutieren könnten). Das Übergeben eines COW-Objekts als Wert stellt die Herausforderungen beim Verwalten dieser Details eher an den API-Entwickler als an den API-Benutzer.

Die neuen Regeln für C + 11 verbieten insbesondere COW std::string. Iteratoren in einer Zeichenfolge müssen ungültig gemacht werden, wenn der Sicherungspuffer getrennt wird. Wenn der Iterator als char*(im Gegensatz zu a string*und einem Index) implementiert wurde , sind diese Iteratoren nicht mehr gültig. Die C ++ - Community musste entscheiden, wie oft Iteratoren ungültig gemacht werden konnten, und die Entscheidung war, dass dies operator[]nicht einer dieser Fälle sein sollte. operator[]on a std::stringgibt a zurück char&, das geändert werden kann. Daher operator[]müsste die Zeichenfolge getrennt werden, wodurch Iteratoren ungültig werden. Dies wurde als schlechter Handel angesehen, und im Gegensatz zu Funktionen wie end()und cend()gibt es keine Möglichkeit, nach der const-Version von operator[]short of const zu fragen, die den String wirft. ( verwandt ).

COW lebt noch und befindet sich weit außerhalb der STL. Insbesondere habe ich es als sehr nützlich empfunden, wenn es für einen Benutzer meiner APIs nicht zumutbar ist, zu erwarten, dass sich hinter einem scheinbar sehr leichten Objekt ein schweres Objekt befindet. Ich möchte möglicherweise COW im Hintergrund verwenden, um sicherzustellen, dass sie sich nie mit solchen Implementierungsdetails befassen müssen.

Cort Ammon
quelle
Das Mutieren derselben Zeichenfolge in mehreren Threads scheint ein sehr schlechtes Design zu sein, unabhängig davon, ob Sie Iteratoren oder den []Operator verwenden. COW ermöglicht also schlechtes Design - das klingt nicht nach einem großen Vorteil :) Der Punkt im letzten Absatz scheint gültig zu sein, aber ich selbst bin kein großer Fan impliziten Verhaltens - die Leute halten es für selbstverständlich und haben es dann Es ist schwierig herauszufinden, warum Code nicht wie erwartet funktioniert, und sich immer wieder zu fragen, bis sie herausgefunden haben, was sich hinter dem impliziten Verhalten verbirgt.
dtech
Was den Punkt der Verwendung const_castbetrifft, so scheint es, als könne es die Kuh genauso leicht brechen wie das Passieren durch konstante Referenz. Wenn Sie beispielsweise QString::constData()a zurückgeben const QChar *- const_castdas und COW zusammenbrechen -, werden die Daten des Originalobjekts mutiert.
dtech
Wenn Sie Daten von einer COW zurückgeben können, müssen Sie diese entweder vorher trennen oder in einer Form zurückgeben, die noch COW-fähig ist (a ist char*offensichtlich nicht bekannt). Was das implizite Verhalten betrifft, denke ich, dass Sie Recht haben, es gibt Probleme damit. Das API-Design ist ein konstantes Gleichgewicht zwischen den beiden Extremen. Zu implizit, und die Leute verlassen sich auf spezielles Verhalten, als ob es de facto Teil der Spezifikation wäre. Zu explizit und die API wird zu unhandlich, wenn Sie zu viele zugrunde liegende Details offenlegen, die nicht wirklich wichtig waren und plötzlich in Ihre API-Spezifikation geschrieben werden.
Cort Ammon
Ich glaube, die stringKlassen haben COW-Verhalten, weil die Compiler-Designer bemerkt haben, dass ein großer Teil des Codes Zeichenfolgen kopiert, anstatt const-reference zu verwenden. Wenn sie COW hinzufügen, könnten sie diesen Fall optimieren und mehr Menschen glücklich machen (und es war legal, bis C ++ 11). Ich schätze ihre Position: Während ich meine Zeichenfolgen immer als konstante Referenz übergebe, habe ich all diesen syntaktischen Müll gesehen, der die Lesbarkeit nur beeinträchtigt. Ich hasse es zu schreiben, const std::shared_ptr<const std::string>&nur um die richtige Semantik zu erfassen!
Cort Ammon
5

Für Strings und dergleichen scheint es, als würde es häufigere Anwendungsfälle pessimieren als nicht, da der übliche Fall für Strings häufig kleine Strings sind und dort der Aufwand für COW die Kosten für das einfache Kopieren des kleinen Strings bei weitem überwiegt. Eine kleine Pufferoptimierung ist für mich dort viel sinnvoller, um in solchen Fällen die Heap-Zuordnung anstelle der String-Kopien zu vermeiden.

Wenn Sie jedoch ein schwereres Objekt wie ein Android-Gerät haben und es kopieren und nur seinen kybernetischen Arm ersetzen möchten, erscheint COW durchaus sinnvoll, um eine veränderbare Syntax beizubehalten und gleichzeitig zu vermeiden, dass das gesamte Android-Dokument nur tief kopiert werden muss Geben Sie der Kopie einen einzigartigen Arm. Es mag überlegen sein, es zu diesem Zeitpunkt nur als persistente Datenstruktur unveränderlich zu machen, aber eine "partielle Kuh", die auf einzelne Android-Teile angewendet wird, erscheint in diesen Fällen vernünftig.

In einem solchen Fall würden sich die beiden Kopien des Android den gleichen Oberkörper, die gleichen Beine, Füße, den gleichen Kopf, den gleichen Hals, die gleichen Schultern, das gleiche Becken usw. teilen. Die einzigen Daten, die zwischen ihnen unterschiedlich und nicht geteilt wären, sind der Arm, der hergestellt wurde Einzigartig für den zweiten Android beim Überschreiben seines Armes.


quelle
Das ist alles gut, aber es erfordert keine KUH und unterliegt immer noch einer Menge schädlicher Implikationen. Das hat auch einen Nachteil: Möglicherweise möchten Sie häufig Objekte instanziieren, und ich meine nicht die Typinstanzierung, sondern kopieren ein Objekt als Instanz. Wenn Sie also das Quellobjekt ändern, werden die Kopien ebenfalls aktualisiert. COW schließt diese Möglichkeit einfach aus, da jede Änderung an einem "gemeinsam genutzten" Objekt sie löst.
dtech
Korrektheit IMO sollte nicht "leicht" zu erreichen sein, nicht mit implizitem Verhalten. Ein gutes Beispiel für Korrektheit ist die CONST-Korrektheit, da sie explizit ist und keinen Raum für Unklarheiten oder unsichtbare Nebenwirkungen lässt. Wenn Sie so etwas "einfach" und automatisch haben, wird nie ein zusätzliches Verständnis für die Funktionsweise der Dinge aufgebaut, das nicht nur für die Gesamtproduktivität wichtig ist, sondern auch die Möglichkeit unerwünschten Verhaltens weitgehend ausschließt, dessen Grund möglicherweise schwer zu bestimmen ist . Alles, was implizit mit COW möglich ist, ist auch explizit leicht zu erreichen und klarer.
dtech
Meine Frage war durch ein Dilemma motiviert, ob COW standardmäßig in der Sprache bereitgestellt werden soll, an der ich arbeite. Nachdem ich die Vor- und Nachteile gewichtet hatte, entschied ich mich, sie nicht standardmäßig zu verwenden, sondern als Modifikator, der sowohl auf neue als auch auf bereits vorhandene Typen angewendet werden kann. Scheint das Beste aus beiden Welten zu sein, Sie können immer noch die implizite Aussage von COW haben, wenn Sie explizit wollen, dass Sie es wollen.
dtech
@ddriver Was wir haben, ist so etwas wie eine Programmiersprache mit dem Knotenparadigma, außer der Einfachheit halber verwenden die Knoten Wertesemantik und keine Referenzsemantik (vielleicht etwas ähnlich wie std::vector<std::string>zuvor emplace_backund verschieben Semantik in C ++ 11) . Grundsätzlich verwenden wir aber auch Instanzen. Das Knotensystem kann die Daten ändern oder nicht. Wir haben Dinge wie Pass-Through-Knoten, die nichts mit der Eingabe tun, sondern nur eine Kopie ausgeben (sie sind für die Benutzerorganisation seines Programms da). In diesen Fällen werden alle Daten für komplexe Typen flach kopiert ...
@ddriver Unser Copy-on-Write-Vorgang ist praktisch ein Kopiervorgang, bei dem die Instanz bei Änderungen implizit eindeutig gemacht wird. Es macht es unmöglich, das Original zu ändern. Wenn ein Objekt Akopiert wird und nichts mit dem Objekt gemacht wird B, ist es eine billige flache Kopie für komplexe Datentypen wie Netze. Wenn wir jetzt Änderungen vornehmen B, werden die Daten, die wir ändern, Bdurch COW eindeutig, Ableiben jedoch unberührt (mit Ausnahme einiger atomarer Referenzzählungen).