Perché il C ++ non ha un Garbage Collector?


270

Non sto facendo questa domanda a causa dei meriti della raccolta dei rifiuti prima di tutto. La mia ragione principale per chiederlo è che so che Bjarne Stroustrup ha detto che C ++ avrà un garbage collector a un certo punto nel tempo.

Detto questo, perché non è stato aggiunto? Esistono già alcuni garbage collector per C ++. È solo una di quelle cose "più facili a dirsi che a farsi"? Oppure ci sono altri motivi per cui non è stato aggiunto (e non verrà aggiunto in C ++ 11)?

Collegamenti incrociati:

Giusto per chiarire, capisco i motivi per cui C ++ non aveva un Garbage Collector al momento della sua creazione. Mi chiedo perché non sia possibile aggiungere il raccoglitore.


26
Questo è uno dei dieci migliori miti sul C ++ che gli odiatori sollevano sempre. La Garbage Collection non è "integrata", ma ci sono molti semplici modi per farlo in C ++. Pubblicare un commento perché altri hanno già risposto meglio di quanto potessi fare qui sotto :)
davr

5
Ma questo è tutto il punto di non essere incorporato, devi farlo da solo. Realibility dall'alto verso il basso: incorporato, libreria, fatto in casa. Uso C ++ da solo, e sicuramente non è un odio perché è il miglior linguaggio al mondo. Ma la gestione dinamica della memoria è una seccatura.
QBziZ,

4
@Davr - Non sono un odiatore del C ++ né sto nemmeno cercando di sostenere che il C ++ abbia bisogno di un garbage collector. Lo sto chiedendo perché so che Bjarne Stroustrup ha detto che sarà aggiunto ed era solo curioso di sapere quali fossero le ragioni per non implementarlo.
Jason Baker,

1
Questo articolo The Boehm Collector per C e C ++ del Dr. Dobbs descrive un garbage collector open source che può essere utilizzato con C e C ++. Discute alcuni dei problemi che sorgono con l'uso di un garbage collector con distruttori C ++ e con la libreria standard C.
Richard Chambers,

1
@rogerdpack: Ma ormai non è così utile (vedi la mia risposta ...) quindi è improbabile che le implementazioni investano nel possederne uno.
einpoklum,

Risposte:


160

La raccolta dei rifiuti implicita avrebbe potuto essere aggiunta, ma non ha fatto il taglio. Probabilmente a causa non solo di complicazioni di implementazione, ma anche di non riuscire a raggiungere un consenso generale abbastanza velocemente.

Una citazione dello stesso Bjarne Stroustrup:

Avevo sperato che un garbage collector che potesse essere abilitato facoltativamente facesse parte di C ++ 0x, ma c'erano abbastanza problemi tecnici che devo accontentarmi di una specifica dettagliata di come un tale collector si integri con il resto del linguaggio , se previsto. Come nel caso essenzialmente di tutte le funzionalità C ++ 0x, esiste un'implementazione sperimentale.

C'è una buona discussione sull'argomento qui .

Panoramica generale:

Il C ++ è molto potente e ti consente di fare quasi tutto. Per questo motivo non spinge automaticamente su di te molte cose che potrebbero influire sulle prestazioni. La garbage collection può essere facilmente implementata con i puntatori intelligenti (oggetti che avvolgono i puntatori con un conteggio dei riferimenti, che si eliminano automaticamente quando il conteggio dei riferimenti raggiunge 0).

Il C ++ è stato creato pensando ai concorrenti che non avevano la garbage collection. L'efficienza era la principale preoccupazione da cui il C ++ doveva respingere le critiche rispetto al C e ad altri.

Esistono 2 tipi di garbage collection ...

Raccolta dei rifiuti esplicita:

C ++ 0x avrà garbage collection tramite puntatori creati con shared_ptr

Se lo vuoi puoi usarlo, se non lo vuoi non sei obbligato ad usarlo.

Al momento puoi usare boost: shared_ptr anche se non vuoi aspettare C ++ 0x.

Raccolta dei rifiuti implicita:

Non ha però la garbage collection trasparente. Sarà comunque un punto focale per le future specifiche C ++.

Perché Tr1 non ha la garbage collection implicita?

Ci sono molte cose che tr1 di C ++ 0x avrebbe dovuto avere, Bjarne Stroustrup nelle precedenti interviste affermava che tr1 non aveva quanto avrebbe voluto.


71
Vorrei diventare un nemico se C ++ garbage collection costretto su di me! Perché le persone non possono usare smart_ptr's? Come faresti a biforcare a basso livello in stile Unix, con un bidone della spazzatura in mezzo? Altre cose sarebbero interessate come il threading. Python ha il suo blocco di interprete globale principalmente a causa della sua garbage collection (vedi Cython). Tienilo fuori da C / C ++, grazie.
unixman83,

26
@ unixman83: il problema principale con la garbage collection contata con riferimento (ovvero std::shared_ptr) sono i riferimenti ciclici, che causano una perdita di memoria. Pertanto è necessario utilizzare attentamente std::weak_ptrper interrompere i cicli, che è disordinato. Mark and sweep style GC non presenta questo problema. Non vi è alcuna incompatibilità intrinseca tra threading / biforcazione e garbage collection. Java e C # hanno entrambi un multithreading preventivo ad alte prestazioni e un garbage collector. Esistono problemi relativi alle applicazioni in tempo reale e a un Garbage Collector, poiché la maggior parte dei Garbage Collector deve interrompere l'esecuzione del mondo.
Andrew Tomazos,

9
"Il problema principale con la raccolta dei rifiuti contata con riferimento (ovvero std::shared_ptr) sono riferimenti ciclici" e prestazioni terribili che sono ironiche perché prestazioni migliori sono di solito la giustificazione per l'utilizzo di C ++ ... flyingfrogblog.blogspot.co.uk/2011/01/…
Jon Harrop,

11
"Come faresti a biforcare a basso livello in stile Unix". Allo stesso modo in cui linguaggi GC come OCaml lo fanno da circa 20 anni o più.
Jon Harrop,

9
"Python ha il suo blocco di interprete globale principalmente a causa della sua garbage collection". Argomento di Strawman. Java e .NET hanno entrambi GC ma non hanno blocchi globali.
Jon Harrop,

149

Da aggiungere al dibattito qui.

Esistono problemi noti con Garbage Collection e la loro comprensione aiuta a capire perché non esiste nessuno in C ++.

1. Prestazioni?

La prima lamentela riguarda spesso le prestazioni, ma la maggior parte delle persone non capisce davvero di cosa sta parlando. Come illustrato dal Martin Beckettproblema potrebbe non essere la prestazione in sé, ma la prevedibilità della prestazione.

Attualmente ci sono 2 famiglie di GC ampiamente utilizzate:

  • Tipo Mark-And-Sweep
  • Tipo di conteggio di riferimento

Il Mark And Sweepè più veloce (meno impatto sulle prestazioni complessive), ma soffre di una sindrome di "congelare il mondo": vale a dire quando i calci GC in, tutto il resto è fermato fino a quando il GC ha fatto la sua pulizia. Se desideri costruire un server che risponda in pochi millisecondi ... alcune transazioni non saranno all'altezza delle tue aspettative :)

Il problema Reference Countingè diverso: il conteggio dei riferimenti aggiunge un sovraccarico, specialmente negli ambienti multi-thread perché è necessario disporre di un conteggio atomico. Inoltre c'è il problema dei cicli di riferimento, quindi è necessario un algoritmo intelligente per rilevare quei cicli ed eliminarli (generalmente implementare anche un "congelare il mondo", anche se meno frequente). In generale, ad oggi, questo tipo (anche se normalmente più reattivo o meglio, si blocca meno spesso) è più lento di Mark And Sweep.

Ho visto un documento degli implementatori di Eiffel che cercavano di implementare un Reference CountingGarbage Collector che avrebbe avuto una performance globale simile a quella Mark And Sweepsenza l'aspetto "Freeze The World". Richiede un thread separato per il GC (tipico). L'algoritmo è stato un po 'spaventoso (alla fine) ma il documento ha fatto un buon lavoro introducendo i concetti uno alla volta e mostrando l'evoluzione dell'algoritmo dalla versione "semplice" a quella a tutti gli effetti. Lettura consigliata se solo potessi rimettere le mani sul file PDF ...

2. Acquisizione delle risorse è inizializzazione (RAII)

È un linguaggio comune in C++quanto avvolgerai la proprietà delle risorse all'interno di un oggetto per assicurarti che vengano rilasciate correttamente. Viene utilizzato principalmente per la memoria poiché non abbiamo Garbage Collection, ma è utile anche per molte altre situazioni:

  • blocchi (multi-thread, handle di file, ...)
  • connessioni (a un database, un altro server, ...)

L'idea è di controllare correttamente la durata dell'oggetto:

  • dovrebbe essere vivo finché ne hai bisogno
  • dovrebbe essere ucciso quando hai finito con esso

Il problema di GC è che se aiuta con il primo e alla fine garantisce che in seguito ... questo "ultimo" potrebbe non essere sufficiente. Se rilasci un lucchetto, ti piacerebbe davvero che fosse rilasciato ora, in modo che non blocchi ulteriori chiamate!

Le lingue con GC hanno due soluzioni:

  • non usare GC quando l'allocazione dello stack è sufficiente: è normalmente per problemi di prestazioni, ma nel nostro caso aiuta davvero poiché l'ambito definisce la durata
  • usingcostruire ... ma è RAII esplicito (debole) mentre in C ++ RAII è implicito in modo che l'utente NON PUO 'fare involontariamente l'errore (omettendo la usingparola chiave)

3. Puntatori intelligenti

I puntatori intelligenti appaiono spesso come un proiettile d'argento per gestire la memoria C++. Spesso ho sentito: dopotutto non abbiamo bisogno di GC, dato che abbiamo puntatori intelligenti.

Uno non potrebbe essere più sbagliato.

I puntatori intelligenti aiutano: auto_ptre unique_ptrusano concetti RAII, estremamente utili. Sono così semplici che puoi scriverli da solo abbastanza facilmente.

Quando è necessario condividere la proprietà, tuttavia, diventa più difficile: è possibile condividere tra più thread e ci sono alcuni problemi sottili con la gestione del conteggio. Pertanto, si va naturalmente verso shared_ptr.

È fantastico, questo è ciò per cui Boost dopo tutto, ma non è un proiettile d'argento. In effetti, il problema principale shared_ptrè che emula un GC implementato da Reference Countingma è necessario implementare il rilevamento del ciclo da soli ... Urg

Ovviamente c'è questo weak_ptr, ma purtroppo ho già visto perdite di memoria nonostante l'uso a shared_ptrcausa di quei cicli ... e quando ti trovi in ​​un ambiente multi-thread, è estremamente difficile da rilevare!

4. Qual è la soluzione?

Non esiste un proiettile d'argento, ma come sempre è sicuramente fattibile. In assenza di GC è necessario essere chiari sulla proprietà:

  • preferire avere un unico proprietario in un dato momento, se possibile
  • in caso contrario, assicurati che il tuo diagramma di classe non abbia alcun ciclo pertinente alla proprietà e spezzali con una sottile applicazione di weak_ptr

Quindi, in effetti, sarebbe bello avere un GC ... comunque non è un problema banale. E nel frattempo, dobbiamo solo rimboccarci le maniche.


2
Vorrei poter accettare due risposte! Questo è semplicemente fantastico. Una cosa da sottolineare, per quanto riguarda le prestazioni, il GC che gira in un thread separato è in realtà piuttosto comune (è usato in Java e .Net). Certo, ciò potrebbe non essere accettabile nei sistemi embedded.
Jason Baker,

14
Solo due tipi? Che ne dici di copiare i collezionisti? Collezionisti generazionali? Collezionisti simultanei assortiti (incluso il duro tapis roulant in tempo reale di Baker)? Vari collezionisti ibridi? Amico, la pura ignoranza nell'industria di questo campo mi stupisce a volte.
SOLO IL MIO OPINIONE corretta,

12
Ho detto che c'erano solo 2 tipi? Ho detto che ce n'erano 2 ampiamente utilizzati. Per quanto ne so Python, Java e C # ora usano tutti gli algoritmi Mark e Sweep (Java aveva un algoritmo di conteggio dei riferimenti). Per essere ancora più precisi, mi sembra che C # usi GC generazionale per cicli minori, Mark And Sweep per cicli maggiori e copia per combattere la frammentazione della memoria; anche se direi che il cuore dell'algoritmo è Mark And Sweep. Conosci qualche linguaggio tradizionale che utilizza un'altra tecnologia? Sono sempre felice di imparare.
Matthieu M.

3
Hai appena nominato un linguaggio tradizionale che ne ha usati tre.
SOLO IL MIO OPINIONE corretta,

3
La differenza principale è che il GC generazionale e incrementale non ha bisogno di fermare il mondo per funzionare, e puoi farli funzionare su sistemi a thread singolo senza eccessivo sovraccarico eseguendo occasionalmente iterazioni del traversal tree quando accedi ai puntatori GC (il fattore può essere determinato dal numero di nuovi nodi, insieme a una previsione di base della necessità di raccogliere). Puoi approfondire ulteriormente GC includendo i dati su dove nel codice è avvenuta la creazione / modifica del nodo, il che potrebbe consentire di migliorare le tue previsioni e ottenere Escape Analysis gratuitamente con esso.
Keldon Alleyne,

56

Quale tipo? dovrebbe essere ottimizzato per controller di lavatrice, telefoni cellulari, workstation o supercomputer integrati?
Dovrebbe dare la priorità alla reattività della GUI o al caricamento del server?
dovrebbe usare molta memoria o molta CPU?

C / c ++ è usato in troppe circostanze diverse. Ho il sospetto che qualcosa come aumentare i puntatori intelligenti sarà sufficiente per la maggior parte degli utenti

Modifica: i garbage collector automatici non sono un problema di prestazioni (puoi sempre acquistare più server) è una questione di prestazioni prevedibili.
Non sapere quando il GC sta per entrare è come impiegare un pilota di una compagnia aerea narcolettica, il più delle volte sono fantastici, ma quando hai davvero bisogno di reattività!


6
Sicuramente vedo il tuo punto, ma mi sento in dovere di chiedere: Java non viene utilizzato in altrettante applicazioni?
Jason Baker,

35
No. Java non è adatto per applicazioni ad alte prestazioni, per la semplice ragione che non ha garanzie di prestazioni nella stessa misura di C ++. Quindi lo troverai in un telefono cellulare, ma non lo troverai in un interruttore cellulare o in un supercomputer.
Zathrus,

11
Puoi sempre acquistare più server, ma non puoi sempre acquistare più CPU per il cellulare già nella tasca del cliente!
Crashworks,

8
Java ha registrato molte prestazioni in termini di efficienza della CPU. Il problema davvero irrisolvibile è l'utilizzo della memoria, Java è intrinsecamente meno efficiente della memoria rispetto al C ++. E quell'inefficienza è dovuta al fatto che è raccolta dei rifiuti. La garbage collection non può essere sia veloce sia efficiente in termini di memoria, un fatto che diventa ovvio se si esamina la velocità con cui funzionano gli algoritmi GC.
Nate CK,

2
@Zathrus java può vincere sul throughput b / c della jit ottimizzante, anche se non sulla latenza (boo in tempo reale), e certamente non sull'impronta della memoria.
gtrak,

34

Uno dei maggiori motivi per cui il C ++ non ha incorporato la Garbage Collection è che far sì che la Garbage Collection funzioni bene con i distruttori è davvero, davvero difficile. Per quanto ne so, nessuno sa ancora come risolverlo completamente. Ci sono molti problemi da affrontare:

  • durata deterministica degli oggetti (il conteggio dei riferimenti ti dà questo, ma GC no. Anche se potrebbe non essere un grosso problema).
  • cosa succede se un distruttore lancia quando l'oggetto viene spazzato via? La maggior parte delle lingue ignora questa eccezione, dal momento che in realtà non esiste un blocco di cattura per poterlo trasportare, ma questa probabilmente non è una soluzione accettabile per C ++.
  • Come abilitarlo / disabilitarlo? Naturalmente sarebbe probabilmente una decisione in fase di compilazione, ma il codice scritto per GC vs il codice scritto per NOT GC sarà molto diverso e probabilmente incompatibile. Come conciliare questo?

Questi sono solo alcuni dei problemi affrontati.


17
GC e destructors sono un problema risolto, da parte di un simpatico evitatore di Bjarne. I distruttori non funzionano durante GC, perché non è questo il punto di GC. GC in C ++ esiste per creare la nozione di memoria infinita , non infinite altre risorse.
MSalters,

2
Se i distruttori non funzionano, ciò cambia completamente la semantica della lingua. Immagino che perlomeno avresti bisogno di una nuova parola chiave "gcnew" o qualcosa del genere in modo da consentire esplicitamente a questo oggetto di essere GC (e quindi non dovresti usarlo per avvolgere risorse oltre alla memoria).
Greg Rogers

7
Questo è un argomento falso. Poiché C ++ ha una gestione esplicita della memoria, è necessario capire quando ogni oggetto deve essere liberato. Con GC, non è peggio; piuttosto, il problema si riduce a capire quando alcuni oggetti vengono liberati, vale a dire quegli oggetti che richiedono particolari considerazioni al momento della cancellazione. L'esperienza nella programmazione in Java e C # rivela che la stragrande maggioranza degli oggetti non richiede considerazioni particolari e può essere tranquillamente lasciata al GC. A quanto pare, una delle principali funzioni dei distruttori in C ++ è quella di liberare oggetti figlio, che GC gestisce automaticamente.
Nate CK,

2
@ NateC-K: Una cosa che è migliorata in GC rispetto a non GC (forse la cosa più grande) è la capacità di un solido sistema GC di garantire che ogni riferimento continui a puntare allo stesso oggetto finché esiste il riferimento. Richiamare Disposeun oggetto può renderlo impossibile, ma i riferimenti che indicavano l'oggetto quando era vivo continueranno a farlo dopo che è morto. Al contrario, nei sistemi non GC, gli oggetti possono essere eliminati mentre esistono riferimenti e raramente esiste un limite al caos che può essere distrutto se uno di questi riferimenti viene utilizzato.
supercat

22

Sebbene questa sia una vecchia domanda, c'è ancora un problema che non vedo nessuno aver affrontato affatto: la garbage collection è quasi impossibile da specificare.

In particolare, lo standard C ++ è abbastanza attento a specificare il linguaggio in termini di comportamento osservabile esternamente, piuttosto che come l'implementazione raggiunge quel comportamento. Nel caso della raccolta dei rifiuti, tuttavia, non esiste praticamente alcun comportamento osservabile esternamente.

L' idea generale di Garbage Collection è che dovrebbe fare un ragionevole tentativo di garantire che un'allocazione di memoria abbia esito positivo. Sfortunatamente, è essenzialmente impossibile garantire che qualsiasi allocazione di memoria avrà successo, anche se si dispone di un Garbage Collector in funzione. Ciò è vero in una certa misura in ogni caso, ma in particolare nel caso del C ++, perché (probabilmente) non è possibile utilizzare un raccoglitore di copie (o qualcosa di simile) che sposta gli oggetti in memoria durante un ciclo di raccolta.

Se non riesci a spostare oggetti, non puoi creare un singolo spazio di memoria contiguo da cui eseguire le allocazioni - e ciò significa che il tuo heap (o negozio gratuito o come preferisci chiamarlo) può, e probabilmente lo farà , si frammenta nel tempo. Questo, a sua volta, può impedire il successo di un'allocazione, anche quando c'è più memoria libera rispetto alla quantità richiesta.

Mentre potrebbe essere possibile trovarne alcuni garanzia che dice (in sostanza) che se ripeti esattamente lo stesso modello di allocazione ripetutamente, e ha avuto successo la prima volta, continuerà ad avere successo su iterazioni successive, a condizione che la memoria allocata è diventato inaccessibile tra le iterazioni. Questa è una garanzia così debole che è essenzialmente inutile, ma non vedo alcuna ragionevole speranza di rafforzarla.

Anche così, è più forte di quanto è stato proposto per il C ++. La proposta precedente [avvertimento: PDF] (che è stata abbandonata) non garantiva nulla. In 28 pagine di proposta, ciò che hai ottenuto in termini di comportamento osservabile esternamente è stata una singola nota (non normativa) che diceva:

[Nota: per i programmi garbage collection, un'implementazione ospitata di alta qualità dovrebbe tentare di massimizzare la quantità di memoria non raggiungibile che recupera. —Endola nota]

Almeno per me, questo solleva una seria domanda sul ritorno sugli investimenti. Stiamo per violare il codice esistente (nessuno è sicuro di quanto, ma sicuramente abbastanza), imporre nuovi requisiti sulle implementazioni e nuove restrizioni sul codice, e ciò che otteniamo in cambio non è assolutamente nulla?

Anche nella migliore delle ipotesi, ciò che otteniamo sono programmi che, basandosi sui test con Java , probabilmente richiederanno circa sei volte più memoria per funzionare alla stessa velocità che fanno ora. Peggio ancora, la garbage collection faceva parte di Java sin dall'inizio - C ++ pone abbastanza più restrizioni sul garbage collector che quasi sicuramente avrà un peggio ancora rapporto costi / benefici (anche se andiamo oltre ciò che la proposta garantiva e presumiamo che ci sarebbe qualche vantaggio).

Riassumerei la situazione matematicamente: questa è una situazione complessa. Come ogni matematico sa, un numero complesso ha due parti: reale e immaginario. Mi sembra che ciò che abbiamo qui siano costi reali, ma benefici (almeno per lo più) immaginari.


Suppongo che anche se si specifica che per un corretto funzionamento tutti gli oggetti devono essere eliminati e solo gli oggetti che sono stati eliminati sarebbero idonei per la raccolta, il supporto del compilatore per la raccolta dei rifiuti di tracciamento dei riferimenti potrebbe ancora essere utile, poiché tale linguaggio potrebbe garantire l'uso di un puntatore eliminato (riferimento) sarebbe garantito per intercettare, piuttosto che causare un comportamento indefinito.
supercat

2
Anche in Java, il GC non è realmente specificato per fare qualcosa di utile AFAIK. Potrebbe chiamarti free(dove intendo freeanalogo al linguaggio C). Ma Java non garantisce mai di chiamare finalizzatori o cose del genere. In effetti, C ++ fa molto di più di Java per eseguire il commit di scritture di database, svuotare gli handle di file e così via. Java afferma di avere "GC", ma gli sviluppatori Java devono chiamare meticolosamente close()tutto il tempo e devono essere molto consapevoli della gestione delle risorse, facendo attenzione a non chiamare close()troppo presto o troppo tardi. Il C ++ ci libera da questo. ... (continua)
Aaron McDaid,

2
.. il mio commento un momento fa non ha lo scopo di criticare Java. Sto solo osservando che il termine "raccolta dei rifiuti" è un termine molto strano - significa molto meno di quanto la gente pensi che faccia e quindi è difficile discuterne senza essere chiari sul significato.
Aaron McDaid,

@AaronMcDaid È vero che GC non aiuta affatto con risorse non di memoria. Fortunatamente, tali risorse vengono allocate abbastanza raramente rispetto alla memoria. Inoltre, oltre il 90% di essi può essere liberato nel metodo che li ha allocati, quindi try (Whatever w=...) {...}risolvilo (e ricevi un avviso quando lo dimentichi). I restanti sono problematici anche con RAII. Chiamare close()"tutto il tempo" significa forse una volta per decine di migliaia di linee, quindi non è poi così male, mentre la memoria viene allocata quasi su ogni linea Java.
maaartinus,

15

Se si desidera la garbage collection automatica, ci sono buoni garbage collector commerciali e di dominio pubblico per C ++. Per le applicazioni in cui la garbage collection è adatta, C ++ è un eccellente linguaggio garbage collection con prestazioni che si confrontano favorevolmente con altre lingue garbage collection. Vedi Il linguaggio di programmazione C ++ (4a edizione) per una discussione sulla garbage collection automatica in C ++. Vedi anche Hans-J. Sito di Boehm per la raccolta dei rifiuti C e C ++ ( archivio ).

Inoltre, C ++ supporta tecniche di programmazione che consentono alla gestione della memoria di essere sicura e implicita senza un garbage collector . Considero la raccolta dei rifiuti un'ultima scelta e un modo imperfetto di gestione della gestione delle risorse. Ciò non significa che non sia mai utile, solo che ci sono approcci migliori in molte situazioni.

Fonte: http://www.stroustrup.com/bs_faq.html#garbage-collection

Per quanto riguarda il motivo per cui non lo ha incorporato, se ricordo bene è stato inventato prima che GC fosse la cosa giusta , e non credo che il linguaggio avrebbe potuto avere GC per diversi motivi (compatibilità IE Backwards con C)

Spero che questo ti aiuti.


"con una prestazione che si confronta favorevolmente con altre lingue raccolte immondizia". Citazione?
Jon Harrop,

1
Il mio collegamento è stato interrotto. Ho scritto questa risposta 5 anni fa.
Rayne,

1
Ok, speravo in una verifica indipendente di queste affermazioni, cioè non da Stroustrup o Boehm. :-)
Jon Harrop,

12

Stroustrup ha fatto alcuni buoni commenti al riguardo alla conferenza Going Native del 2013.

Salta a circa 25m50 in questo video . (Consiglio di guardare l'intero video in realtà, ma questo salta alle cose sulla raccolta dei rifiuti.)

Quando hai un linguaggio davvero eccezionale che rende facile (e sicuro, prevedibile, facile da leggere e da insegnare) gestire oggetti e valori in modo diretto, evitando l'uso (esplicito) del heap, quindi non vuoi nemmeno la raccolta dei rifiuti.

Con il C ++ moderno e le cose che abbiamo in C ++ 11, la garbage collection non è più desiderabile se non in circostanze limitate. In effetti, anche se un buon garbage collector è incorporato in uno dei principali compilatori C ++, penso che non verrà usato molto spesso. Sarà più facile , non più difficile, evitare il GC.

Mostra questo esempio:

void f(int n, int x) {
    Gadget *p = new Gadget{n};
    if(x<100) throw SomeException{};
    if(x<200) return;
    delete p;
}

Questo non è sicuro in C ++. Ma non è sicuro in Java! In C ++, se la funzione torna presto, deletenon verrà mai chiamata. Ma se hai avuto la garbage collection completa, come in Java, hai semplicemente un suggerimento che l'oggetto verrà distrutto "ad un certo punto in futuro" ( Aggiornamento: è anche peggio che questo. Java non lo faprometto di chiamare mai il finalizzatore - forse non verrà mai chiamato). Questo non è abbastanza buono se il gadget contiene un handle di file aperto, o una connessione a un database o dati che sono stati bufferizzati per la scrittura su un database in un momento successivo. Vogliamo che il gadget venga distrutto non appena finito, al fine di liberare queste risorse il prima possibile. Non vuoi che il tuo server di database stia lottando con migliaia di connessioni al database che non sono più necessarie - non sa che il tuo programma ha finito di funzionare.

Quindi qual è la soluzione? Ci sono alcuni approcci. L'approccio ovvio, che utilizzerai per la stragrande maggioranza dei tuoi oggetti è:

void f(int n, int x) {
    Gadget p = {n};  // Just leave it on the stack (where it belongs!)
    if(x<100) throw SomeException{};
    if(x<200) return;
}

Questo richiede meno caratteri per digitare. Non deve newintromettersi. Non richiede di digitare Gadgetdue volte. L'oggetto viene distrutto al termine della funzione. Se questo è quello che vuoi, è molto intuitivo. Gadgets si comportano allo stesso modo di into double. Prevedibile, facile da leggere, facile da insegnare. Tutto è un "valore". A volte è un grande valore, ma i valori sono più facili da insegnare perché non hai questa cosa "azione a distanza" che ottieni con puntatori (o riferimenti).

La maggior parte degli oggetti creati viene utilizzata solo nella funzione che li ha creati e può essere passata come input alle funzioni figlio. Il programmatore non dovrebbe pensare alla "gestione della memoria" quando restituisce oggetti o altrimenti condivide oggetti attraverso parti del software ampiamente separate.

Ambito e durata sono importanti. Il più delle volte, è più facile se la durata è uguale all'ambito. È più facile da capire e più facile da insegnare. Quando vuoi una vita diversa, dovrebbe essere ovvio leggere il codice che stai facendo, usando shared_ptrad esempio. (O restituendo oggetti (di grandi dimensioni) per valore, sfruttando la semantica di spostamento o unique_ptr.

Questo potrebbe sembrare un problema di efficienza. Cosa succede se desidero restituire un gadget da foo()? La semantica di spostamento di C ++ 11 semplifica la restituzione di oggetti di grandi dimensioni. Basta scrivere Gadget foo() { ... }e funzionerà e funzionerà rapidamente. Non devi fare confusione con &&te stesso, restituisci semplicemente le cose in base al valore e la lingua sarà spesso in grado di fare le ottimizzazioni necessarie. (Anche prima di C ++ 03, i compilatori hanno fatto un ottimo lavoro per evitare copie inutili.)

Come Stroustrup ha detto altrove nel video (parafrasando): "Solo uno scienziato informatico dovrebbe insistere per copiare un oggetto e poi distruggere l'originale. (Il pubblico ride). Perché non spostare l'oggetto direttamente nella nuova posizione? Questo è ciò che gli umani (non gli informatici) si aspettano ".

Quando puoi garantire che è necessaria una sola copia di un oggetto, è molto più facile capire la durata dell'oggetto. Puoi scegliere quale criterio di durata desideri e, se lo desideri, è presente la garbage collection. Ma quando capisci i vantaggi degli altri approcci, scoprirai che la garbage collection è in fondo al tuo elenco di preferenze.

Se questo non funziona per voi, è possibile utilizzare unique_ptr, o, in mancanza, shared_ptr. C ++ 11 ben scritto è più breve, più facile da leggere e più facile da insegnare rispetto a molte altre lingue quando si tratta di gestione della memoria.


1
GC dovrebbe essere usato solo per oggetti che non acquisiscono risorse (es. Chiedere ad altre entità di fare le cose per loro conto "fino a nuovo avviso"). Se Gadgetnon si chiede ad altro di fare qualcosa per suo conto, il codice originale sarebbe perfettamente sicuro in Java se l' deleteistruzione senza significato (per Java) fosse rimossa.
supercat

@supercat, gli oggetti con noiosi distruttori sono interessanti. (Non ho definito "noioso", ma sostanzialmente distruttori che non hanno mai bisogno di essere chiamati, tranne per la liberazione della memoria). Potrebbe essere possibile per un singolo compilatore trattare shared_ptr<T>specialmente quando Tè 'noioso'. Potrebbe decidere di non gestire effettivamente un contatore di riferimento per quel tipo e invece di utilizzare GC. Ciò consentirebbe l'utilizzo di GC senza che lo sviluppatore debba notarlo. A shared_ptrpotrebbe essere semplicemente visto come un puntatore GC, per essere adatto T. Ma ci sono delle limitazioni in questo e renderebbe più lenti molti programmi.
Aaron McDaid il

Un buon sistema di tipi dovrebbe avere tipi diversi per gli oggetti heap gestiti da GC e RAII, poiché alcuni schemi di utilizzo funzionano molto bene con uno e molto male con l'altro. In .NET o Java, un'istruzione string1=string2;verrà eseguita molto rapidamente indipendentemente dalla lunghezza della stringa (non è letteralmente nient'altro che un carico di registro e un archivio di registro) e non richiede alcun blocco per garantire che se l'istruzione precedente viene eseguita mentre string2è in fase di scrittura, string1conterrà il vecchio valore o il nuovo valore, senza comportamento indefinito).
supercat

In C ++, l'assegnazione di a shared_ptr<String>richiede molta sincronizzazione dietro le quinte e l'assegnazione di a Stringpuò comportarsi in modo strano se una variabile viene letta e scritta contemporaneamente. I casi in cui si vorrebbe scrivere e leggere Stringcontemporaneamente non sono terribilmente comuni, ma possono insorgere se, ad esempio, un codice desidera rendere disponibili altri report sullo stato in corso. In .NET e Java, cose del genere "funzionano".
supercat

1
@curiousguy non è cambiato nulla, a meno che non si prendano le giuste precauzioni, Java consente comunque di chiamare il finalizzatore non appena il costruttore ha terminato. Ecco un esempio di vita reale: " finalize () ha chiamato oggetti fortemente raggiungibili in Java 8 ". La conclusione è di non usare mai questa funzione, che quasi tutti accettano di essere un errore di design storico del linguaggio. Quando seguiamo quel consiglio, la lingua fornisce il determinismo che amiamo.
Holger,

11

Perché il C ++ moderno non ha bisogno della garbage collection.

La risposta alle domande frequenti di Bjarne Stroustrup su questo argomento dice :

Non mi piace la spazzatura. Non mi piace sporcare. Il mio ideale è quello di eliminare la necessità di un bidone della spazzatura non producendo rifiuti. Questo è ora possibile.


La situazione, per il codice scritto in questi giorni (C ++ 17 e seguendo le Linee guida di base ufficiali ) è la seguente:

  • La maggior parte del codice relativo alla proprietà della memoria si trova nelle librerie (in particolare quelle che forniscono contenitori).
  • La maggior parte dell'uso del codice che coinvolge la proprietà della memoria segue il modello RAII , quindi l'allocazione viene effettuata sulla costruzione e la deallocazione sulla distruzione, che si verifica quando si esce dall'ambito in cui è stato allocato qualcosa.
  • Non allocare o deallocare esplicitamente la memoria direttamente .
  • I puntatori non elaborati non possiedono memoria (se hai seguito le linee guida), quindi non puoi perdere facendoli passare.
  • Se ti stai chiedendo come passare gli indirizzi iniziali delle sequenze di valori in memoria, lo farai in un arco di tempo ; non è necessario alcun puntatore non elaborato.
  • Se hai davvero bisogno di un "puntatore" proprietario, usi i puntatori intelligenti della libreria standard di C ++ : non possono fuoriuscire e sono abbastanza efficienti (sebbene l'ABI possa ostacolarlo ). In alternativa, è possibile passare la proprietà oltre i limiti dell'ambito con "puntatori proprietario" . Questi non sono comuni e devono essere utilizzati esplicitamente; ma una volta adottati, consentono un buon controllo statico contro le perdite.

"Oh sì? Ma che dire ...

... se scrivo semplicemente il codice nel modo in cui scrivevamo C ++ ai vecchi tempi? "

In effetti, si potrebbe semplicemente ignorare tutte le linee guida e scrivere il codice dell'applicazione che perde - e sarà compilare ed eseguire (e perdite), quello di sempre.

Ma non è una situazione "semplicemente non farlo", dove lo sviluppatore dovrebbe essere virtuoso ed esercitare un sacco di autocontrollo; non è semplicemente più semplice scrivere codice non conforme, né è più veloce da scrivere, né è più performante. A poco a poco diventerà anche più difficile scrivere, dato che dovresti affrontare una crescente "discrepanza di impedenza" con ciò che il codice conforme fornisce e si aspetta.

... se io reintrepret_cast? O l'aritmetica del puntatore complesso? O altri simili hack? "

In effetti, se ci pensi, puoi scrivere un codice che rovina tutto nonostante abbia giocato bene con le linee guida. Ma:

  1. Lo faresti raramente (in termini di posizioni nel codice, non necessariamente in termini di frazione del tempo di esecuzione)
  2. Lo faresti solo intenzionalmente, non per caso.
  3. Ciò si distinguerà in una base di codice conforme alle linee guida.
  4. È il tipo di codice in cui si ignorerebbe comunque il GC in un'altra lingua.

... sviluppo della biblioteca? "

Se sei uno sviluppatore di librerie C ++, scrivi un codice non sicuro che coinvolge puntatori non elaborati e ti viene richiesto di codificare con cura e responsabilità - ma questi sono pezzi di codice autonomi scritti da esperti (e, soprattutto, revisionati da esperti).


Quindi, è proprio come ha detto Bjarne: non c'è davvero alcuna motivazione per raccogliere la spazzatura in generale, come tutti voi ma assicuratevi di non produrre spazzatura. GC sta diventando un problema con C ++.

Ciò non significa che GC non sia un problema interessante per determinate applicazioni specifiche, quando si desidera utilizzare strategie personalizzate di allocazione e de-allocazione. Per quelli che desideri allocazione e disallocazione personalizzate, non un GC a livello di lingua.


Bene, (hai bisogno di GC) se stai macinando stringhe. Immagina di avere array di stringhe di grandi dimensioni (pensa a centinaia di megabyte) che stai costruendo frammentariamente, quindi elaborando e ricostruendo in lunghezze diverse, eliminando quelle inutilizzate, combinando altre ecc. I so perché ho dovuto passare a lingue di alto livello per farcela. (Ovviamente potresti anche costruire il tuo GC).
www-0av-Com,

2
@ user1863152: Questo è un caso in cui un allocatore personalizzato sarebbe utile. Non richiede ancora un GC integrato nel linguaggio ...
einpoklum,

a einpoklum: vero. È solo un cavallo per i corsi. Il mio requisito era quello di elaborare dinamicamente cambiando galloni di informazioni sui passeggeri di trasporto. Argomento affascinante .. Dipende davvero dalla filosofia del software.
www-0av-Com,

GC, come hanno scoperto il mondo Java e .NET, ha finalmente un grosso problema: non si ridimensiona. Quando hai miliardi di oggetti vivi in ​​memoria come facciamo in questi giorni con qualsiasi software non banale, dovrai iniziare a scrivere codice per nascondere le cose dal GC. È un onere avere GC in Java e .NET.
Zach vide il

10

L'idea alla base di C ++ era che non avresti pagato alcun impatto sulle prestazioni per le funzionalità che non usi. Quindi aggiungere la garbage collection avrebbe significato avere alcuni programmi eseguiti direttamente sull'hardware come fa C e altri all'interno di una sorta di macchina virtuale di runtime.

Nulla ti impedisce di utilizzare una forma di puntatori intelligenti associati a un meccanismo di garbage collection di terze parti. Mi sembra di ricordare che Microsoft abbia fatto qualcosa del genere con COM e non è andata bene.


2
Non credo che GC richieda una VM. Il compilatore potrebbe aggiungere codice a tutte le operazioni del puntatore per aggiornare uno stato globale, mentre un thread separato viene eseguito in background eliminando gli oggetti secondo necessità.
user83255,

3
Sono d'accordo. Non hai bisogno di una macchina virtuale, ma nel momento in cui inizi ad avere qualcosa che ti gestisce la memoria in quel modo in background, la mia sensazione è che hai lasciato i veri "cavi elettrici" e hai una sorta di situazione di VM.
Uri,


4

Uno dei principi fondamentali alla base del linguaggio C originale è che la memoria è composta da una sequenza di byte e il codice deve solo preoccuparsi di cosa significano quei byte nel momento esatto in cui vengono utilizzati. Il moderno C consente ai compilatori di imporre ulteriori restrizioni, ma C include - e C ++ mantiene - la possibilità di scomporre un puntatore in una sequenza di byte, assemblare qualsiasi sequenza di byte contenente gli stessi valori in un puntatore, quindi utilizzare quel puntatore per accedere all'oggetto precedente.

Mentre quella capacità può essere utile - o addirittura indispensabile - in alcuni tipi di applicazioni, un linguaggio che include tale capacità sarà molto limitato nella sua capacità di supportare qualsiasi tipo di raccolta dei rifiuti utile e affidabile. Se un compilatore non conosce tutto ciò che è stato fatto con i bit che compongono un puntatore, non avrà modo di sapere se potrebbero esistere da qualche parte nell'universo informazioni sufficienti per ricostruire il puntatore. Dal momento che sarebbe possibile che tali informazioni fossero archiviate in modo che il computer non sarebbe in grado di accedere anche se ne fosse a conoscenza (ad es. I byte che compongono il puntatore potrebbero essere stati visualizzati sullo schermo abbastanza a lungo da consentire a qualcuno di scrivere su un foglio di carta), potrebbe essere letteralmente impossibile per un computer sapere se un puntatore potrebbe essere utilizzato in futuro.

Una stranezza interessante di molti framework raccolti dalla spazzatura è che un riferimento a un oggetto non definito dai pattern di bit in esso contenuti, ma dalla relazione tra i bit contenuti nel riferimento a oggetto e altre informazioni conservate altrove. In C e C ++, se il modello di bit memorizzato in un puntatore identifica un oggetto, quel modello di bit identificherà quell'oggetto fino a quando l'oggetto non viene esplicitamente distrutto. In un tipico sistema GC, un oggetto può essere rappresentato da un modello di bit 0x1234ABCD in un momento nel tempo, ma il ciclo GC successivo potrebbe sostituire tutti i riferimenti a 0x1234ABCD con riferimenti a 0x4321BABE, al che l'oggetto sarebbe rappresentato da quest'ultimo modello. Anche se si dovesse visualizzare il modello di bit associato a un riferimento a un oggetto e successivamente rileggerlo dalla tastiera,


Questo è davvero un buon punto, ho recentemente rubato alcuni bit dai miei puntatori perché altrimenti ci sarebbero stupide quantità di errori nella cache.
Passer Entro il

@PasserBy: Mi chiedo quante applicazioni che utilizzano puntatori a 64 bit trarrebbero maggiori benefici dall'utilizzo di puntatori a 32 bit ridimensionati come riferimenti a oggetti, oppure mantenendo quasi tutto in 4GiB di spazio degli indirizzi e utilizzando oggetti speciali per archiviare / recuperare dati da alte velocità di archiviazione oltre? Le macchine dispongono di RAM sufficiente per non importare il consumo di RAM dei puntatori a 64 bit, ad eccezione del fatto che divorano il doppio della cache rispetto ai puntatori a 32 bit.
supercat

3

Tutto il parlare tecnico sta complicando il concetto.

Se metti GC in C ++ per tutta la memoria automaticamente, prendi in considerazione qualcosa come un browser web. Il browser Web deve caricare un documento Web completo ed eseguire script Web. È possibile memorizzare le variabili degli script Web nella struttura del documento. In un GRANDE documento in un browser con molte schede aperte, significa che ogni volta che il GC deve eseguire una raccolta completa, deve anche scansionare tutti gli elementi del documento.

Sulla maggior parte dei computer ciò significa che si verificheranno GUASTI PAGINA. Quindi il motivo principale, per rispondere alla domanda è che si verificheranno GUASTI PAGINA. Lo saprai come quando il tuo PC inizia a fare un sacco di accesso al disco. Questo perché il GC deve toccare molta memoria per dimostrare i puntatori non validi. Quando si dispone di un'applicazione in buona fede che utilizza molta memoria, dover scansionare tutti gli oggetti ogni raccolta è devastante a causa dei GUASTI DELLA PAGINA. Un errore di pagina si verifica quando la memoria virtuale deve essere letta nuovamente nella RAM dal disco.

Quindi la soluzione corretta è quella di dividere un'applicazione nelle parti che richiedono GC e le parti che non lo fanno. Nel caso dell'esempio del browser Web sopra, se l'albero del documento è stato allocato con malloc, ma il javascript è stato eseguito con GC, ogni volta che il GC entra in esso scansiona solo una piccola parte della memoria e tutti gli elementi PAGED OUT della memoria per l'albero del documento non deve essere ricollegato.

Per comprendere ulteriormente questo problema, cercare nella memoria virtuale e come viene implementata nei computer. Tutto dipende dal fatto che 2 GB sono disponibili per il programma quando non c'è davvero tanta RAM. Sui computer moderni con 2 GB di RAM per un sistema da 32 GB, non è un problema del genere, a condizione che sia in esecuzione un solo programma.

Come esempio aggiuntivo, considera una raccolta completa che deve tracciare tutti gli oggetti. Per prima cosa devi scansionare tutti gli oggetti raggiungibili tramite root. In secondo luogo scansionare tutti gli oggetti visibili nel passaggio 1. Quindi scansionare i distruttori in attesa. Quindi vai di nuovo a tutte le pagine e spegni tutti gli oggetti invisibili. Ciò significa che molte pagine potrebbero essere scambiate e rimpiazzate più volte.

Quindi la mia risposta per farla breve è che il numero di GUASTI PAGINA che si verificano a seguito del contatto di tutta la memoria rende irrealizzabile il GC completo per tutti gli oggetti in un programma e quindi il programmatore deve vedere GC come un aiuto per cose come gli script e il database funziona, ma fa cose normali con la gestione manuale della memoria.

E l'altra ragione molto importante ovviamente sono le variabili globali. Affinché il raccoglitore sappia che un puntatore a variabile globale si trova nel GC richiederebbe parole chiave specifiche e quindi il codice C ++ esistente non funzionerebbe.


3

RISPOSTA BREVE: non sappiamo come eseguire la raccolta dei rifiuti in modo efficiente (con un tempo minore e un sovraccarico di spazio) e correttamente sempre (in tutti i casi possibili).

RISPOSTA LUNGA: Proprio come C, C ++ è un linguaggio di sistema; questo significa che viene utilizzato quando si scrive il codice di sistema, ad esempio il sistema operativo. In altre parole, C ++ è progettato, proprio come C, con le migliori prestazioni possibili come obiettivo principale. Lo standard del linguaggio non aggiungerà alcuna caratteristica che potrebbe ostacolare l'obiettivo delle prestazioni.

Questo mette in pausa la domanda: perché la garbage collection ostacola le prestazioni? Il motivo principale è che, quando si tratta di implementazione, noi [informatici] non sappiamo come fare la garbage collection con un sovraccarico minimo, in tutti i casi. Quindi è impossibile per il compilatore C ++ e il sistema di runtime eseguire in modo efficiente la garbage collection in ogni momento. D'altra parte, un programmatore C ++ dovrebbe conoscere il suo design / implementazione ed è la persona migliore per decidere come eseguire al meglio la garbage collection.

Infine, se controllo (hardware, dettagli, ecc.) E prestazioni (tempo, spazio, potenza, ecc.) Non sono i vincoli principali, allora C ++ non è lo strumento di scrittura. Altre lingue potrebbero servire meglio e offrire una gestione del runtime più [nascosta], con il sovraccarico necessario.


3

Quando confrontiamo C ++ con Java, vediamo che C ++ non è stato progettato tenendo presente la Garbage Collection implicita, mentre Java lo era.

Avere cose come puntatori arbitrari in C-Style non è solo male per le implementazioni GC, ma distruggerebbe anche la compatibilità con le versioni precedenti per una grande quantità di C ++ - codice legacy.

Inoltre, C ++ è un linguaggio che deve essere eseguito come eseguibile autonomo anziché avere un ambiente di runtime complesso.

Tutto sommato: Sì, potrebbe essere possibile aggiungere Garbage Collection a C ++, ma per motivi di continuità è meglio non farlo.


1
La liberazione di memoria e l'esecuzione di distruttori sono problemi troppo completamente separati. (Java non ha distruttori, che è un PITA.) GC libera la memoria, non esegue i dottori.
curioso

0

Principalmente per due motivi:

  1. Perché non ne ha bisogno (IMHO)
  2. Perché è praticamente incompatibile con RAII, che è la pietra angolare del C ++

C ++ offre già gestione manuale della memoria, allocazione dello stack, RAII, container, puntatori automatici, puntatori intelligenti ... Dovrebbe essere sufficiente. I garbage collector sono per programmatori pigri che non vogliono passare 5 minuti a pensare a chi dovrebbe possedere quali oggetti o quando dovrebbero essere liberate le risorse. Non è così che facciamo le cose in C ++.


Esistono numerosi (nuovi) algoritmi che sono intrinsecamente difficili da implementare senza garbage collection. Il tempo è passato. L'innovazione deriva anche da nuove intuizioni che si adattano bene alle lingue di alto livello (immondizia). Prova a eseguire il backport di uno di questi su C ++ gratuito GC, noterai i dossi sulla strada. (So ​​che dovrei fare degli esempi, ma ho un po 'di fretta in questo momento. Scusa. Uno a cui riesco a pensare in questo momento ruota attorno a strutture di dati persistenti, in cui il conteggio dei riferimenti non funzionerà.).
BitTickler

0

L'imposizione della garbage collection è davvero un cambiamento di paradigma di livello da basso ad alto.

Se osservi il modo in cui le stringhe vengono gestite in una lingua con Garbage Collection, scoprirai che consentono SOLO funzioni di manipolazione delle stringhe di alto livello e non consentono l'accesso binario alle stringhe. In poche parole, tutte le funzioni di stringa controllano prima i puntatori per vedere dove si trova la stringa, anche se si sta disegnando solo un byte. Quindi, se stai eseguendo un ciclo che elabora ogni byte in una stringa in una lingua con garbage collection, deve calcolare la posizione di base più l'offset per ogni iterazione, perché non può sapere quando la stringa è stata spostata. Quindi devi pensare a cumuli, pile, fili, ecc. Ecc.

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.