Perché i puntatori intelligenti conteggio dei riferimenti sono così popolari?


52

Come posso vedere, i puntatori intelligenti sono ampiamente utilizzati in molti progetti C ++ nel mondo reale.

Sebbene alcuni tipi di puntatori intelligenti siano ovviamente utili per supportare RAII e i trasferimenti di proprietà, c'è anche una tendenza a utilizzare i puntatori condivisi per impostazione predefinita , come un modo di "garbage collection" , in modo che il programmatore non debba pensare all'allocazione così tanto .

Perché i puntatori condivisi sono più popolari dell'integrazione di un vero e proprio garbage collector come Boehm GC ? (O sei d'accordo, che sono più popolari dei GC attuali?)

Conosco due vantaggi dei GC convenzionali rispetto al conteggio dei riferimenti:

  • Gli algoritmi GC convenzionali non hanno problemi con i cicli di riferimento .
  • Il conteggio dei riferimenti è generalmente più lento di un GC adeguato.

Quali sono i motivi per utilizzare i puntatori intelligenti di conteggio dei riferimenti?


6
Vorrei solo aggiungere un commento che questo è un valore predefinito errato da utilizzare: nella maggior parte dei casi, std::unique_ptrè sufficiente e come tale ha un overhead over over puntatori non elaborati in termini di prestazioni di runtime. Usando std::shared_ptrovunque oscureresti anche la semantica della proprietà, perdendo uno dei maggiori vantaggi dei puntatori intelligenti diversi dalla gestione automatica delle risorse: una chiara comprensione dell'intento alla base del codice.
Matt

2
Ci dispiace ma la risposta accettata qui è completamente sbagliata. Il conteggio dei riferimenti ha costi generali più elevati (un conteggio invece di un bit di segno e prestazioni di runtime più lente), tempi di pausa illimitati quando diminuisce la valanga e non più complessi che, diciamo, il semispazio di Cheney.
Jon Harrop,

Risposte:


57

Alcuni vantaggi del conteggio dei riferimenti sulla raccolta dei rifiuti:

  1. Spese generali basse. I garbage collector possono essere piuttosto invadenti (ad esempio, il congelamento del programma in momenti imprevedibili durante l'elaborazione di un ciclo di garbage collection) e piuttosto intensivo in termini di memoria (ad esempio, il footprint di memoria del processo aumenta inutilmente a molti megabyte prima che la garbage collection abbia finalmente inizio)

  2. Comportamento più prevedibile. Con il conteggio dei riferimenti, sei sicuro che il tuo oggetto verrà liberato nell'istante in cui l'ultimo riferimento scompare. Con la garbage collection, d'altra parte, il tuo oggetto verrà liberato "qualche volta", quando il sistema vi aggirerà. Per la RAM questo di solito non è un grosso problema su desktop o server leggermente caricati, ma per altre risorse (ad es. Handle di file) è spesso necessario che vengano chiusi al più presto per evitare potenziali conflitti in seguito.

  3. Più semplice. Il conteggio dei riferimenti può essere spiegato in pochi minuti e implementato in un'ora o due. I netturbini, in particolare quelli con prestazioni decenti, sono estremamente complessi e non molte persone li capiscono.

  4. Standard. Il C ++ include il conteggio dei riferimenti (tramite shared_ptr) e gli amici nell'STL, il che significa che la maggior parte dei programmatori C ++ ha familiarità con esso e la maggior parte del codice C ++ funzionerà con esso. Tuttavia, non esiste un Garbage Collector C ++ standard, il che significa che devi sceglierne uno e sperare che funzioni bene per il tuo caso d'uso - e in caso contrario, è il tuo problema da risolvere, non il linguaggio.

Per quanto riguarda i presunti svantaggi del conteggio dei riferimenti, non rilevare i cicli è un problema, ma uno che non ho mai incontrato personalmente negli ultimi dieci anni di utilizzo del conteggio dei riferimenti. La maggior parte delle strutture di dati sono naturalmente acicliche e se ti imbatti in una situazione in cui hai bisogno di riferimenti ciclici (ad es. Puntatore parent in un nodo dell'albero) puoi semplicemente usare un pointer_ptr o un puntatore C grezzo per la "direzione all'indietro". Finché si è consapevoli del potenziale problema durante la progettazione delle strutture dei dati, non si tratta di un problema.

Per quanto riguarda le prestazioni, non ho mai avuto problemi con le prestazioni del conteggio dei riferimenti. Ho avuto problemi con le prestazioni della garbage collection, in particolare i congelamenti casuali che GC può sostenere, a cui l'unica soluzione ("non allocare oggetti") potrebbe essere riformulata come "non usare GC" .


16
Le implementazioni di conteggio dei riferimenti naïf generalmente ottengono un throughput molto inferiore rispetto ai GC di produzione (30–40%) a spese della latenza. Il divario può essere chiuso con ottimizzazioni come l'uso di un minor numero di bit per il refcount ed evitare il tracciamento degli oggetti fino a quando non scappano; il C ++ dura quest'ultima naturalmente se si make_sharedritorna principalmente . Tuttavia, la latenza tende ad essere il problema più grande nelle applicazioni in tempo reale, ma il throughput è generalmente più importante, motivo per cui i GC di tracciamento sono così ampiamente utilizzati. Non sarei così veloce a parlarne male.
Jon Purdy,

3
Mi piacerebbe cavillare 'semplice': più semplice dal punto di vista della quantità totale di implementazione richiesto sì, ma non più semplice per il codice che usi IT: confronta dire a qualcuno come utilizzare RC ( 'fanno questo durante la creazione di oggetti e questo quando distruggerli' ) su come (ingenuamente, che spesso è abbastanza) usare GC ('...').
AakashM

4
"Con il conteggio dei riferimenti, sei sicuro che il tuo oggetto verrà liberato nell'istante in cui l'ultimo riferimento scompare". Questo è un malinteso comune. flyingfrogblog.blogspot.co.uk/2013/10/…
Jon Harrop,

4
@JonHarrop: quel post sul blog è orribilmente sbagliato. Dovresti anche leggere tutti i commenti, specialmente l'ultimo.
Deduplicatore,

3
@JonHarrop: Sì, c'è. Non capisce che la vita è l'ambito completo che arriva al tutore di chiusura. E l'ottimizzazione in F #, che secondo i commenti funziona solo a volte, sta terminando la vita prima, se la variabile non viene utilizzata di nuovo. Che naturalmente ha i suoi pericoli.
Deduplicatore,

26

Per ottenere buone prestazioni da un GC, il GC deve essere in grado di spostare oggetti in memoria. In un linguaggio come il C ++ in cui è possibile interagire direttamente con le posizioni di memoria, questo è praticamente impossibile. (Microsoft C ++ / CLR non conta perché introduce una nuova sintassi per i puntatori gestiti da GC ed è quindi effettivamente una lingua diversa.)

Il GC Boehm, sebbene un'idea intelligente, è in realtà il peggiore di entrambi i mondi: hai bisogno di un malloc () che è più lento di un buon GC, e quindi perdi il comportamento deterministico di allocazione / deallocazione senza il corrispondente aumento delle prestazioni di un GC generazionale . Inoltre è per necessità conservatore, quindi non raccoglierà necessariamente tutta la tua spazzatura comunque.

Un buon GC ben calibrato può essere un'ottima cosa. Ma in un linguaggio come il C ++, i guadagni sono minimi e i costi spesso non ne valgono la pena.

Sarà interessante vedere, tuttavia, man mano che il C ++ 11 diventerà più popolare, se lambda e semantica di acquisizione inizieranno a guidare la comunità C ++ verso gli stessi tipi di problemi di allocazione e di durata degli oggetti che hanno portato la comunità Lisp a inventare GC nel primo posto.

Vedi anche la mia risposta a una domanda correlata su StackOverflow .


6
Per quanto riguarda il Boehm GC, mi sono talvolta chiesto quanto sia personalmente responsabile della tradizionale avversione al GC tra i programmatori C e C ++ semplicemente fornendo una brutta prima impressione della tecnologia in generale.
Leushenko,

@Leushenko Ben detto. Un caso in questione è questa domanda, in cui Boehm gc è chiamato un "proprio" gc, ignorando il fatto che è lento e praticamente garantito di perdere. Ho trovato questa domanda mentre cercavo se qualcuno implementasse un interruttore automatico in stile pitone per shared_ptr, che suona come un obiettivo utile per un'implementazione di c ++.
user4815162342

4

Come posso vedere, i puntatori intelligenti sono ampiamente utilizzati in molti progetti C ++ nel mondo reale.

Vero ma, oggettivamente, la stragrande maggioranza del codice è ora scritta in lingue moderne con tracciatori di immondizia.

Sebbene alcuni tipi di puntatori intelligenti siano ovviamente utili per supportare RAII e i trasferimenti di proprietà, c'è anche una tendenza a utilizzare i puntatori condivisi per impostazione predefinita, come un modo di "garbage collection", in modo che il programmatore non debba pensare all'allocazione così tanto .

Questa è una cattiva idea perché devi ancora preoccuparti dei cicli.

Perché i puntatori condivisi sono più popolari dell'integrazione di un vero e proprio garbage collector come Boehm GC? (O sei d'accordo, che sono più popolari dei GC attuali?)

Oh wow, ci sono così tante cose che non vanno nel tuo modo di pensare:

  1. Il GC di Boehm non è un GC "corretto" in alcun senso della parola. È davvero orribile. È conservativo, quindi perde ed è inefficiente dal design. Vedi: http://flyingfrogblog.blogspot.co.uk/search/label/boehm

  2. I puntatori condivisi non sono oggettivamente così popolari come GC perché la stragrande maggioranza degli sviluppatori utilizza ora i linguaggi GC e non ha bisogno di puntatori condivisi. Guarda Java e Javascript nel mercato del lavoro rispetto al C ++.

  3. Sembra che tu stia limitando la considerazione al C ++ perché, suppongo, pensi che GC sia un problema tangenziale. Non è (l' unico modo per ottenere un GC decente è progettare la lingua e la macchina virtuale sin dall'inizio), quindi stai introducendo un errore di selezione. Le persone che vogliono davvero una corretta raccolta dei rifiuti non si attaccano al C ++.

Quali sono i motivi per utilizzare i puntatori intelligenti di conteggio dei riferimenti?

Sei limitato al C ++ ma desideri avere una gestione automatica della memoria.


7
Ehm, è una domanda taggata c ++ che parla delle caratteristiche del C ++. Chiaramente, qualsiasi istruzione generale parlano all'interno del codice C ++, non la totalità di programmazione. Quindi, tuttavia, la raccolta dei rifiuti "obiettivamente" potrebbe essere in uso al di fuori del mondo C ++, il che alla fine è irrilevante per la domanda in corso.
Nicol Bolas,

2
L'ultima riga è palesemente sbagliata: sei in C ++ e felice di non essere costretto a trattare con GC ed è in ritardo con la liberazione delle risorse. C'è una ragione per cui ad Apple non piace GC, e la linea guida più importante per i linguaggi GC'd è: non creare spazzatura a meno che non si disponga di risorse inattive o non si possa farne a meno.
Deduplicatore il

3
@JonHarrop: Quindi, confronta piccoli programmi equivalenti con e senza GC, che non sono esplicitamente scelti per giocare a vantaggio di entrambe le parti. Quale ti aspetti di aver bisogno di più memoria?
Deduplicatore

1
@Deduplicator: posso immaginare programmi che danno entrambi i risultati. Il conteggio dei riferimenti supererebbe la traccia del GC quando il programma è progettato per mantenere l'heap alloca la memoria fino a quando non sopravvive al vivaio (ad esempio una coda di elenchi) perché si tratta di prestazioni patologiche per un GC generazionale e genererebbe la spazzatura più fluttuante. Tracciare la garbage collection richiederebbe meno memoria del conteggio dei riferimenti basato sull'ambito quando ci sono molti piccoli oggetti e le vite sono brevi ma non staticamente ben note, quindi qualcosa come un programma logico che utilizza strutture di dati puramente funzionali.
Jon Harrop,

3
@JonHarrop: intendevo con GC (traccia o altro) e RAII se parli C ++. Il che include il conteggio dei riferimenti, ma solo dove è utile. Oppure potresti confrontare con un programma Swift.
Deduplicatore

3

In MacOS X e iOS e con gli sviluppatori che usano Objective-C o Swift, il conteggio dei riferimenti è popolare perché viene gestito automaticamente e l'uso della raccolta dei rifiuti è notevolmente diminuito poiché Apple non lo supporta più (mi hanno detto che le app che utilizzano garbage collection si interromperà nella prossima versione di MacOS X e garbage collection non è mai stata implementata in iOS). In realtà dubito seriamente che ci fosse sempre molto software che utilizzava la garbage collection quando era disponibile.

Il motivo per sbarazzarsi della garbage collection: non ha mai funzionato in modo affidabile in un ambiente in stile C in cui i puntatori potrebbero "sfuggire" ad aree non accessibili al garbage collector. Apple ha creduto fermamente e ritiene che il conteggio dei riferimenti sia più veloce. (Puoi fare qualsiasi reclamo qui sulla velocità relativa, ma nessuno è stato in grado di convincere Apple). E alla fine, nessuno ha usato la raccolta dei rifiuti.

La prima cosa che apprende qualsiasi sviluppatore di MacOS X o iOS è come gestire i cicli di riferimento, quindi non è un problema per un vero sviluppatore.


Per come lo capisco, non è che è un ambiente simile a C che ha deciso le cose, ma che GC è indeterministico e ha bisogno di molta più memoria per avere prestazioni accettabili e server / desktop esterno che è sempre un po 'scarso.
Deduplicatore il

Il debugging del motivo per cui il garbage collector ha distrutto un oggetto che stavo ancora usando (portando a un incidente) lo ha deciso per me :-)
gnasher729,

Oh sì, lo farebbe anche questo. Alla fine hai scoperto perché?
Deduplicatore,

Sì, era una delle molte funzioni Unix in cui si passa un vuoto * come "contesto" che viene restituito in una funzione di callback; il vuoto * era in realtà un oggetto Objective-C e il garbage collector non si rendeva conto che l'oggetto era stato nascosto nella chiamata Unix. Viene chiamato il callback, lancia void * su Object *, kaboom!
gnasher729,

2

Il più grande svantaggio della garbage collection in C ++ è che è semplicemente impossibile ottenere il giusto:

  • In C ++, i puntatori non vivono nella propria comunità murata, sono mescolati con altri dati. Pertanto, non è possibile distinguere un puntatore da altri dati che hanno semplicemente un modello di bit che può essere interpretato come un puntatore valido.

    Conseguenza: qualsiasi garbage collector C ++ perderà oggetti che dovrebbero essere raccolti.

  • In C ++, è possibile eseguire l'aritmetica dei puntatori per derivare i puntatori. Pertanto, se non si trova un puntatore all'inizio di un blocco, ciò non significa che non è possibile fare riferimento a quel blocco.

    Conseguenza: qualsiasi garbage collector C ++ deve tenere conto di queste regolazioni, trattando qualsiasi sequenza di bit che capita di puntare ovunque all'interno di un blocco, incluso subito dopo la fine di esso, come un puntatore valido che fa riferimento al blocco.

    Nota: nessun garbage collector C ++ può gestire il codice con trucchi come questi:

    int* array = new int[7];
    array--;    //undefined behavior, but people may be tempted anyway...
    for(int i = 1; i <= 7; i++) array[i] = i;

    È vero, questo invoca un comportamento indefinito. Ma un po 'di codice esistente è più intelligente di quanto non sia utile e può innescare la deallocazione preliminare di un garbage collector.


2
" sono mescolati con altri dati. " Non è così tanto che sono "mescolati" con altri dati. È facile usare il sistema di tipo C ++ per vedere cos'è un puntatore e cosa no. Il problema è che i puntatori diventano spesso altri dati. Nascondere un puntatore in un numero intero è purtroppo uno strumento comune per molte API di tipo C.
Nicol Bolas,

1
Non hai nemmeno bisogno di un comportamento indefinito per rovinare un garbage collector in c ++. Ad esempio, è possibile serializzare un puntatore a un file e leggerlo in seguito. Nel frattempo, il tuo processo potrebbe non contenere quel puntatore da nessuna parte nel suo spazio degli indirizzi, quindi il garbage collector potrebbe raccogliere quell'oggetto, e poi quando deserializzi il puntatore ... Whoops.
Bwmat,

@Bwmat "Even"? Scrivere puntatori su un file del genere sembra un po '... inverosimile. Ad ogni modo, lo stesso grave problema affligge i puntatori per impilare oggetti, potrebbero sparire quando si legge il puntatore indietro da un file altrove nel codice! La deserializzazione di un valore di puntatore non valido è un comportamento indefinito, non farlo.
hyde,

Se certo, dovresti stare attento se stai facendo qualcosa del genere. Doveva essere un esempio che, in generale, un garbage collector non può funzionare "correttamente" in tutti i casi in c ++ (senza cambiare la lingua)
Bwmat,

1
@ gnasher729: Ehm, no? I puntatori del passato-fine vanno perfettamente bene?
Deduplicatore
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.