Esiste un equivalente non atomico di std :: shared_ptr? E perché non ce n'è uno in <memory>?


88

Questa è una domanda un po 'in due parti, tutta sull'atomicità di std::shared_ptr:

1. Per quanto ne so, std::shared_ptrè l'unico puntatore intelligente <memory>che è atomico. Mi chiedo se sia disponibile una versione non atomica std::shared_ptr(non riesco a vedere nulla <memory>, quindi sono aperto anche a suggerimenti al di fuori dello standard, come quelli in Boost). So che boost::shared_ptrè anche atomico (se BOOST_SP_DISABLE_THREADSnon è definito), ma forse c'è un'altra alternativa? Sto cercando qualcosa che abbia la stessa semantica di std::shared_ptr, ma senza l'atomicità.

2. Capisco perché std::shared_ptrè atomico; è piuttosto carino. Tuttavia, non è piacevole per ogni situazione e il C ++ ha storicamente avuto il mantra di "pagare solo per quello che usi". Se non sto usando più thread o se sto usando più thread ma non condivido la proprietà del puntatore tra i thread, un puntatore intelligente atomico è eccessivo. La mia seconda domanda è perché nonstd::shared_ptr è stata fornita una versione non atomica di C ++ 11 ? (supponendo che ci sia un perché ) (se la risposta è semplicemente "una versione non atomica semplicemente non è stata mai considerata" o "nessuno ha mai chiesto una versione non atomica" va bene!).

Con la domanda n. 2, mi chiedo se qualcuno abbia mai proposto una versione non atomica di shared_ptr(a Boost o al comitato degli standard) (non per sostituire la versione atomica di shared_ptr, ma per coesistere con essa) ed è stata abbattuta per un motivo specifico.


4
Di quale "costo" ti preoccupi esattamente qui? Il costo dell'incremento atomico di un numero intero? È davvero un costo che ti preoccupa per qualsiasi applicazione reale? O stai solo ottimizzando prematuramente?
Nicol Bolas

9
@NicolBolas: è più curiosità che altro; Non ho (attualmente) alcun codice / progetto in cui desidero seriamente utilizzare un puntatore condiviso non atomico. Tuttavia, ho avuto progetti (in passato) in cui Boost è shared_ptrstato un rallentamento significativo a causa della sua atomicità, e la definizione ha BOOST_DISABLE_THREADSfatto una notevole differenza (non so se std::shared_ptravrebbe avuto lo stesso costo che boost::shared_ptraveva).
Steli di mais

13
@ Elettori stretti: quale parte della domanda non è costruttiva? Se non c'è un motivo specifico per la seconda domanda, va bene (un semplice "semplicemente non è stato considerato" sarebbe una risposta sufficientemente valida). Sono curioso di sapere se esiste una ragione / logica specifica. E la prima domanda è certamente una domanda valida, direi. Se devo chiarire la domanda o apportare lievi modifiche, per favore fatemelo sapere. Ma non vedo come non sia costruttivo.
Steli di mais

10
@Cornstalks Beh, probabilmente è solo che le persone non reagiscono così bene alle domande che possono facilmente liquidare come "ottimizzazione prematura" , non importa quanto sia valida, ben posta o pertinente la domanda, immagino. Per quanto mi riguarda non vedo alcun motivo per chiudere questo come non costruttivo.
Christian Rau

13
(non puoi scrivere una risposta ora che è chiuso, quindi commentare) Con GCC quando il tuo programma non usa più thread shared_ptrnon usa operazioni atomiche per il refcount. Vedere (2) su gcc.gnu.org/ml/libstdc++/2007-10/msg00180.html per una patch a GCC per consentire l'utilizzo dell'implementazione non atomica anche nelle app multithread, per gli shared_ptroggetti che non sono condivisi tra discussioni. Sono stato seduto su quella patch per anni ma sto pensando di impegnarmi finalmente per GCC 4.9
Jonathan Wakely,

Risposte:


104

1. Mi chiedo se sia disponibile una versione non atomica di std :: shared_ptr

Non previsto dallo standard. Potrebbe essercene uno fornito da una libreria di "terze parti". In effetti, prima di C ++ 11 e prima di Boost, sembrava che tutti scrivessero il proprio puntatore intelligente conteggio dei riferimenti (me compreso).

2. La mia seconda domanda è perché non è stata fornita una versione non atomica di std :: shared_ptr in C ++ 11?

La questione è stata discussa alla riunione di Rapperswil nel 2010. L'argomento è stato introdotto da un Commento dell'ente nazionale n. 20 della Svizzera. C'erano forti argomenti su entrambi i lati del dibattito, compresi quelli che fornisci nella tua domanda. Tuttavia, alla fine della discussione, il voto è stato in modo schiacciante (ma non unanime) contro l'aggiunta di una versione non sincronizzata (non atomica) di shared_ptr.

Argomenti contro inclusi:

  • Il codice scritto con shared_ptr non sincronizzato può finire per essere utilizzato nel codice threaded lungo la strada, finendo per causare problemi di debug difficili senza preavviso.

  • Avere un shared_ptr "universale" che è il "senso unico" per il traffico nel conteggio dei riferimenti ha dei vantaggi: Dalla proposta originale :

    Ha lo stesso tipo di oggetto indipendentemente dalle funzionalità utilizzate, facilitando notevolmente l'interoperabilità tra le librerie, comprese le librerie di terze parti.

  • Il costo dell'atomica, sebbene non nullo, non è esagerato. Il costo è mitigato dall'uso della costruzione del trasloco e dell'assegnazione del trasloco che non necessita di operazioni atomiche. Tali operazioni sono comunemente utilizzate per vector<shared_ptr<T>>cancellare e inserire.

  • Niente vieta alle persone di scrivere il proprio puntatore intelligente con conteggio dei riferimenti non atomici se è davvero quello che vogliono fare.

L'ultima parola del LWG di Rapperswil quel giorno fu:

Rifiuta CH 20. Nessun consenso per apportare una modifica in questo momento.


7
Wow, perfetto, grazie per l'informazione! Questo è esattamente il tipo di informazioni che speravo di trovare.
Steli di mais

> Has the same object type regardless of features used, greatly facilitating interoperability between libraries, including third-party libraries. questo è un ragionamento estremamente strano. Le librerie di terze parti forniranno comunque i propri tipi, quindi perché sarebbe importante se lo fornissero sotto forma di std :: shared_ptr <CustomType>, std :: non_atomic_shared_ptr <CustomType>, ecc.? dovrai sempre adattare il tuo codice a quello che restituisce la libreria
Jean-Michaël Celerier

Questo è vero per quanto riguarda i tipi specifici della libreria, ma l'idea è che ci sono anche molti posti in cui i tipi standard vengono visualizzati nelle API di terze parti. Ad esempio, la mia libreria potrebbe richiedere un file std::shared_ptr<std::string>da qualche parte. Se anche la libreria di qualcun altro accetta quel tipo, i chiamanti possono passare le stesse stringhe a entrambi senza l'inconveniente o il sovraccarico della conversione tra diverse rappresentazioni, e questa è una piccola vittoria per tutti.
Jack O'Connor

52

Howard ha già risposto bene alla domanda e Nicol ha sottolineato alcuni buoni punti sui vantaggi di avere un singolo tipo di puntatore condiviso standard, piuttosto che molti tipi incompatibili.

Anche se sono completamente d'accordo con la decisione del comitato, penso che ci sia qualche vantaggio nell'usare un shared_ptrtipo non sincronizzato in casi speciali , quindi ho esaminato l'argomento alcune volte.

Se non sto utilizzando più thread o se sto utilizzando più thread ma non condivido la proprietà del puntatore tra i thread, un puntatore intelligente atomico è eccessivo.

Con GCC quando il tuo programma non utilizza più thread shared_ptr non usa operazioni atomiche per il refcount. Questo viene fatto aggiornando i conteggi dei riferimenti tramite funzioni wrapper che rilevano se il programma è multithread (su GNU / Linux questo viene fatto semplicemente rilevando se il programma si collega a libpthread.so) e invia di conseguenza a operazioni atomiche o non atomiche.

Mi sono reso conto molti anni fa che poiché GCC shared_ptr<T>è implementato in termini di una __shared_ptr<T, _LockPolicy>classe base , è possibile utilizzare la classe base con la politica di blocco a thread singolo anche in codice multithread, utilizzando esplicitamente __shared_ptr<T, __gnu_cxx::_S_single>. Sfortunatamente, poiché non era un caso d'uso previsto, non funzionava in modo ottimale prima di GCC 4.9 e alcune operazioni utilizzavano ancora le funzioni wrapper e quindi inviate alle operazioni atomiche anche se hai esplicitamente richiesto la _S_singlepolitica. Vedere il punto (2) su http://gcc.gnu.org/ml/libstdc++/2007-10/msg00180.htmlper maggiori dettagli e una patch a GCC per consentire l'utilizzo dell'implementazione non atomica anche nelle app multithread. Mi sono seduto su quella patch per anni, ma alla fine l'ho impegnata per GCC 4.9, che ti consente di utilizzare un modello di alias come questo per definire un tipo di puntatore condiviso che non è thread-safe, ma è leggermente più veloce:

template<typename T>
  using shared_ptr_unsynchronized = std::__shared_ptr<T, __gnu_cxx::_S_single>;

Questo tipo non sarebbe interoperabile std::shared_ptr<T>e sarebbe sicuro da usare solo quando è garantito che gli shared_ptr_unsynchronizedoggetti non sarebbero mai condivisi tra i thread senza un'ulteriore sincronizzazione fornita dall'utente.

Questo è ovviamente completamente non portatile, ma a volte va bene. Con gli hack del preprocessore giusti il ​​tuo codice funzionerebbe ancora bene con altre implementazioni se shared_ptr_unsynchronized<T>è un alias per shared_ptr<T>, sarebbe solo un po 'più veloce con GCC.


Se stai utilizzando un GCC prima della 4.9, potresti usarlo aggiungendo le _Sp_counted_base<_S_single>specializzazioni esplicite al tuo codice (e assicurandoti che nessuno crei mai istanze __shared_ptr<T, _S_single>senza includere le specializzazioni, per evitare violazioni ODR). L'aggiunta di tali specializzazioni di stdtipi è tecnicamente indefinita, ma lo sarebbe funziona in pratica, perché in questo caso non c'è differenza tra me che aggiungo le specializzazioni a GCC o che tu le aggiungi al tuo codice.


2
Mi chiedo solo, c'è un errore di battitura nel tuo esempio di alias del modello? Cioè penso che dovrebbe leggere shared_ptr_unsynchronized = std :: __ shared_ptr <. Per inciso, l'ho testato oggi, insieme a std :: __ enable_shared_from_this e std :: __ weak_ptr, e sembra funzionare bene (gcc 4.9 e gcc 5.2). Lo profilerò / disassemblerò a breve per vedere se effettivamente le operazioni atomiche vengono saltate.
Carl Cook

Dettagli fantastici! Recentemente ho dovuto affrontare un problema, come descritto in questa domanda , che alla fine mi ha fatto esaminare il codice sorgente di std::shared_ptr, std::__shared_ptr, __default_lock_policye così via. Questa risposta ha confermato ciò che ho capito dal codice.
Nawaz

21

La mia seconda domanda è perché non è stata fornita una versione non atomica di std :: shared_ptr in C ++ 11? (supponendo che ci sia un perché).

Ci si potrebbe chiedere altrettanto facilmente perché non c'è un puntatore intrusivo o qualsiasi altra possibile variazione di puntatori condivisi che si potrebbero avere.

Il progetto shared_ptr, tramandato da Boost, è stato quello di creare una lingua franca standard minima di puntatori intelligenti. Che, in generale, puoi semplicemente tirarlo giù dal muro e usarlo. È qualcosa che verrebbe utilizzato in generale, in un'ampia varietà di applicazioni. Puoi metterlo in un'interfaccia e le buone probabilità sono che le brave persone saranno disposte a usarlo.

Il threading diventerà sempre più diffuso in futuro. In effetti, con il passare del tempo, il threading sarà generalmente uno dei mezzi principali per ottenere prestazioni. Richiedere il puntatore intelligente di base per fare il minimo necessario per supportare il threading facilita questa realtà.

Scaricare una mezza dozzina di puntatori intelligenti con piccole variazioni tra loro nello standard, o peggio ancora, un puntatore intelligente basato su criteri, sarebbe stato terribile. Tutti sceglierebbero il puntatore che preferiscono e rinuncerebbero a tutti gli altri. Nessuno sarebbe in grado di comunicare con nessun altro. Sarebbe come nelle situazioni attuali con le stringhe C ++, in cui ognuno ha il proprio tipo. Solo molto peggio, perché l'interoperabilità con le stringhe è molto più semplice dell'interoperabilità tra classi di puntatori intelligenti.

Boost, e per estensione il comitato, ha scelto un puntatore intelligente specifico da utilizzare. Forniva un buon equilibrio di funzionalità ed era ampiamente e comunemente utilizzato nella pratica.

std::vectorpresenta alcune inefficienze rispetto agli array nudi anche in alcuni casi angolari. Ha alcune limitazioni; alcuni usi vogliono davvero avere un limite rigido alla dimensione di a vector, senza usare un allocatore di lancio. Tuttavia, il comitato non ha progettato vectorper essere tutto per tutti. È stato progettato per essere un buon valore predefinito per la maggior parte delle applicazioni. Coloro per i quali non può funzionare possono semplicemente scrivere un'alternativa che soddisfi le loro esigenze.

Proprio come puoi fare per un puntatore intelligente se shared_ptrl'atomicità è un peso. Poi di nuovo, si potrebbe anche considerare di non copiarli così tanto.


7
+1 per "si potrebbe anche considerare di non copiarli così tanto".
Ali

Se colleghi un profiler, sei speciale e puoi semplicemente escludere argomenti come quelli sopra. Se non hai un requisito operativo difficile da soddisfare, non dovresti usare C ++. Discutere come te è un buon modo per rendere C ++ universalmente insultato da chiunque sia interessato a prestazioni elevate o bassa latenza.Questo è il motivo per cui i programmatori di giochi non usano STL, boost o persino eccezioni.
Hans Malherbe

Per chiarezza, penso che la citazione nella parte superiore della tua risposta dovrebbe essere "perché non è stata fornita una versione non atomica di std :: shared_ptr in C ++ 11?"
Charles Savoie il

4

Sto preparando un discorso su shared_ptr al lavoro. Ho utilizzato un boost modificato shared_ptr con evita malloc separato (come quello che può fare make_shared) e un parametro template per la policy di blocco come shared_ptr_unsynchronized menzionato sopra. Sto usando il programma di

http://flyingfrogblog.blogspot.hk/2011/01/boosts-sharedptr-up-to-10-slower-than.html

come prova, dopo aver ripulito le copie shared_ptr non necessarie. Il programma utilizza solo il thread principale e viene mostrato l'argomento test. Il test env è un notebook che esegue linuxmint 14. Ecco il tempo impiegato in secondi:

test run setup boost (1.49) std con make_shared modificato boost
mt-unsafe (11) 11.9 9 / 11.5 (-pthread on) 8.4  
atomico (11) 13,6 12,4 13,0  
mt-unsafe (12) 113,5 85,8 / 108,9 (-pthread su) 81,5  
atomico (12) 126,0 109,1 123,6  

Solo la versione "std" utilizza -std = cxx11 e -pthread probabilmente cambia lock_policy nella classe g ++ __shared_ptr.

Da questi numeri, vedo l'impatto delle istruzioni atomiche sull'ottimizzazione del codice. Il test case non utilizza contenitori C ++, ma vector<shared_ptr<some_small_POD>>è probabile che ne risentirà se l'oggetto non necessita della protezione del thread. Boost soffre meno probabilmente perché il malloc aggiuntivo limita la quantità di inlining e l'ottimizzazione del codice.

Devo ancora trovare una macchina con abbastanza core per testare la scalabilità delle istruzioni atomiche, ma usare std :: shared_ptr solo quando necessario è probabilmente meglio.


3

Boost fornisce un shared_ptrnon atomico. Si chiama local_shared_ptre può essere trovato nella libreria dei puntatori intelligenti di boost.


+1 per una risposta breve e solida con una buona citazione, ma questo tipo di puntatore sembra costoso - in termini sia di memoria che di runtime, a causa di un ulteriore livello di riferimento indiretto (local-> shared-> ptr vs shared-> ptr).
Red.Wave

@ Red.Wave Puoi spiegare cosa intendi con indiretto e come influisce sulle prestazioni? Intendi dire che è comunque un shared_ptrcon un bancone, anche se è locale? O vuoi dire che c'è un altro problema con esso? I documenti dicono che l' unica differenza è che questo non è atomico.
The Quantum Physicist

Ogni ptr locale tiene un conteggio e rifrazione al ptr condiviso originale. Pertanto, qualsiasi accesso alle punte finali richiede una derefrence dalla ptr locale a quella condivisa, che è quindi derefrence dalle punte. Quindi c'è un'altra indirezione accatastata alle indirezioni da ptr condiviso. E questo aumenta le spese generali.
Red.Wave

@ Red.Wave Da dove prendi queste informazioni? Questo: "Quindi qualsiasi accesso alle punte finali necessita di una deroga dal punto locale a quello condiviso" necessita di qualche citazione. Non sono riuscito a trovarlo nei documenti boost. Di nuovo, quello che ho visto nei documenti è che lo dice local_shared_ptre shared_ptrsono identici tranne che per atomici. Sono sinceramente interessato a scoprire se quello che dici è vero perché lo uso local_shared_ptrin applicazioni che richiedono prestazioni elevate.
The Quantum Physicist

3
@ Red.Wave Se guardi il codice sorgente effettivo github.com/boostorg/smart_ptr/blob/… vedrai che non c'è un doppio indiretto. Questo paragrafo nella documentazione è solo un modello mentale.
Ilya Popov
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.