Meriti della semantica copy-on-write


10

Mi chiedo quali possibili meriti ha il copy-on-write? Naturalmente, non mi aspetto opinioni personali, ma scenari pratici del mondo reale in cui può essere tecnicamente e praticamente vantaggioso in modo tangibile. E per tangibile intendo qualcosa di più che salvarti la digitazione di un &personaggio.

Per chiarire, questa domanda è nel contesto dei tipi di dati, in cui l'assegnazione o la costruzione della copia crea una copia superficiale implicita, ma le modifiche ad essa creano una copia profonda implicita e applicano le modifiche ad essa anziché l'oggetto originale.

Il motivo per cui lo sto chiedendo è che non trovo alcun merito di avere COW come comportamento implicito predefinito. Uso Qt, che ha implementato COW per molti tipi di dati, praticamente tutti quelli che hanno una memoria sottostante allocata dinamicamente. Ma come va a beneficio dell'utente?

Un esempio:

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"

Cosa vinciamo usando COW in questo esempio?

Se tutto ciò che intendiamo fare è utilizzare le operazioni const, s1è ridondante, potrebbe anche essere utile s.

Se intendiamo modificare il valore, COW ritarda solo la copia della risorsa fino alla prima operazione non const, al costo (sebbene minimo) di aumentare il conteggio di riferimento per la condivisione implicita e il distacco dalla memoria condivisa. Sembra che tutte le spese generali coinvolte in COW siano inutili.

Non è molto diverso nel contesto del passaggio dei parametri: se non si intende modificare il valore, passare come riferimento const, se si desidera modificare, si può fare una copia profonda implicita se non si desidera modificare l'oggetto originale o passare per riferimento se si desidera modificarlo. Ancora una volta COW sembra inutile sovraccarico che non raggiunge nulla e aggiunge solo una limitazione che non è possibile modificare il valore originale anche se si desidera, poiché qualsiasi modifica si staccherà dall'oggetto originale.

Quindi, a seconda che tu sia a conoscenza di COW o che tu ne sia ignaro, potrebbe risultare in un codice con intenti oscuri e inutili spese generali, o un comportamento completamente confuso che non corrisponde alle aspettative e ti lascia grattarti la testa.

A me sembra che ci siano soluzioni più efficienti e più leggibili se si desidera evitare una copia profonda non necessaria o se si intende realizzarne una. Allora, qual è il vantaggio pratico di COW? Presumo che ci debba essere qualche vantaggio poiché utilizzato in un framework così popolare e potente.

Inoltre, da quello che ho letto, COW è ora esplicitamente vietato nella libreria standard C ++. Non so se le truffe che vedo in essa hanno qualcosa a che fare con esso, ma in entrambi i casi, ci deve essere una ragione per questo.

Risposte:


15

Copia su scrittura viene utilizzata in situazioni in cui molto spesso verrà creata una copia dell'oggetto e non modificata. In quelle situazioni, si ripaga da solo.

Come hai detto, puoi passare un oggetto const, e in molti casi è sufficiente. Tuttavia, const garantisce solo che il chiamante non può modificarlo (a meno che const_cast, ovviamente,). Non gestisce i casi di multithreading e non gestisce i casi in cui sono presenti callback (che potrebbero mutare l'oggetto originale). Passare un oggetto COW in base al valore pone le sfide della gestione di questi dettagli sullo sviluppatore API, piuttosto che sull'utente API.

Le nuove regole per C + 11 vietano std::stringin particolare COW . Gli iteratori su una stringa devono essere invalidati se il buffer di supporto è rimosso. Se l'iteratore veniva implementato come char*(A differenza di a string*e un indice), questi iteratori non sono più validi. La comunità C ++ doveva decidere con quale frequenza gli iteratori potevano essere invalidati e la decisione era che operator[]non doveva essere uno di quei casi. operator[]su a std::stringrestituisce a char&, che può essere modificato. Pertanto, operator[]sarebbe necessario staccare la stringa, invalidando gli iteratori. Questo era considerato un commercio scadente e, diversamente dalle funzioni come end()e cend(), non c'è modo di chiedere la versione const di operator[]short of const che lancia la stringa. ( correlato ).

COW è ancora vivo e ben al di fuori della STL. In particolare, l'ho trovato molto utile nei casi in cui è irragionevole per un utente delle mie API aspettarsi che ci sia qualche oggetto pesante dietro quello che sembra essere un oggetto molto leggero. Potrei utilizzare COW in background per garantire che non debbano mai preoccuparsi di tali dettagli di attuazione.


La mutazione della stessa stringa in più thread sembra una progettazione molto negativa, indipendentemente dal fatto che si utilizzino iteratori o []operatore. Quindi COW consente una cattiva progettazione - che non sembra un grande vantaggio :) Il punto nell'ultimo paragrafo sembra valido, ma io stesso non sono un grande fan del comportamento implicito - le persone tendono a darlo per scontato, e quindi hanno è difficile capire perché il codice non funziona come previsto e continuare a chiedersi fino a quando non riescono a controllare cosa è nascosto dietro il comportamento implicito.
dtech,

Per quanto riguarda il punto di utilizzo, const_castsembra che possa rompere COW con la stessa facilità con cui può interrompere il passaggio per riferimento const. Ad esempio, QString::constData()restituisce a const QChar *- const_castche e COW collassa - si muteranno i dati dell'oggetto originale.
dtech,

Se è possibile restituire dati da un COW, è necessario staccare prima di farlo, oppure restituire i dati in un modulo che è ancora COW ( char*ovviamente non lo è). Per quanto riguarda il comportamento implicito, penso che tu abbia ragione, ci sono problemi con esso. Il design delle API è un equilibrio costante tra i due estremi. Troppo implicito e le persone iniziano a fare affidamento su un comportamento speciale come se fosse di fatto parte delle specifiche. Troppo esplicito e l'API diventa troppo ingombrante quando si espongono troppi dettagli sottostanti che non erano davvero importanti e vengono improvvisamente scritti nelle specifiche dell'API.
Cort Ammon,

Credo che le stringclassi abbiano ottenuto il comportamento COW perché i progettisti del compilatore hanno notato che un grande corpus di codice stava copiando le stringhe anziché usare const-reference. Se aggiungessero COW, potrebbero ottimizzare questo caso e rendere felici più persone (ed era legale, fino al C ++ 11). Apprezzo la loro posizione: mentre passo sempre le mie stringhe per riferimento const, ho visto tutta quella spazzatura sintattica che toglie solo la leggibilità. Odio scrivere const std::shared_ptr<const std::string>&solo per catturare la semantica corretta!
Cort Ammon,

5

Per le stringhe e simili sembra che pessimizzerebbe i casi d'uso più comuni che no, poiché il caso comune per le stringhe è spesso stringhe di piccole dimensioni, e lì il sovraccarico di COW tenderebbe a superare di gran lunga il costo della semplice copia della stringa piccola. Una piccola ottimizzazione del buffer ha molto più senso per me lì per evitare l'allocazione dell'heap in questi casi invece delle copie di stringhe.

Se hai un oggetto più pesante, tuttavia, come un androide, e volevi copiarlo e sostituire semplicemente il suo braccio cibernetico, COW sembra abbastanza ragionevole come un modo per mantenere una sintassi mutabile, evitando la necessità di copiare in profondità l'intero androide solo per dare alla copia un braccio unico. Renderlo semplicemente immutabile come struttura di dati persistente a quel punto potrebbe essere superiore, ma un "COW parziale" applicato su singole parti Android sembra ragionevole per questi casi.

In tal caso, le due copie dell'androide condividono / istanza lo stesso busto, gambe, piedi, testa, collo, spalle, bacino, ecc. I soli dati che sarebbero diversi tra loro e non condivisi sono il braccio che è stato creato unico per il secondo androide a sovrascrivere il braccio.


Tutto questo va bene, ma non richiede COW ed è ancora soggetto a molte implicazioni dannose. Inoltre, c'è un aspetto negativo: potresti spesso voler eseguire l'istanziazione di oggetti e non intendo il tipo di istanziazione, ma copia un oggetto come istanza, quindi quando modifichi l'oggetto di origine, anche le copie vengono aggiornate. COW esclude semplicemente quella possibilità, poiché qualsiasi modifica a un oggetto "condiviso" la stacca.
dtech,

Correttezza L'IMO non dovrebbe essere "facile" da raggiungere, non con comportamenti impliciti. Un buon esempio di correttezza è la correttezza CONST, poiché è esplicita e non lascia spazio ad ambiguità o effetti collaterali invisibili. Avere qualcosa del genere "facile" e automatico non costruisce mai quel livello extra di comprensione di come funzionano le cose, il che non è solo importante per la produttività complessiva, ma praticamente elimina la possibilità di comportamenti indesiderati, il motivo per cui potrebbe essere difficile da individuare . Tutto ciò che è reso possibile implicitamente con COW è facile da ottenere anche esplicitamente, ed è più chiaro.
dtech,

La mia domanda era motivata da un dilemma se fornire o meno COW di default nella lingua su cui sto lavorando. Dopo aver ponderato i pro e i contro, ho deciso di non averlo per impostazione predefinita, ma come modificatore che può essere applicato a tipi nuovi o già esistenti. Sembra il migliore dei due mondi, puoi comunque avere l'implicazione di COW quando sei esplicito nel volerlo.
dtech,

@ddriver Quello che abbiamo è simile a un linguaggio di programmazione con il paradigma nodale, ad eccezione della semplicità il tipo di nodi utilizza la semantica dei valori e nessuna semantica del tipo di riferimento (forse un po 'affine a std::vector<std::string>prima che avessimo avuto emplace_backe spostare la semantica in C ++ 11) . Ma fondamentalmente stiamo anche usando l'istanziamento. Il sistema del nodo può o meno modificare i dati. Abbiamo cose come nodi pass-through che non fanno nulla con l'input ma ne generano solo una copia (sono lì per l'organizzazione utente del suo programma). In questi casi, tutti i dati vengono copiati in modo superficiale per tipi complessi ...

@ddriver Il nostro copy-on-write è effettivamente un tipo di processo di copia "rendere l'istanza univoca implicitamente al cambiamento" . Rende impossibile modificare l'originale. Se l'oggetto Aviene copiato e non viene fatto nulla sull'oggetto B, si tratta di una copia superficiale economica per tipi di dati complessi come le mesh. Ora, se modifichiamo B, i dati in cui modifichiamo Bdiventano univoci tramite COW, ma Anon vengono toccati (ad eccezione di alcuni conteggi di riferimenti atomici).
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.