Why Garbage Collection se ci sono indicatori intelligenti

62

In questi giorni, così tante lingue sono spazzatura raccolte. È persino disponibile per C ++ da terze parti. Ma C ++ ha RAII e puntatori intelligenti. Quindi a che serve usare la garbage collection? Sta facendo qualcosa in più?

E in altre lingue come C #, se tutti i riferimenti sono trattati come puntatori intelligenti (tenendo da parte RAII), per specifica e per implementazione, ci sarà ancora qualche bisogno di garbage collector? Se no, allora perché non è così?

    
posta Gulshan 27.12.2010 - 12:58
fonte

10 risposte

67

So, what's the point of using garbage collection?

Suppongo che tu intenda i puntatori intelligenti contati di riferimento e noterò che sono una forma (rudimentale) di garbage collection, quindi risponderò alla domanda "quali sono i vantaggi di altre forme di garbage collection rispetto ai riferimenti contati puntatori intelligenti "invece.

  • Precisione . Il conteggio dei riferimenti perde i cicli da solo, quindi i puntatori intelligenti conteggiati di riferimento perderanno la memoria in generale, a meno che non vengano aggiunte altre tecniche ai cicli di cattura. Una volta aggiunte queste tecniche, il vantaggio della semplicità del conteggio delle referenze è svanito. Inoltre, tieni presente che i GC di conteggio e tracciamento dei riferimenti basati su ambito consentono di raccogliere valori in momenti diversi, a volte il conteggio dei riferimenti raccoglie in anticipo e talvolta traccia i GC raccolti in precedenza.

  • Throughput . I puntatori intelligenti sono una delle forme meno efficienti di garbage collection, in particolare nel contesto di applicazioni multi-threaded quando i conteggi dei riferimenti vengono urtati atomicamente. Esistono tecniche avanzate di conteggio dei riferimenti progettate per alleviare questo problema, ma la tracciabilità dei GC è ancora l'algoritmo di scelta negli ambienti di produzione.

  • Latenza . Le tipiche implementazioni intelligenti dei puntatori consentono ai distruttori di valanghe, con conseguente tempi di pausa illimitati. Altre forme di raccolta dei rifiuti sono molto più incrementali e possono anche essere in tempo reale, ad es. Tapis roulant di Baker.

risposta data 27.12.2010 - 15:48
fonte
60

Dal momento che nessuno l'ha guardato da questa angolazione, riformulerò la tua domanda: perché inserire qualcosa nella lingua se puoi farlo in una libreria? Ignorando l'implementazione specifica e i dettagli sintattici, GC / smart pointers è fondamentalmente un caso speciale di quella domanda. Perché definire un garbage collector nella lingua stessa se è possibile implementarlo in una libreria?

Ci sono un paio di risposte a questa domanda. Il primo più importante:

  1. Assicurati che tutto il codice possa utilizzarlo per interoperare. Questo è, credo, il grande motivo per cui il riutilizzo del codice e la condivisione del codice non sono decollare fino a Java / C # / Python / Ruby. Le biblioteche devono comunicare e l'unico linguaggio condiviso affidabile che hanno è quello che si trova nelle specifiche del linguaggio stesso (e, fino a un certo punto, nella sua libreria standard). Se hai mai provato a riutilizzare le librerie in C ++, probabilmente hai sperimentato l'orrendo dolore che nessuna semantica di memoria standard causa. Voglio passare una struttura ad alcune lib. Ho passato un riferimento? Pointer? %codice%? %codice%? Sto passando la proprietà, o no? C'è un modo per indicarlo? Cosa succede se la lib ha bisogno di allocare? Devo dargli un allocatore? Non rendendo la gestione della memoria parte del linguaggio, il C ++ impone a ciascuna coppia di librerie di negoziare la propria strategia specifica qui, ed è davvero difficile convincerli tutti. GC rende questo un non-problema completo.

  2. Puoi disegnare la sintassi attorno ad esso. Poiché C ++ non incapsula la gestione della memoria stessa, deve fornire una serie di hook sintattici per consentire al codice a livello utente di esprimere tutto i dettagli. Sono presenti puntatori, riferimenti, scoped_ptr , operatori di dereferenziamento, operatori di riferimento indiretto, indirizzo di ecc. Se si esegue il rollover della gestione della memoria nella lingua stessa, la sintassi può essere progettata attorno a ciò. Tutti questi operatori scompaiono e la lingua diventa più semplice e semplice.

  3. Ottieni un elevato ritorno sull'investimento. Il valore generato da una determinata parte di codice viene moltiplicato per il numero di persone che lo utilizzano. Ciò significa che più utenti hai, più puoi permetterti di spendere su un pezzo di software. Quando sposti una funzione nella lingua, la userò tutti della lingua. Ciò significa che è possibile allocare più risorse rispetto a una libreria utilizzata solo da un sottoinsieme di tali utenti. Questo è il motivo per cui linguaggi come Java e C # dispongono di macchine virtuali di prim'ordine e di garbage collection di altissima qualità: i costi per svilupparli vengono ammortizzati su milioni di utenti.

risposta data 27.12.2010 - 23:06
fonte
29

Garbage Collection in pratica significa solo che gli oggetti allocati vengono rilasciati automaticamente quando non vengono più referenziati.

Più precisamente, vengono rilasciati quando diventano non raggiungibili per il programma, poiché gli oggetti di riferimento circolare non verranno mai rilasciati altrimenti.

Puntatori intelligenti fai semplicemente riferimento a qualsiasi struttura che si comporta come un normale puntatore, ma ha alcune funzionalità aggiuntive allegate. Questi includono , ma non si limitano alla deallocazione, ma anche a copy-on-write, controlli vincolati, ...

Ora, come hai detto, i puntatori intelligenti possono essere usati per implementare una forma di garbage collection.

Ma il filo del pensiero va nel modo seguente:

  1. La raccolta dei rifiuti è una cosa interessante, poiché è comoda e devo occuparmi di meno cose
  2. Pertanto: voglio la garbage collection nella mia lingua
  3. Ora, come posso ottenere GC nella mia lingua?

Naturalmente, è possibile progettarlo in questo modo dall'inizio. C # è stato progettato come garbage collection, quindi solo new il tuo oggetto e verrà rilasciato quando i riferimenti non rientrano nell'ambito di applicazione. Il modo in cui ciò viene fatto dipende dal compilatore.

Ma in C ++ non era prevista la garbage collection. Se assegniamo un puntatore int* p = new int; e non rientra nell'ambito, p stesso viene rimosso dallo stack, ma nessuno si occupa della memoria allocata.

Ora l'unica cosa che hai dall'inizio sono i destructors deterministici . Quando un oggetto lascia l'ambito in cui è stato creato, viene chiamato il suo distruttore. In combinazione con i modelli e l'overloading dell'operatore, è possibile progettare un oggetto wrapper che si comporta come un puntatore, ma utilizza la funzionalità distruttore per ripulire le risorse ad esso collegate (RAII). Si chiama questo un puntatore intelligente .

Questo è tutto altamente specifico del linguaggio C ++: sovraccarico dell'operatore, modelli, distruttori, ... In questa particolare situazione linguistica, hai sviluppato dei puntatori intelligenti per fornirti il GC che desideri.

Ma se si progetta una lingua con GC dall'inizio, questo è semplicemente un dettaglio di implementazione. Dici solo che l'oggetto verrà ripulito e il compilatore lo farà per te.

I puntatori intelligenti come in C ++ probabilmente non sarebbero nemmeno possibili in linguaggi come C #, che non hanno alcuna distruzione deterministica (C # lavora intorno a questo fornendo zucchero sintattico per chiamare un .Dispose() su determinati oggetti). Le risorse non referenziate alla fine saranno recuperate dal GC, ma non definite quando esattamente ciò accadrà.

E questo, a sua volta, può consentire al GC di svolgere il proprio lavoro in modo più efficiente. Essendo costruito nel più profondo del linguaggio rispetto ai puntatori intelligenti, che sono impostati su di esso, .NET GC può ad es. ritardare le operazioni di memoria ed eseguirle in blocchi per renderle più economiche o persino spostare la memoria in giro per aumentare l'efficienza in base alla frequenza di accesso degli oggetti.

    
risposta data 27.12.2010 - 13:24
fonte
4

Ci sono due grandi differenze, a mio parere, tra la raccolta dei rifiuti e i puntatori intelligenti usati per la gestione della memoria:

  1. I puntatori intelligenti non possono raccogliere spazzatura ciclica; la garbage collection può
  2. I puntatori intelligenti eseguono tutto il lavoro nei momenti di referenziazione, dereferenziazione e deallocazione, sul thread dell'applicazione; la garbage collection non ha bisogno di

Il primo significa che GC raccoglierà spazzatura che i puntatori intelligenti non lo faranno; se stai usando i puntatori intelligenti, devi evitare di creare questo tipo di spazzatura o essere pronto a gestirlo manualmente.

Quest'ultimo significa che, indipendentemente da quanto siano intelligenti i puntatori intelligenti, la loro operazione rallenta i thread di lavoro del programma. La garbage collection può posticipare il lavoro e spostarlo su altri thread; che lo rende più efficiente nel suo complesso (infatti, il costo di esecuzione di un GC moderno è inferiore a un normale malloc / sistema libero, anche senza il sovraccarico extra di puntatori intelligenti) e fa ciò che deve ancora fare il lavoro senza entrare nel modo dei thread dell'applicazione.

Ora, nota che i puntatori intelligenti, essendo costrutti programmatici, possono essere usati per fare ogni sorta di altre cose interessanti - vedi la risposta di Dario - che sono completamente al di fuori della portata della garbage collection. Se vuoi farlo, avrai bisogno di puntatori intelligenti.

Tuttavia, ai fini della gestione della memoria, non vedo alcuna prospettiva di puntatori intelligenti che sostituiscano la garbage collection. Semplicemente non sono così bravi.

    
risposta data 27.12.2010 - 13:51
fonte
3

Il termine garbage collection implica che ci sono dei rifiuti da raccogliere. In C ++ i puntatori intelligenti sono disponibili in più versioni, soprattutto l'unique_ptr. Unique_ptr è fondamentalmente una singola proprietà e un costrutto di scoping. In un pezzo di codice ben progettato, la maggior parte delle risorse allocate normalmente risiedono dietro i puntatori intelligenti unique_ptr e la proprietà di tali risorse sarà ben definita in ogni momento. Non c'è quasi nessun sovraccarico in unique_ptr e unique_ptr elimina la maggior parte dei problemi di gestione della memoria manuale che tradizionalmente spingevano le persone verso le lingue gestite. Ora che più core in esecuzione contemporaneamente stanno diventando più comuni, i principi di progettazione che guidano il codice per utilizzare la proprietà unica e ben definita in qualsiasi momento nel tempo diventa più importante per le prestazioni. L'uso del modello di calcolo dell'attore consente la costruzione di programmi con una quantità minima di stato condiviso tra i thread e la proprietà esclusiva gioca un ruolo importante nel rendere i sistemi ad alte prestazioni un uso efficiente di molti core senza il sovraccarico di condivisioni tra i dati dei thread e i requisiti impliciti mutex.

Anche in un programma ben progettato, specialmente in ambienti multi-thread, non tutto può essere espresso senza strutture di dati condivise, e per quelle strutture di dati che veramente richiedono, i thread devono comunicare. RAII in c ++ funziona abbastanza bene per problemi di durata in una singola installazione con thread, in una configurazione multi-thread la vita degli oggetti potrebbe non essere completamente definita in modo gerarchico. Per queste situazioni, l'uso di shared_ptr offre una grande parte della soluzione. Si crea proprietà condivisa di una risorsa e questo in C ++ è l'unico posto in cui si vede la spazzatura, ma in quantità così ridotte che un programma c ++ progettato correttamente dovrebbe essere considerato più per implementare la raccolta "lettiera" con condivisa-ptr rispetto alla raccolta completa della spazzatura come implementato in altre lingue. Il C ++ semplicemente non ha molto "spazzatura" da raccogliere.

Come affermato da altri, i puntatori intelligenti conteggiati di riferimento sono una forma di garbage collection e uno per questo ha un grosso problema. L'esempio utilizzato principalmente come svantaggio delle forme di conteggio di riferimento della garbage collection è il problema con la creazione di strutture di dati orfane collegate a puntatori intelligenti tra loro che creano cluster di oggetti che si mantengono l'un l'altro dalla raccolta. Mentre in un programma progettato secondo il modello di calcolo dell'attore, le strutture dati di solito non consentono che tali cluster non rilevabili si presentino in C ++, quando si utilizza l'ampio approccio di dati condivisi alla programmazione multi-thread, come viene usato prevalentemente in una grande parte del settore, questi gruppi orfani possono diventare rapidamente una realtà.

Quindi riassumendo tutto, se per uso di puntatore condiviso si intende l'ampio uso di unique_ptr combinato con il modello di attore dell'approccio di calcolo per la programmazione multi-thread e l'uso limitato di shared_ptr, che altre forme di garbage collection non comprarti eventuali benefici aggiuntivi. Se, tuttavia, un approccio condiviso a tutto ti porta a condividere shared_ptr ovunque, dovresti prendere in considerazione il passaggio da modelli di concorrenza o il passaggio a un linguaggio gestito più orientato verso una più ampia condivisione della proprietà e l'accesso concorrente alle strutture dati.

    
risposta data 26.01.2016 - 11:29
fonte
2

La maggior parte dei puntatori intelligenti vengono implementati utilizzando il conteggio dei riferimenti. Cioè, ogni puntatore intelligente che fa riferimento a un oggetto incrementa il conteggio dei riferimenti degli oggetti. Quando il conteggio viene azzerato, l'oggetto viene rilasciato.

Il problema esiste se hai riferimenti circolari. Cioè, A ha un riferimento a B, B ha un riferimento a C e C ha un riferimento a A. Se stai usando puntatori intelligenti, allora per liberare la memoria associata ad A, B & C devi inserire manualmente una "interruzione" del riferimento circolare (ad esempio utilizzando weak_ptr in C ++).

La raccolta dei rifiuti (in genere) funziona in modo molto diverso. Al giorno d'oggi la maggior parte dei netturbini usa un test di raggiungibilità . Cioè, guarda tutti i riferimenti sullo stack e quelli che sono accessibili a livello globale e quindi traccia ogni oggetto a cui questi riferimenti si riferiscono, e gli oggetti loro fanno riferimento, ecc. Tutto il resto è spazzatura .

In questo modo, i riferimenti circolari non contano più: finché A, B e C non sono raggiungibili , la memoria può essere recuperata.

Ci sono altri vantaggi nella "garbage collection" reale. Ad esempio, l'allocazione della memoria è estremamente economica: basta incrementare il puntatore alla "fine" del blocco di memoria. Anche la deallocazione ha un costo ammortizzato costante. Ma naturalmente linguaggi come il C ++ ti permettono di implementare la gestione della memoria praticamente come preferisci, in modo che tu possa elaborare una strategia di allocazione ancora più veloce.

Naturalmente, in C ++ la quantità di memoria allocata nell'heap è in genere inferiore a un linguaggio di riferimento come C # /. NET. Ma questo non è in realtà un problema di raccolta di rifiuti rispetto a smart pointers.

In ogni caso, il problema non è secco e uno è migliore dell'altro. Ognuno di essi ha vantaggi e svantaggi.

    
risposta data 27.12.2010 - 13:45
fonte
2

Riguarda rendimento . La disallocazione della memoria richiede molta amministrazione. Se l'unallocazione viene eseguita in background, le prestazioni del processo in primo piano aumentano. Purtroppo, l'allocazione della memoria non può essere pigra (gli oggetti allocati saranno usati nell'attimo successivo), ma possono essere rilasciati oggetti.

Prova in C ++ (senza GC) per allocare una grande quantità di oggetti, stampa "ciao", quindi cancellali. Sarai sorpreso di quanto tempo ci vuole per liberare oggetti.

Inoltre, GNU libc fornisce strumenti più efficaci per l'annullamento della memoria, vedi ostacoli . Deve saperlo, non ho esperienza con gli ostacoli, non li ho mai usati.

    
risposta data 27.12.2010 - 15:11
fonte
2

La raccolta dei dati inutili può essere più efficiente: fondamentalmente "accumula" il sovraccarico della gestione della memoria e fa tutto in una volta. In generale ciò comporterà un minor dispendio di risorse complessive della CPU sulla de-allocazione della memoria, ma ciò significa che a un certo punto si avrà una grande esplosione di attività di de-allocazione. Se il GC non è progettato correttamente, questo può diventare visibile all'utente come una "pausa" mentre il GC tenta di annullare la memoria. I GC più moderni sono molto bravi a mantenere questo invisibile all'utente tranne che nelle condizioni più avverse.

I puntatori intelligenti (o qualsiasi schema di conteggio dei riferimenti) hanno il vantaggio che si verificano esattamente quando ci si aspetterebbe di guardare il codice (il puntatore intelligente esce dal campo di applicazione, la cosa viene eliminata). Ottieni piccole raffiche di disallocazione qua e là. Nel complesso potresti utilizzare più tempo della CPU per la disallocazione, ma poiché è distribuito su tutte le cose che accadono nel tuo programma, è meno probabile (escludendo la disallocazione di una struttura di dati mostruosi) per renderlo visibile all'utente.

Se stai facendo qualcosa per cui la responsività è importante, ti suggerirei che i puntatori intelligenti / conteggio ref ti consentono di sapere esattamente quando accadono le cose, quindi puoi sapere mentre codifichi ciò che rischia di diventare visibile ai tuoi utenti. In un ambiente GC hai solo il controllo più effimero sul garbage collector e devi semplicemente provare a risolvere il problema.

D'altro canto, se il tuo obiettivo è il throughput complessivo, un sistema basato su GC può essere una scelta molto migliore, poiché riduce al minimo le risorse necessarie per la gestione della memoria.

Cicli: non considero significativo il problema dei cicli. In un sistema in cui hai puntatori intelligenti, tendi verso strutture di dati che non hanno cicli, o stai semplicemente attento a come lasci andare queste cose. Se necessario, gli oggetti custode che sanno come rompere i cicli negli oggetti di proprietà possono essere utilizzati per assicurare automaticamente la distruzione corretta. In alcuni ambiti della programmazione questo può essere importante, ma per la maggior parte del lavoro quotidiano è irrilevante.

    
risposta data 27.12.2010 - 18:09
fonte
1

La limitazione numero uno dei puntatori intelligenti è che non sempre aiutano contro riferimenti circolari. Ad esempio, si dispone di un oggetto A che memorizza un puntatore intelligente sull'oggetto B e l'oggetto B sta memorizzando un puntatore intelligente sull'oggetto A. Se vengono lasciati insieme senza reimpostare nessuno dei puntatori, non verranno mai deallocati.

Questo accade perché un puntatore intelligente deve eseguire un'azione specifica che non sarà sottoposta a un triiger nello scenario sopra perché entrambi gli oggetti sono irraggiungibili per il programma. La raccolta dei dati inutili sarà in grado di identificare correttamente gli oggetti che non sono raggiungibili dal programma e verranno raccolti.

    
risposta data 27.12.2010 - 13:40
fonte
0

Ricorda che alla fine tutto si riduce a una procedura di esecuzione della CPU. A mia conoscenza, tutte le CPU di livello consumer dispongono di set di istruzioni che richiedono la memorizzazione dei dati in un dato luogo in memoria e sono disponibili riferimenti a tali dati. Questo è tutto ciò che hai al livello base.

Tutto ciò al di sopra di ciò con la garbage collection, i riferimenti ai dati che potrebbero essere stati spostati, la compattazione dell'heap, ecc. ecc. stanno facendo il lavoro entro le restrizioni date dal paradigma "frammento di memoria con un puntatore dell'indirizzo". Stessa cosa con i puntatori intelligenti: ANCORA devi eseguire il codice su hardware reale.

    
risposta data 27.12.2010 - 14:13
fonte

Leggi altre domande sui tag