Legalità dell'implementazione di COW std :: string in C ++ 11


117

std::stringAvevo capito che il copy-on-write non è un modo praticabile per implementare un conforming in C ++ 11, ma quando è emerso recentemente in discussione mi sono trovato incapace di supportare direttamente questa affermazione.

Ho ragione che C ++ 11 non ammette implementazioni basate su COW std::string?

In caso affermativo, questa restrizione è esplicitamente dichiarata da qualche parte nel nuovo standard (dove)?

Oppure questa restrizione è implicita, nel senso che è l'effetto combinato dei nuovi requisiti su std::stringciò che preclude un'implementazione basata su COW di std::string. In questo caso, sarei interessato a una derivazione in stile capitolo e verso di "C ++ 11 proibisce efficacemente le std::stringimplementazioni basate su COW ".


5
Il bug di GCC per la loro stringa COW è gcc.gnu.org/bugzilla/show_bug.cgi?id=21334#c45 . Uno dei bug che tiene traccia di una nuova implementazione del compilant C ++ 11 di std :: string in libstdc ++ è gcc.gnu.org/bugzilla/show_bug.cgi?id=53221
user7610

Risposte:


120

Non è consentito, perché secondo lo standard 21.4.1 p6, l'invalidazione di iteratori / riferimenti è consentita solo per

- come argomento di qualsiasi funzione di libreria standard che prenda un riferimento a stringa_base non const come argomento.

- Chiamare funzioni membro non const, eccetto operator [], at, front, back, begin, rbegin, end e rend.

Per una stringa COW, la chiamata a non-const operator[]richiederebbe la creazione di una copia (e l'invalidazione dei riferimenti), cosa non consentita dal paragrafo precedente. Quindi, non è più legale avere una stringa COW in C ++ 11.


4
Alcune motivazioni: N2534
MM

8
-1 La logica non regge l'acqua. Al momento di una copia COW non ci sono riferimenti o iteratori che possono essere invalidati, il punto centrale della copia è che tali riferimenti o iteratori vengono ora ottenuti, quindi la copia è necessaria. Ma può ancora essere che C ++ 11 non consenta le implementazioni COW.
Saluti e salute. - Alf

11
@ Cheersandhth.-Alf: La logica può essere vista di seguito se COW fosse consentito: std::string a("something"); char& c1 = a[0]; std::string b(a); char& c2 = a[1]; c1 è un riferimento a a. Quindi "copia" un file. Quindi, quando si tenta di prendere il riferimento la seconda volta, deve fare una copia per ottenere un riferimento non const poiché ci sono due stringhe che puntano allo stesso buffer. Ciò dovrebbe invalidare il primo riferimento preso, ed è contro la sezione citata sopra.
Dave S

9
@ Cheersandhth.-Alf, secondo questo , almeno l'implementazione COW di GCC fa esattamente quello che dice DaveS. Quindi almeno quello stile di COW è proibito dallo standard.
Tavian Barnes

4
@Alf: questa risposta sostiene che non-const operator[](1) deve fare una copia e che (2) è illegale farlo. Con quale di questi due punti non sei d'accordo? Guardando il tuo primo commento, sembra che un'implementazione possa condividere la stringa, almeno in base a questo requisito, fino al momento in cui si accede, ma che sia l'accesso in lettura che in scrittura dovrebbero annullare la condivisione. È questo il tuo ragionamento?
Ben Voigt

48

Le risposte di Dave S e gbjbaanb sono corrette . (E anche quello di Luc Danton è corretto, sebbene sia più un effetto collaterale del divieto delle stringhe COW piuttosto che la regola originale che lo vieta.)

Ma per chiarire un po 'di confusione, aggiungerò qualche ulteriore esposizione. Vari commenti si collegano a un mio commento sulla bugzilla di GCC che fornisce il seguente esempio:

std::string s("str");
const char* p = s.data();
{
    std::string s2(s);
    (void) s[0];
}
std::cout << *p << '\n';  // p is dangling

Lo scopo di questo esempio è dimostrare perché la stringa COW (reference counted) di GCC non è valida in C ++ 11. Lo standard C ++ 11 richiede che questo codice funzioni correttamente. Niente nel codice consente pche venga invalidato in C ++ 11.

Utilizzando la vecchia std::stringimplementazione conteggio dei riferimenti di GCC , quel codice ha un comportamento indefinito, perché p viene invalidato, diventando un puntatore penzolante. (Quello che succede è che quando s2viene costruito condivide i dati con s, ma l'ottenimento di un riferimento non const tramite s[0]richiede che i dati non siano condivisi, così sfa una "copia su scrittura" perché il riferimento s[0]potrebbe essere potenzialmente usato per scrivere s, quindi s2va fuori campo, distruggendo l'array puntato da p).

Lo standard C ++ 03 consente esplicitamente quel comportamento in 21.3 [lib.basic.string] p5 dove si dice che dopo una chiamata alla data()prima chiamata a operator[]()può invalidare puntatori, riferimenti e iteratori. Quindi la stringa COW di GCC era un'implementazione C ++ 03 valida.

Lo standard C ++ 11 non consente più quel comportamento, perché nessuna chiamata a operator[]()può invalidare puntatori, riferimenti o iteratori, indipendentemente dal fatto che seguano una chiamata a data().

Quindi l'esempio sopra deve funzionare in C ++ 11, ma non funziona con il tipo di stringa COW di libstdc ++, quindi quel tipo di stringa COW non è consentito in C ++ 11.


3
Un'implementazione che annulla la condivisione sulla chiamata a .data()(e su ogni ritorno di puntatore, riferimento o iteratore) non soffre di questo problema. Cioè (invariante) un buffer viene in qualsiasi momento non condiviso, oppure condiviso senza riferimenti esterni. Pensavo avessi inteso il commento su questo esempio come una segnalazione informale di bug come commento, mi dispiace molto per averlo frainteso! Ma come puoi vedere considerando l'implementazione come descrivo qui, che funziona bene in C ++ 11 quando i noexceptrequisiti vengono ignorati, l'esempio non dice nulla sul formale. Posso fornire il codice se vuoi.
Saluti e salute. - Alf

7
Se annulli la condivisione su quasi tutti gli accessi alla stringa, perdi tutti i vantaggi della condivisione. Un'implementazione COW deve essere pratica affinché una libreria standard si preoccupi di usarla come std::string, e dubito sinceramente che tu possa dimostrare una stringa COW utile e performante che soddisfi i requisiti di invalidazione C ++ 11. Quindi ritengo che le noexceptspecifiche aggiunte all'ultimo minuto siano una conseguenza del divieto delle stringhe COW, non il motivo sottostante. N2668 sembra perfettamente chiaro, perché continui a negare la chiara evidenza dell'intento del comitato delineato lì?
Jonathan Wakely

Inoltre, ricorda che data()è una funzione membro const, quindi deve essere sicuro chiamare contemporaneamente con altri membri const e, ad esempio, chiamare data()contemporaneamente con un altro thread che esegue una copia della stringa. Quindi avrai bisogno di tutto il sovraccarico di un mutex per ogni operazione di stringa, anche di const, o la complessità di una struttura con conteggio dei riferimenti mutabile priva di lock, e dopotutto ottieni la condivisione solo se non modifichi o accedi le tue stringhe, tante, tante stringhe avranno un conteggio dei riferimenti pari a uno. Fornisci il codice, sentiti libero di ignorare le noexceptgaranzie.
Jonathan Wakely

2
Mettendo insieme un po 'di codice ora ho scoperto che ci sono 129 basic_stringfunzioni membro, più funzioni gratuite. Costo di astrazione: questo codice di versione zero non ottimizzato pronto all'uso è dal 50 al 100% più lento sia con g ++ che con MSVC. Non fa thread safety (abbastanza facile da sfruttare shared_ptr, credo) ed è appena sufficiente per supportare l'ordinamento di un dizionario per motivi di tempistica, ma i bug del modulo dimostrano il punto che un riferimento conteggiato basic_stringè consentito, ad eccezione dei noexceptrequisiti C ++ . github.com/alfps/In-principle-demo-of-ref-counted-basic_string
Cheers and hth. - Alf


20

Lo è, CoW è un meccanismo accettabile per creare stringhe più veloci ... ma ...

rende il codice multithreading più lento (tutto quel blocco per verificare se sei l'unico a scrivere uccide le prestazioni quando usi molte stringhe). Questa è stata la ragione principale per cui CoW è stata uccisa anni fa.

Gli altri motivi sono che l' []operatore ti restituirà i dati della stringa, senza alcuna protezione per sovrascrivere una stringa che qualcun altro si aspetta non cambierà. Lo stesso vale per c_str()e data().

Quick google afferma che il multithreading è fondamentalmente il motivo per cui è stato effettivamente vietato (non esplicitamente).

La proposta dice:

Proposta

Si propone di rendere eseguibili simultaneamente tutte le operazioni di accesso agli iteratori e agli elementi.

Stiamo aumentando la stabilità delle operazioni anche nel codice sequenziale.

Questa modifica non consente in modo efficace le implementazioni di copia su scrittura.

seguito da

La più grande perdita potenziale di prestazioni dovuta a un passaggio dalle implementazioni copy-on-write è l'aumento del consumo di memoria per le applicazioni con stringhe di lettura molto grandi. Tuttavia, riteniamo che per queste applicazioni le corde siano una soluzione tecnica migliore e raccomandiamo di prendere in considerazione una proposta di fune da includere nella Libreria TR2.

Le corde fanno parte di STLPort e SGIs STL.


2
Il problema dell'operatore [] non è realmente un problema. La variante const offre protezione e la variante non const ha sempre la possibilità di fare il CoW in quel momento (o essere davvero pazzo e impostare un errore di pagina per attivarlo).
Christopher Smith,

+1 Va ai problemi.
Saluti e salute. - Alf

5
è semplicemente sciocco che una classe std :: cow_string non sia stata inclusa, con lock_buffer (), ecc. ci sono molte volte in cui so che il threading non è un problema. il più delle volte, in realtà.
Erik Aronesty

Mi piace il suggerimento di un'alternativa, ig ropes. Mi chiedo se sono disponibili altri tipi di alternative e implementazioni.
Voltaire

5

Dalla 21.4.2 costruttori basic_string e operatori di assegnazione [string.cons]

basic_string(const basic_string<charT,traits,Allocator>& str);

[...]

2 Effetti : costruisce un oggetto di classe basic_stringcome indicato nella Tabella 64. [...]

La Tabella 64 documenta utilmente che dopo la costruzione di un oggetto tramite questo costruttore (copia), this->data()ha come valore:

punta al primo elemento di una copia allocata dell'array il cui primo elemento è puntato da str.data ()

Esistono requisiti simili per altri costruttori simili.


+1 Spiega come C ++ 11 (almeno parzialmente) proibisce COW.
Saluti e salute. - Alf

Scusa, ero stanco. Non spiega niente di più che una chiamata di .data () deve attivare la copia COW se il buffer è attualmente condiviso. Tuttavia sono informazioni utili, quindi ho lasciato il voto positivo.
Saluti e salute. - Alf

1

Poiché ora è garantito che le stringhe siano archiviate in modo contiguo e ora puoi prendere un puntatore alla memoria interna di una stringa, (cioè & str [0] funziona come se fosse per un array), non è possibile creare un COW utile implementazione. Dovresti fare una copia per troppe cose. Anche solo l'utilizzo di operator[]o begin()su una stringa non const richiederebbe una copia.


1
Penso che le stringhe in C ++ 11 siano memorizzate in modo contiguo.
mfontanini

4
In passato
dovevi

@mfontanini sì, ma prima non lo erano
Dirk Holsopple il

3
Sebbene C ++ 11 garantisca che le stringhe siano contigue, ciò è ortogonale al divieto delle stringhe COW. La stringa COW di GCC è contigua, quindi chiaramente la tua affermazione che "non è possibile realizzare un'implementazione COW utile" è falsa.
Jonathan Wakely

1
@supercat, la richiesta del backing store (ad esempio chiamando c_str()) deve essere O (1) e non può lanciare, e non deve introdurre gare di dati, quindi è molto difficile soddisfare questi requisiti se si concatena pigramente. In pratica l'unica opzione ragionevole è quella di memorizzare sempre dati contigui.
Jonathan Wakely

1

COW è basic_stringvietato in C ++ 11 e versioni successive?

per quanto riguarda

" Ho ragione che C ++ 11 non ammette implementazioni basate su COW std::string?

Sì.

per quanto riguarda

"In caso affermativo, questa restrizione è esplicitamente dichiarata da qualche parte nel nuovo standard (dove)?

Quasi direttamente, in base a requisiti di complessità costante per una serie di operazioni che richiederebbero O ( n ) la copia fisica dei dati della stringa in un'implementazione COW.

Ad esempio, per le funzioni membro

auto operator[](size_type pos) const -> const_reference;
auto operator[](size_type pos) -> reference;

... che in un'implementazione COW ¹ entrambi attivano la copia dei dati della stringa per annullare la condivisione del valore della stringa, lo standard C ++ 11 richiede

C ++ 11 §21.4.5 / 4 :

Complessità: tempo costante.

... che esclude la copia di tali dati e, quindi, COW.

C ++ 03 supportato implementazioni COW da non avere questi requisiti costante complessità, e, in determinate condizioni restrittive, consentendo chiamate operator[](), at(), begin(), rbegin(), end(), o rend()per i riferimenti di invalidazione, puntatori e iteratori rinvio alle voci di stringa, cioè addirittura subire una Copia dei dati COW. Questo supporto è stato rimosso in C ++ 11.


COW è vietato anche tramite le regole di invalidazione C ++ 11?

In un'altra risposta che al momento della scrittura è selezionata come soluzione, e che è fortemente votata e quindi apparentemente creduta, si afferma che

Per una stringa COW, chiamare non- const operator[]richiederebbe la creazione di una copia (e l'invalidamento dei riferimenti), cosa non consentita dal paragrafo [tra virgolette] sopra [C ++ 11 §21.4.1 / 6]. Quindi, non è più legale avere una stringa COW in C ++ 11.

Questa affermazione è errata e fuorviante in due modi principali:

  • Indica erroneamente che solo le constfunzioni di accesso non articolo devono attivare una copia dei dati COW.
    Ma anche le constfunzioni di accesso agli elementi devono attivare la copia dei dati, perché consentono al codice client di formare riferimenti o puntatori che (in C ++ 11) non è consentito invalidare in seguito tramite le operazioni che possono attivare la copia dei dati COW.
  • Si presume erroneamente che la copia dei dati COW possa causare l'annullamento del riferimento.
    Ma in una corretta implementazione la copia dei dati COW, l'annullamento della condivisione del valore della stringa, viene eseguita in un punto prima che vi siano riferimenti che possono essere invalidati.

Per vedere come funzionerebbe una corretta implementazione C ++ 11 COW di basic_string, quando i requisiti O (1) che lo rendono non valido vengono ignorati, pensa a un'implementazione in cui una stringa può passare da una politica di proprietà all'altra. Un'istanza di stringa inizia con la policy Sharable. Con questo criterio attivo non possono esserci riferimenti a elementi esterni. L'istanza può passare alla policy Unique e deve farlo quando viene potenzialmente creato un riferimento a un elemento, ad esempio con una chiamata a .c_str()(almeno se ciò produce un puntatore al buffer interno). Nel caso generale di più istanze che condividono la proprietà del valore, ciò comporta la copia dei dati della stringa. Dopo la transizione alla policy Unique, l'istanza può tornare a Sharable solo tramite un'operazione che invalida tutti i riferimenti, come l'assegnazione.

Quindi, mentre la conclusione di quella risposta, che le stringhe COW sono escluse, è corretta, il ragionamento offerto è errato e fortemente fuorviante.

Sospetto che la causa di questo malinteso sia una nota non normativa nell'appendice C di C ++ 11:

C ++ 11 §C.2.11 [diff.cpp03.strings], su §21.3:

Modifica : i basic_stringrequisiti non consentono più stringhe con conteggio dei riferimenti.
Logica: l' invalidazione è leggermente diversa con le stringhe conteggiate in riferimento. Questa modifica regolarizza il comportamento (sic) per questo standard internazionale.
Effetto sulla funzionalità originale: il codice C ++ 2003 valido può essere eseguito in modo diverso in questo standard internazionale

Qui la logica spiega il motivo principale per cui si è deciso di rimuovere il supporto COW speciale per C ++ 03. Questa logica, il perché , non è il modo in cui lo standard impedisce effettivamente l'implementazione COW. Lo standard non consente COW tramite i requisiti O (1).

In breve, le regole di invalidazione C ++ 11 non escludono un'implementazione COW di std::basic_string. Ma escludono un'implementazione COW in stile C ++ 03 ragionevolmente efficiente e senza restrizioni come quella in almeno una delle implementazioni della libreria standard di g ++. Lo speciale supporto C ++ 03 COW ha consentito l'efficienza pratica, in particolare utilizzando le constfunzioni di accesso agli oggetti, a costo di regole sottili e complesse per l'invalidazione:

C ++ 03 §21.3 / 5 che include il supporto COW "prima chiamata":

" Riferimenti, puntatori e iteratori che si riferiscono agli elementi di una basic_stringsequenza possono essere invalidati dai seguenti usi di basic_stringquell'oggetto:
- Come argomento per funzioni non membri swap()(21.3.7.8), operator>>()(21.3.7.9) e getline()(21.3. 7.9).
- Come argomento per basic_string::swap().
- Chiamata data()e c_str()funzioni membro.
- Chiamata non constfunzioni membro, ad eccezione operator[](), at(), begin(), rbegin(), end(), e rend().
- A valle di uno degli usi di cui sopra, tranne le forme insert()e erase()che iteratori ritorno, la prima chiamata a non constfunzioni membro operator[](), at(), begin(), rbegin(),end(), o rend().

Queste regole sono così complesse e sottili che dubito che molti programmatori, se ce ne siano, potrebbero fornire un riassunto preciso. Non potevo.


Cosa succede se i requisiti O (1) vengono ignorati?

Se i requisiti di tempo costante di C ++ 11, ad esempio, operator[]vengono ignorati, COW per basic_stringpotrebbe essere tecnicamente fattibile, ma difficile da implementare.

Le operazioni che potrebbero accedere al contenuto di una stringa senza incorrere nella copia dei dati COW includono:

  • Concatenazione tramite +.
  • Uscita tramite <<.
  • Utilizzo di un basic_stringargomento come per le funzioni di libreria standard.

Quest'ultimo perché la libreria standard può fare affidamento su conoscenze e costrutti specifici dell'implementazione.

Inoltre, un'implementazione potrebbe offrire varie funzioni non standard per l'accesso ai contenuti delle stringhe senza attivare la copia dei dati COW.

Un fattore di complicanza principale è che in C ++ 11 l' basic_stringaccesso agli elementi deve attivare la copia dei dati (annullando la condivisione dei dati della stringa) ma è necessario non lanciare , ad esempio C ++ 11 §21.4.5 / 3 " Produce: niente.". E quindi non può utilizzare l'allocazione dinamica ordinaria per creare un nuovo buffer per la copia dei dati COW. Un modo per aggirare questo è utilizzare un heap speciale in cui la memoria può essere riservata senza essere effettivamente allocata, quindi riservare la quantità richiesta per ogni riferimento logico a un valore stringa. Riservare e annullare la prenotazione in un tale mucchio può essere tempo costante, O (1), e allocare l'importo che si è già prenotato può esserenoexcept. Per soddisfare i requisiti dello standard, con questo approccio sembra che dovrebbe esserci uno di questi heap speciali basati sulla prenotazione per allocatore distinto.


Note:
¹ La funzione di accesso constelemento attiva una copia dei dati COW perché consente al codice client di ottenere un riferimento o un puntatore ai dati, che non è consentito invalidare da una successiva copia dei dati attivata, ad esempio, dalla funzione di accesso non constelemento.


3
" Il tuo esempio è un buon esempio di un'implementazione errata per C ++ 11. Forse era corretto per C ++ 03." Sì, questo è il punto dell'esempio . Mostra una stringa COW che era legale in C ++ 03 perché non infrange le vecchie regole di invalidazione dell'iteratore e non è legale in C ++ 11 perché infrange le nuove regole di invalidazione dell'iteratore. E contraddice anche l'affermazione che ho citato nel commento sopra.
Jonathan Wakely

2
Se avessi detto condivisibile inizialmente non condiviso non avrei discusso. Dire che qualcosa è inizialmente condiviso è solo fonte di confusione. Condiviso con se stesso? Non è questo il significato della parola. Ma ripeto: il tuo tentativo di sostenere che le regole di invalidazione dell'iteratore C ++ 11 non vietano alcune ipotetiche stringhe COW che non sono mai state utilizzate nella pratica (e avrebbero prestazioni inaccettabili), quando sicuramente vietano il tipo di stringa COW che è stato utilizzato nella pratica, è in qualche modo accademico e inutile.
Jonathan Wakely

5
La tua stringa COW proposta è interessante, ma non sono sicuro di quanto sarebbe utile . Lo scopo di una stringa COW è copiare i dati della stringa solo nel caso in cui vengano scritte le due stringhe. L'implementazione suggerita richiede la copia quando si verifica un'operazione di lettura definita dall'utente. Anche se il compilatore sa che è solo una lettura, deve comunque copiare. Inoltre, la copia di una stringa Unique risulterà in una copia dei dati della sua stringa (presumibilmente in uno stato condivisibile), il che di nuovo rende COW piuttosto inutile. Quindi senza le garanzie di complessità, potresti scrivere ... una stringa COW davvero schifosa .
Nicol Bolas

2
Quindi, mentre sei tecnicamente corretto sul fatto che le garanzie di complessità sono ciò che ti impedisce di scrivere qualsiasi forma di COW, in realtà è [basic.string] / 5 che ti impedisce di scrivere qualsiasi forma veramente utile di stringa COW.
Nicol Bolas

4
@ JonathanWakely: (1) La tua citazione non è la domanda. Ecco la domanda: "Ho ragione che C ++ 11 non ammette implementazioni basate su COW di std :: string? In caso affermativo, questa restrizione è esplicitamente dichiarata da qualche parte nel nuovo standard (dove)? " (2) La tua opinione che una MUCCA std::string, ignorando i requisiti O (1), sarebbe inefficiente, è la tua opinione. Non so quale possa essere la performance, ma penso che quell'affermazione sia avanzata più per la sensazione, per le vibrazioni che trasmette, che per qualsiasi rilevanza per questa risposta.
Saluti e salute. - Alf

0

Mi sono sempre chiesto delle vacche immutabili: una volta creata la mucca potrei essere cambiata solo tramite assegnazione da un'altra vacca, quindi sarà conforme allo standard.

Ho avuto il tempo di provarlo oggi per un semplice test di confronto: una mappa di dimensione N codificata da stringa / mucca con ogni nodo che contiene un insieme di tutte le stringhe nella mappa (abbiamo un numero NxN di oggetti).

Con stringhe di dimensioni ~ 300 byte e N = 2000 le vacche sono leggermente più veloci e utilizzano quasi un ordine di grandezza in meno di memoria. Vedi sotto, le dimensioni sono in kbs, la corsa b è con le mucche.

~/icow$ ./tst 2000
preparation a
run
done a: time-delta=6 mem-delta=1563276
preparation b
run
done a: time-delta=3 mem-delta=186384
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.