Quali sono le complessità della programmazione non gestita dalla memoria?


24

O in altre parole, quali problemi specifici ha risolto la raccolta automatica dei rifiuti? Non ho mai fatto una programmazione di basso livello, quindi non so quanto possano essere complicate le risorse di liberazione.

Il tipo di bug a cui si rivolge GC sembra (almeno a un osservatore esterno) il tipo di cose che un programmatore che conosce bene il suo linguaggio, le sue biblioteche, i suoi concetti, i suoi modi di dire, ecc., Non farebbe. Ma potrei sbagliarmi: la gestione manuale della memoria è intrinsecamente complicata?


3
Espandi per dirci come la tua domanda non trova risposta nell'articolo di Wikipedia sulla raccolta di garbace e più specificamente nella sezione sui suoi benefici
yannis,

Un altro vantaggio è la sicurezza, ad esempio i sovraccarichi del buffer sono altamente sfruttabili e molte altre vulnerabilità della sicurezza derivano dalla gestione della memoria (errata).
Stuper:

7
@StuperUser: non ha nulla a che fare con l'origine della memoria. È possibile bufferizzare la memoria sovraccarica proveniente da un GC. Il fatto che i linguaggi GC di solito impediscano ciò è ortogonale e che linguaggi che sono meno di trent'anni dietro la tecnologia GC li stai confrontando per offrire anche una protezione da sovraccarico del buffer.
DeadMG

Risposte:


29

Non ho mai fatto una programmazione di basso livello, quindi non so quanto possano essere complicate le risorse di liberazione.

Divertente come la definizione di "basso livello" cambi nel tempo. Quando stavo imparando a programmare per la prima volta, qualsiasi lingua che forniva un modello heap standardizzato che rendesse possibile un semplice modello di allocazione / libera era considerata di alto livello. Nella programmazione di basso livello , dovresti tenere traccia della memoria da solo (non delle allocazioni, ma delle posizioni di memoria stesse!), O scrivere il tuo allocatore di heap se ti senti davvero fantasioso.

Detto questo, non c'è proprio nulla di spaventoso o "complicato" a riguardo. Ricordi quando eri un bambino e tua madre ti ha detto di mettere via i tuoi giocattoli quando hai finito di giocare con loro, che non è la tua cameriera e non ti pulirà la stanza? La gestione della memoria è semplicemente lo stesso principio applicato al codice. (GC è come avere una cameriera che si pulire dopo te, ma lei è molto pigro e un po 'confusi.) Il principio di esso è semplice: Ogni variabile nel codice ha uno e un solo proprietario, ed è la responsabilità di tale proprietario libera la memoria della variabile quando non è più necessaria. ( Il principio della proprietà unica) Ciò richiede una chiamata per allocazione ed esistono diversi schemi che automatizzano la proprietà e la pulizia in un modo o nell'altro, quindi non è nemmeno necessario scrivere quella chiamata nel proprio codice.

La raccolta dei rifiuti dovrebbe risolvere due problemi. Invariabilmente fa un lavoro pessimo in uno di essi e, a seconda dell'implementazione, può o meno fare bene con l'altro. I problemi sono perdite di memoria (aggrappandosi alla memoria dopo aver finito con essa) e riferimenti penzolanti (liberare memoria prima di averla finita). Vediamo entrambi i problemi:

Riferimenti ciondolanti: discutiamo prima di questo perché è davvero serio. Hai due puntatori allo stesso oggetto. Ne liberi uno e non noti l'altro. Quindi in un secondo momento si tenta di leggere (o scrivere o liberare) il secondo. Ne consegue un comportamento indefinito. Se non lo noti, puoi facilmente corrompere la tua memoria. La raccolta dei rifiuti dovrebbe rendere impossibile questo problema assicurando che nulla venga mai liberato fino a quando tutti i riferimenti ad esso non saranno scomparsi. In un linguaggio completamente gestito, questo funziona quasi fino a quando non si ha a che fare con risorse di memoria esterne non gestite. Poi torna al punto 1. E in un linguaggio non gestito, le cose sono ancora più complicate. (Frugando su Mozilla '

Fortunatamente, affrontare questo problema è fondamentalmente un problema risolto. Non è necessario un Garbage Collector, è necessario un gestore della memoria di debug. Uso Delphi, ad esempio, e con una singola libreria esterna e una semplice direttiva del compilatore posso impostare l'allocatore su "Modalità di debug completa". Ciò aggiunge un overhead trascurabile (inferiore al 5%) in cambio dell'abilitazione di alcune funzionalità che tengono traccia della memoria utilizzata. Se libero un oggetto, riempie la sua memoria0x80byte (facilmente riconoscibile nel debugger) e se provo mai a chiamare un metodo virtuale (incluso il distruttore) su un oggetto liberato, nota e interrompe il programma con una casella di errore con tre tracce dello stack: quando l'oggetto è stato creato, quando è stato liberato e dove sono ora, oltre ad alcune altre informazioni utili, solleva un'eccezione. Questo ovviamente non è adatto per le build di rilascio, ma rende banale rintracciare e risolvere problemi di riferimento penzolanti.

Il secondo problema sono le perdite di memoria. Questo è ciò che accade quando si continua a trattenere la memoria allocata quando non è più necessaria. Può accadere in qualsiasi lingua, con o senza garbage collection, e può essere risolto solo scrivendo il codice giusto. La garbage collection aiuta a mitigare una forma specifica di perdita di memoria, il tipo che si verifica quando non si hanno riferimenti validi a un pezzo di memoria che non è stato ancora liberato, il che significa che la memoria rimane allocata fino al termine del programma. Sfortunatamente, l'unico modo per farlo in modo automatizzato è trasformare ogni allocazione in una perdita di memoria!

Probabilmente mi fotteranno i sostenitori della GC se provo a dire qualcosa del genere, quindi permettimi di spiegare. Ricordare che la definizione di una perdita di memoria si aggrappa alla memoria allocata quando non è più necessaria. Oltre a non avere riferimenti a qualcosa, puoi anche perdere la memoria avendone un riferimento non necessario, come tenerlo in un oggetto contenitore quando dovresti averlo liberato. Ho visto alcune perdite di memoria causate da questo, e sono molto difficili da rintracciare se si dispone di un GC o meno, poiché comportano un riferimento perfettamente valido alla memoria e non ci sono "bug" chiari per gli strumenti di debug catturare. Per quanto ne so, non esiste uno strumento automatizzato che consente di rilevare questo tipo di perdita di memoria.

Quindi un garbage collector si occupa solo della varietà senza riferimenti di perdite di memoria, perché è l'unico tipo che può essere gestito in modo automatizzato. Se potesse guardare tutti i tuoi riferimenti a tutto e liberare ogni oggetto non appena ha zero riferimenti che lo puntano, sarebbe perfetto, almeno per quanto riguarda il problema senza riferimenti. Fare questo in modo automatizzato si chiama conteggio dei riferimenti, e può essere fatto in alcune situazioni limitate, ma ha i suoi problemi da affrontare. (Ad esempio, l'oggetto A che contiene un riferimento all'oggetto B, che contiene un riferimento all'oggetto A. In uno schema di conteggio dei riferimenti, nessuno degli oggetti può essere liberato automaticamente, anche quando non ci sono riferimenti esterni ad A o B.) i netturbini usano la tracciainvece: inizia con una serie di oggetti noti, trova tutti gli oggetti a cui fanno riferimento, trova tutti gli oggetti a cui fanno riferimento e così via in modo ricorsivo fino a quando non hai trovato tutto. Tutto ciò che non viene trovato nel processo di tracciamento è immondizia e può essere gettato via. (Ovviamente, per farlo con successo è necessario un linguaggio gestito che ponga alcune restrizioni sul sistema dei tipi per garantire che il garbage collector di tracciamento possa sempre dire la differenza tra un riferimento e un pezzo di memoria casuale che sembra apparire come un puntatore.)

Esistono due problemi con la traccia. Innanzitutto, è lento e mentre sta accadendo il programma deve essere più o meno in pausa per evitare le condizioni di gara. Questo può portare a notevoli singhiozzi di esecuzione quando si suppone che il programma stia interagendo con un utente o a prestazioni impantanate in un'app server. Ciò può essere mitigato da varie tecniche, come la suddivisione della memoria allocata in "generazioni" in base al principio che se un'allocazione non viene raccolta la prima volta che si tenta, è probabile che rimanga in sospeso per un po '. Sia .NET framework che JVM utilizzano i garbage collector generazionali.

Sfortunatamente, questo alimenta il secondo problema: la memoria non viene liberata quando hai finito. A meno che la traccia non venga eseguita immediatamente dopo aver terminato con un oggetto, rimarrà fino alla traccia successiva o anche più a lungo se supera la prima generazione. In effetti, una delle migliori spiegazioni del Garbage Collector .NET che ho visto spiega che, per rendere il processo il più veloce possibile, il GC deve rinviare la raccolta il più a lungo possibile! Quindi il problema delle perdite di memoria viene "risolto" in modo piuttosto bizzarro, perdendo quanta più memoria possibile il più a lungo possibile! Questo è ciò che intendo quando dico che un GC trasforma ogni allocazione in una perdita di memoria. In realtà, non vi è alcuna garanzia che un determinato oggetto verrà mai raccolto.

Perché questo è un problema, quando la memoria viene ancora recuperata quando necessario? Per un paio di ragioni. Innanzitutto, immagina di allocare un oggetto di grandi dimensioni (ad esempio una bitmap) che richiede una notevole quantità di memoria. E subito dopo aver finito, è necessario un altro oggetto di grandi dimensioni che occupi la stessa quantità di memoria (o quasi la stessa). Se il primo oggetto fosse stato liberato, il secondo può riutilizzare la sua memoria. Ma su un sistema di raccolta dati inutili, è possibile che tu stia ancora aspettando che venga eseguita la traccia successiva e quindi sprechi inutilmente memoria per un secondo oggetto di grandi dimensioni. È fondamentalmente una condizione di gara.

In secondo luogo, conservare inutilmente la memoria, specialmente in grandi quantità, può causare problemi in un moderno sistema multitasking. Se occupi troppa memoria fisica, potresti fare in modo che il tuo programma o altri programmi debbano sfogliare (scambiando parte della loro memoria su disco) che rallenta davvero le cose. Per alcuni sistemi, come i server, il paging non solo può rallentare il sistema, ma può causare il crash dell'intero sistema se è sotto carico.

Come il problema dei riferimenti penzolanti, il problema senza riferimenti può essere risolto con un gestore della memoria di debug. Ancora una volta, menzionerò la modalità di debug completo dal gestore della memoria FastMM di Delphi, poiché è quella con cui ho più familiarità. (Sono sicuro che esistono sistemi simili per altre lingue.)

Al termine di un programma in esecuzione in FastMM, puoi facoltativamente riportare l'esistenza di tutte le allocazioni che non sono mai state liberate. La modalità di debug completo fa un ulteriore passo avanti: può salvare un file su disco contenente non solo il tipo di allocazione, ma una traccia dello stack da quando è stata allocata e altre informazioni di debug, per ogni allocazione trapelata. Ciò rende banale il rilevamento delle perdite di memoria senza riferimenti.

Quando lo guardi davvero, la garbage collection può o meno fare bene con la prevenzione dei riferimenti penzolanti e universalmente fa un brutto lavoro nella gestione delle perdite di memoria. La sua unica virtù, infatti, non è la raccolta dei rifiuti in sé, ma un effetto collaterale: fornisce un modo automatizzato per eseguire la compattazione dell'heap. Questo può prevenire un problema arcano (esaurimento della memoria attraverso la frammentazione dell'heap) che può uccidere i programmi che vengono eseguiti continuamente per un lungo periodo di tempo e hanno un elevato grado di sfornamento della memoria e la compattazione dell'heap è praticamente impossibile senza la garbage collection. Tuttavia, ogni buon allocatore di memoria in questi giorni utilizza i bucket per ridurre al minimo la frammentazione, il che significa che la frammentazione diventa davvero un problema solo in circostanze estreme. Per un programma in cui è probabile che la frammentazione dell'heap sia un problema, " s è consigliabile utilizzare un cestino per la spazzatura compatto. Ma IMO in ogni altro caso, l'uso della garbage collection è un'ottimizzazione prematura, e esistono soluzioni migliori ai problemi che "risolve".


5
Adoro questa risposta - continuo a leggerlo ogni tanto. Non riesco a formulare un'osservazione pertinente, quindi tutto quello che posso dire è: grazie.
Vemv,

3
Vorrei sottolineare che sì, i GC tendono a "perdere" memoria (almeno per un po '), ma questo non è un problema perché raccoglierà la memoria quando l'allocatore di memoria non può allocare memoria prima della raccolta. Con un linguaggio non GC, una perdita rimane sempre una perdita, il che significa che puoi effettivamente esaurire la memoria a causa di troppa memoria non raccolta. "la garbage collection è un'ottimizzazione prematura" ... GC non è un'ottimizzazione e non è stata progettata pensando a questo. Altrimenti, buona risposta.
Thomas Eding,

7
@ThomasEding: GC è certamente un'ottimizzazione; ottimizza per il minimo sforzo del programmatore, a scapito delle prestazioni e di vari altri parametri di qualità del programma.
Mason Wheeler

5
Divertente che tu indichi il bug tracker di Mozilla ad un certo punto, perché Mozilla è giunto a una conclusione del tutto diversa. Firefox ha avuto e continua ad avere innumerevoli problemi di sicurezza derivanti da errori di gestione della memoria. Si noti che non si tratta di quanto sia stato facile correggere l'errore una volta rilevato, in genere il danno è già stato fatto quando gli sviluppatori vengono a conoscenza del problema. Mozilla sta finanziando il linguaggio di programmazione Rust proprio per aiutare a prevenire l'introduzione di tali errori.

1
Rust non usa la garbage collection, ma usa il conteggio dei riferimenti esattamente come sta descrivendo Mason, solo con estesi controlli in fase di compilazione piuttosto che dover usare un debugger per rilevare errori in fase di esecuzione ...
Sean Burton

13

Considerando una tecnica di gestione della memoria non garbage collection di un'epoca equivalente come i garbage collector in uso negli attuali sistemi popolari, come RAII di C ++. Dato questo approccio, il costo per non utilizzare la garbage collection automatizzata è minimo e GC presenta molti dei suoi problemi. Come tale, suggerirei che "Non molto" è la risposta al tuo problema.

Ricorda, quando le persone pensano di non GC, pensano malloce free. Ma questo è un errore logico gigantesco: confronteresti la gestione delle risorse non GC dei primi anni '70 con i raccoglitori di rifiuti della fine degli anni '90. Questo è ovviamente un confronto piuttosto ingiusto: i raccoglitori di immondizia che erano in uso quando malloce freesono stati progettati erano troppo lenti per eseguire qualsiasi programma significativo, se ricordo bene. Confrontare qualcosa di un periodo di tempo vagamente equivalente, ad esempio unique_ptr, è molto più significativo.

I netturbini possono gestire i cicli di riferimento più facilmente, sebbene si tratti di esperienze piuttosto rare. Inoltre, i GC possono semplicemente "lanciare" il codice perché il GC si occuperà di tutta la gestione della memoria, il che significa che possono portare a cicli di sviluppo più veloci.

D'altra parte, tendono a incorrere in enormi problemi quando si tratta di memoria proveniente da qualsiasi luogo tranne il proprio pool GC. Inoltre, perdono molto del loro vantaggio quando è coinvolta la concorrenza, poiché si deve comunque considerare la proprietà dell'oggetto.

Modifica: molte delle cose che menzioni non hanno nulla a che fare con GC. Stai confondendo la gestione della memoria e l'orientamento agli oggetti. Vedete, ecco la cosa: se programmate in un sistema completamente non gestito, come C ++, potete avere tutti i limiti di controllo che volete e le classi di container Standard lo offrono. Non c'è nulla di GC nel controllo dei limiti, ad esempio, o nella digitazione forte.

I problemi citati sono risolti dall'orientamento agli oggetti, non da GC. L'origine della memoria dell'array e assicurarsi di non scrivere al di fuori di essa sono concetti ortogonali.

Modifica: vale la pena notare che tecniche più avanzate possono evitare la necessità di qualsiasi forma di allocazione dinamica della memoria. Ad esempio, considera l'uso di questo , che implementa la combinazione Y in C ++ senza allocazione dinamica.


La discussione estesa qui è stata ripulita: se tutti possono portarlo in chat per discutere ulteriormente l'argomento, lo apprezzerei davvero.

@DeadMG, sai cosa dovrebbe fare il combinatore? Dovrebbe COMBINARE. Per definizione, il combinatore è una funzione senza variabili libere.
SK-logic,

2
@ SK-logic: avrei potuto scegliere di implementarlo esclusivamente in base al modello e senza variabili membro. Ma poi non saresti in grado di passare in chiusure, il che limita significativamente la sua utilità. Vuoi venire a chattare?
DeadMG

@DeadMG, una definizione è cristallina. Nessuna variabile libera. Considero qualsiasi linguaggio "abbastanza funzionale" se è possibile definire il Y-combinatore (correttamente, non a modo tuo). Un grande "+" è se è possibile definirlo tramite i combinatori S, K e I. Altrimenti il ​​linguaggio non è abbastanza espressivo.
SK-logic,

4
@ SK-logic: Perché non vieni in chat , come ha chiesto il gentile moderatore? Inoltre, un Y-combinatore è un Y-combinatore, fa il lavoro o no. La versione Haskell di Y-combinator è sostanzialmente la stessa di questa, è solo che lo stato espresso è nascosto da te.
DeadMG

11

La "libertà da doversi preoccupare di liberare risorse" che i linguaggi di garbage collection presumibilmente forniscono è quello di una considerevole misura un'illusione. Continua ad aggiungere elementi in una mappa senza rimuoverne mai e capirai presto di cosa sto parlando.

In effetti, le perdite di memoria sono abbastanza frequenti nei programmi scritti in linguaggi GCed, perché queste lingue tendono a rendere pigri i programmatori e fanno acquisire loro un falso senso di sicurezza che la lingua si prenderà sempre (magicamente) in qualche modo cura di ogni oggetto che essi non voglio più pensarci.

La garbage collection è semplicemente una funzione necessaria per le lingue che hanno un altro obiettivo più nobile: trattare tutto come un puntatore a un oggetto e allo stesso tempo nascondere al programmatore il fatto che è un puntatore, in modo che il programmatore non possa impegnarsi suicidio tentando l'aritmetica del puntatore e simili. Essendo tutto un oggetto, i linguaggi GCed hanno bisogno di allocare oggetti molto più spesso dei linguaggi non GCed, il che significa che se gravano sul deallocatore sul programmatore, sarebbero immensamente poco attraenti.

Inoltre, la garbage collection è utile al fine di fornire al programmatore la possibilità di scrivere codice stretto, manipolando oggetti all'interno di espressioni, in un modo di programmazione funzionale, senza dover scomporre le espressioni in istruzioni separate al fine di provvedere alla deallocazione di ogni singolo oggetto che partecipa all'espressione.

Oltre a tutto ciò, si ricorda che all'inizio della mia risposta ho scritto "è ad un notevole misura un'illusione". Non ho scritto che è un'illusione. Non ho nemmeno scritto che è principalmente un'illusione. La raccolta dei rifiuti è utile per sottrarre al programmatore il compito umile di occuparsi della deallocazione dei suoi oggetti. Quindi, in questo senso è una caratteristica di produttività.


4

Il Garbage Collector non risolve alcun "bug". È una parte necessaria di alcune semantiche di lingue di alto livello. Con un GC è possibile definire livelli più alti di astrazioni, come chiusure lessicali e simili, mentre con una gestione manuale della memoria tali astrazioni saranno fuoriuscite, inutilmente legate ai livelli inferiori della gestione delle risorse.

Un "principio di proprietà unica", menzionato nei commenti, è un buon esempio di un'astrazione così impercettibile. Uno sviluppatore non dovrebbe preoccuparsi del numero di collegamenti a una particolare istanza di struttura di dati elementare, altrimenti qualsiasi parte di codice non sarebbe generica e trasparente senza un numero enorme di limitazioni e requisiti aggiuntivi (non direttamente visibili nel codice stesso) . Un tale codice non può essere composto in un codice di livello superiore, che è una violazione intollerabile del principio di separazione dei livelli di responsabilità (un elemento fondamentale dell'ingegneria del software, purtroppo non rispettato dalla maggior parte degli sviluppatori di basso livello).


1
@Mason Wheeler, anche il C ++ implementa una forma molto limitata di chiusure. Ma non è quasi una chiusura corretta, generalmente utilizzabile.
Logica SK

1
Hai torto. Nessun GC può proteggerti dal fatto che non puoi fare riferimento alle variabili dello stack. Ed è divertente: in C ++ puoi anche usare l'approccio "Copia un puntatore a una variabile allocata in modo dinamico che verrà opportunamente e automaticamente distrutta".
DeadMG

1
@DeadMG, non vedi che il tuo codice sta perdendo entità di basso livello attraverso qualsiasi altro livello che costruisci in cima?
Logica SK

1
@ SK-Logic: OK, abbiamo un problema di terminologia. Qual è la tua definizione di "vera chiusura" e cosa possono fare che le chiusure di Delphi non possono fare? (E includere qualsiasi cosa sulla gestione della memoria nella tua definizione sta spostando i messaggi obiettivo. Parliamo di comportamento, non di dettagli di implementazione.)
Mason Wheeler,

1
@ SK-Logic: ... e hai un esempio di qualcosa che può essere fatto con chiusure lambda semplici non tipizzate che le chiusure di Delphi non possono realizzare?
Mason Wheeler,

2

In realtà, gestire la propria memoria è solo un'altra potenziale fonte di bug.

Se dimentichi una chiamata a free(o qualunque sia l'equivalente nella lingua che stai usando), il tuo programma può superare tutti i suoi test, ma perde memoria. E in un programma moderatamente complesso, è abbastanza facile trascurare una chiamata a free.


3
Perso freenon è la cosa peggiore. All'inizio freeè molto più devastante.
citato

2
E il doppio free!
quant_dev,

Hehe! Vorrei andare d'accordo con entrambi i due commenti sopra. Non ho mai commesso una di queste trasgressioni da solo (per quanto ne so), ma posso vedere quanto possano essere terribili gli effetti. La risposta di quant_dev dice tutto: gli errori nell'allocazione e nella disallocazione della memoria sono notoriamente difficili da trovare e correggere.
Dawood dice di ripristinare Monica l'

1
Questo è un errore. Stai confrontando "inizio 1970" con "fine 1990". I GC esistenti al momento in cui era malloced freeera la strada non GC da fare erano enormemente troppo lenti per essere utili a qualsiasi cosa. Devi confrontarlo con un moderno approccio non GC, come RAII.
DeadMG

2
@DeadMG RAII non è la gestione manuale della memoria
quant_dev l'

2

La risorsa manuale non è solo noiosa, ma anche difficile da eseguire il debug. In altre parole, non solo è noioso farlo nel modo giusto, ma anche quando lo sbagli, non è ovvio dove si trovi il problema. Questo perché, diversamente dalla divisione per zero, gli effetti dell'errore si manifestano lontano dalla fonte dell'errore e il collegamento dei punti richiede tempo, attenzione ed esperienza.


1

Penso che la garbage collection abbia molto credito per i miglioramenti del linguaggio che non hanno nulla a che fare con GC, oltre a far parte di una grande ondata di progressi.

L'unico solido vantaggio di GC che conosco è che puoi liberare un oggetto nel tuo programma e sapere che andrà via quando tutti avranno finito. Puoi passarlo al metodo di un'altra classe e non preoccuparti. Non ti interessa a quali altri metodi viene passato o a quali altre classi lo fanno riferimento. (Le perdite di memoria sono di responsabilità della classe che fa riferimento a un oggetto, non alla classe che lo ha creato.)

Senza GC è necessario tenere traccia dell'intero ciclo di vita della memoria allocata. Ogni volta che passi un indirizzo su o giù dalla subroutine che lo ha creato, hai un riferimento fuori controllo a quella memoria. Ai vecchi tempi, anche con un solo thread, la ricorsione e un sistema operativo originale (Windows NT) mi rendevano impossibile controllare l'accesso alla memoria allocata. Ho dovuto attrezzare il metodo gratuito nel mio sistema di allocazione per mantenere i blocchi di memoria in giro per un po 'fino a quando tutti i riferimenti non sono stati cancellati. Il tempo di attesa era pura supposizione, ma ha funzionato.

Quindi questo è l'unico vantaggio GC che conosco, ma non potrei vivere senza di esso. Non credo che nessun tipo di OOP volerà senza di essa.


1
Appena al di sopra della mia testa, Delphi e C ++ hanno avuto entrambi un discreto successo come linguaggi OOP senza GC. Tutto ciò che serve per prevenire "riferimenti fuori controllo" è un po 'di disciplina. Se capisci il Principio della proprietà singola (vedi la mia risposta), i problemi di cui stai parlando qui diventano non-problemi totali.
Mason Wheeler,

@MasonWheeler: quando è il momento di liberare l'oggetto proprietario, deve conoscere tutti i luoghi a cui fanno riferimento i suoi oggetti di proprietà. Mantenere queste informazioni e usarle per rimuovere i riferimenti mi sembra un sacco di lavoro. Ho spesso scoperto che i riferimenti non potevano essere ancora chiariti. Ho dovuto contrassegnare il proprietario come cancellato, quindi riportarlo in vita periodicamente per vedere se poteva tranquillamente liberarsi. Non ho mai usato Delphi, ma per un piccolo sacrificio nell'efficienza dell'esecuzione C # / Java mi ha dato un grande impulso nei tempi di sviluppo su C ++. (Non tutto a causa di GC, ma ha aiutato.)
RalphChapin,

1

Perdite fisiche

Il tipo di bug affrontato da GC sembra (almeno a un osservatore esterno) il tipo di cose che un programmatore che conosce bene il suo linguaggio, le sue biblioteche, i suoi concetti, i suoi modi di dire, ecc., Non farebbe. Ma potrei sbagliarmi: la gestione manuale della memoria è intrinsecamente complicata?

Provenendo dall'estremità C che rende la gestione della memoria il più manuale e pronunciata possibile in modo che stiamo confrontando gli estremi (il C ++ automatizza principalmente la gestione della memoria senza GC), direi "non proprio" nel senso di confrontare con GC quando arriva a perdite . Un principiante e talvolta anche un professionista può dimenticare di scrivere freeper un dato malloc. Succede sicuramente.

Tuttavia, esistono strumenti come il valgrindrilevamento delle perdite che individuano immediatamente, quando si esegue il codice, quando / dove si verificano tali errori fino alla riga esatta del codice. Quando è integrato nell'IC, diventa quasi impossibile unire tali errori e facile come correggerli. Quindi non è mai un grosso problema in qualsiasi team / processo con standard ragionevoli.

Certo, potrebbero esserci alcuni casi esotici di esecuzione che volano sotto il radar dei test in cui freenon è stato possibile chiamare, forse incontrando un oscuro errore di input esterno come un file corrotto, nel qual caso forse il sistema perde 32 byte o qualcosa del genere. Penso che ciò possa sicuramente accadere anche con standard di collaudo e strumenti di rilevamento delle perdite piuttosto buoni, ma non sarebbe altrettanto critico perdere un po 'di memoria su qualcosa che non accade quasi mai. Vedremo un problema molto più grande in cui possiamo perdere enormi risorse anche nei percorsi di esecuzione comuni di seguito in un modo che GC non può prevenire.

È anche difficile senza qualcosa che assomigli a una pseudo-forma di GC (conteggio dei riferimenti, ad esempio) quando la durata di un oggetto deve essere estesa per una qualche forma di elaborazione differita / asincrona, forse da un altro thread.

Puntatori ciondolanti

Il vero problema con più forme manuali di gestione della memoria non è una perdita per me. Quante applicazioni native scritte in C o C ++ sappiamo che sono davvero trapelate? Il kernel Linux perde? MySQL? CryEngine 3? Workstation e sintetizzatori audio digitali? Java VM perde (è implementato nel codice nativo)? Photoshop?

Semmai, quando ci guardiamo intorno, penso che le applicazioni più difficili tendano ad essere quelle scritte usando schemi GC. Ma prima che venga considerato uno schianto nella garbage collection, il codice nativo presenta un problema significativo che non è affatto correlato alle perdite di memoria.

Il problema per me era sempre la sicurezza. Anche quando freememorizziamo tramite un puntatore, se ci sono altri puntatori alla risorsa, diventeranno puntatori penzolanti (invalidati).

Quando proviamo ad accedere alle punte di quei puntatori penzolanti, finiamo per imbatterci in un comportamento indefinito, anche se quasi sempre una violazione segfault / accesso che porta a un arresto immediato e immediato.

Tutte quelle applicazioni native che ho elencato sopra hanno potenzialmente un oscuro limite o due che possono portare a un crash principalmente a causa di questo problema, e ci sono sicuramente una buona parte di applicazioni scadenti scritte in codice nativo che sono molto pesanti e spesso in gran parte a causa di questo problema.

... ed è perché la gestione delle risorse è difficile, indipendentemente dal fatto che tu usi GC o meno. La differenza pratica è spesso la perdita (GC) o il crash (senza GC) di fronte a un errore che porta alla cattiva gestione delle risorse.

Gestione delle risorse: Garbage Collection

La gestione complessa delle risorse è un processo manuale difficile, qualunque cosa accada. GC non può automatizzare nulla qui.

Facciamo un esempio in cui abbiamo questo oggetto, "Joe". Joe fa riferimento a diverse organizzazioni di cui è membro. Ogni mese circa estrapolano una quota associativa dalla sua carta di credito.

inserisci qui la descrizione dell'immagine

Abbiamo anche un riferimento a Joe per controllare la sua vita. Diciamo che come programmatori non abbiamo più bisogno di Joe. Sta iniziando a infastidirci e non abbiamo più bisogno di queste organizzazioni a cui appartiene per perdere tempo a occuparsi di lui. Quindi tentiamo di cancellarlo dalla faccia della terra rimuovendo il suo riferimento all'ancora di salvezza.

inserisci qui la descrizione dell'immagine

... ma aspetta, stiamo usando la garbage collection. Ogni forte riferimento a Joe lo terrà in giro. Quindi rimuoviamo anche i riferimenti a lui dalle organizzazioni a cui appartiene (annullando la sua iscrizione).

inserisci qui la descrizione dell'immagine

... ad eccezione di whoops, abbiamo dimenticato di annullare la sua iscrizione alla rivista! Ora Joe rimane nella memoria, ci infastidisce e usa risorse, e anche la rivista finisce per continuare a elaborare l'iscrizione a Joe ogni mese.

Questo è l'errore principale che può causare la perdita di molti programmi complessi scritti utilizzando schemi di garbage collection e iniziare a utilizzare sempre più memoria il più a lungo possibile, e probabilmente sempre più elaborazione (l'abbonamento periodico alla rivista). Si sono dimenticati di rimuovere uno o più di quei riferimenti, rendendo impossibile per il garbage collector fare la sua magia fino a quando l'intero programma non viene chiuso.

Tuttavia, il programma non si arresta in modo anomalo. È perfettamente sicuro. Continuerà solo ad accumulare memoria e Joe continuerà a indugiare in giro. Per molte applicazioni, questo tipo di comportamento che perde nel momento in cui gettiamo sempre più memoria / elaborazione al problema potrebbe essere di gran lunga preferibile a un arresto anomalo, soprattutto vista la quantità di memoria e potenza di elaborazione che le nostre macchine hanno oggi.

Gestione delle risorse: manuale

Consideriamo ora l'alternativa in cui utilizziamo i puntatori a Joe e la gestione manuale della memoria, in questo modo:

inserisci qui la descrizione dell'immagine

Questi collegamenti blu non gestiscono la vita di Joe. Se vogliamo rimuoverlo dalla faccia della terra, chiediamo manualmente di distruggerlo, in questo modo:

inserisci qui la descrizione dell'immagine

Ora che normalmente ci lascerebbe con puntatori penzolanti dappertutto, quindi rimuoviamo i puntatori a Joe.

inserisci qui la descrizione dell'immagine

... spiacenti, abbiamo commesso di nuovo lo stesso identico errore e ci siamo dimenticati di annullare l'iscrizione alla rivista Joe!

Tranne ora che abbiamo un puntatore penzolante. Quando l'abbonamento alla rivista cerca di elaborare il canone mensile di Joe, il mondo intero esploderà, in genere si ottiene immediatamente il duro incidente.

Lo stesso errore di base nella gestione errata delle risorse in cui lo sviluppatore ha dimenticato di rimuovere manualmente tutti i puntatori / riferimenti a una risorsa può portare a molti arresti anomali nelle applicazioni native. Non accumulano memoria più a lungo in genere perché in questo caso spesso si bloccano in modo definitivo.

Mondo reale

Ora l'esempio sopra sta usando un diagramma ridicolmente semplice. Un'applicazione del mondo reale potrebbe richiedere migliaia di immagini cucite insieme per coprire un grafico completo, con centinaia di diversi tipi di risorse memorizzate in un grafico di scena, risorse GPU associate ad alcuni di essi, acceleratori legati ad altri, osservatori distribuiti su centinaia di plugin guardare un certo numero di tipi di entità nella scena per i cambiamenti, osservatori osservatori osservatori, audio sincronizzati con animazioni, ecc. Quindi potrebbe sembrare facile evitare l'errore che ho descritto sopra, ma generalmente non è affatto vicino a questo semplice in un mondo reale base di codice di produzione per un'applicazione complessa che copre milioni di righe di codice.

La possibilità che qualcuno, un giorno, gestisca male le risorse da qualche parte in quella base di codice tende ad essere piuttosto elevata e che la probabilità è la stessa con o senza GC. La differenza principale è ciò che accadrà a seguito di questo errore, che influisce anche potenzialmente sulla velocità con cui questo errore verrà individuato e corretto.

Crash vs. Leak

Ora quale è peggio? Un incidente immediato o una silenziosa perdita di memoria in cui Joe si sofferma misteriosamente?

La maggior parte potrebbe rispondere a quest'ultimo, ma supponiamo che questo software sia progettato per funzionare per ore e ore, eventualmente giorni, e ognuno di questi Joe e Jane che aggiungiamo aumenta l'utilizzo di memoria del software di un gigabyte. Non è un software mission-critical (gli arresti anomali in realtà non uccidono gli utenti), ma critico per le prestazioni.

In questo caso, un arresto anomalo che si manifesta immediatamente durante il debug, sottolineando l'errore che hai commesso, potrebbe in realtà essere preferibile a un software che perde anche che potrebbe volare sotto il radar della tua procedura di test.

Il rovescio della medaglia, se si tratta di un software mission-critical in cui le prestazioni non sono l'obiettivo, semplicemente non andare in crash con ogni mezzo possibile, allora le perdite potrebbero essere preferibili.

Riferimenti deboli

Esiste un tipo di ibrido di queste idee disponibile negli schemi GC noto come riferimenti deboli. Con riferimenti deboli, possiamo avere tutte queste organizzazioni con riferimenti deboli a Joe, ma non impedirgli di essere rimosso quando il riferimento forte (proprietario / linea di vita di Joe) scompare. Tuttavia, otteniamo il vantaggio di essere in grado di rilevare quando Joe non è più in giro attraverso questi riferimenti deboli, permettendoci di ottenere una sorta di errore facilmente riproducibile.

Sfortunatamente i riferimenti deboli non vengono usati quasi quanto dovrebbero probabilmente essere usati, quindi spesso molte applicazioni GC complesse potrebbero essere suscettibili a perdite anche se sono potenzialmente molto meno crash di un'applicazione C complessa, ad es.

In ogni caso, se GC ti semplifichi o meno la vita dipende da quanto sia importante per il tuo software evitare perdite e se si tratta di gestire complesse risorse di questo tipo.

Nel mio caso, lavoro in un campo critico per le prestazioni in cui le risorse coprono centinaia di megabyte in gigabyte e non rilasciare quella memoria quando gli utenti richiedono di scaricare a causa di un errore come quello sopra può effettivamente essere meno preferibile a un arresto anomalo. Gli arresti anomali sono facili da individuare e riprodurre, rendendoli spesso il tipo di errore preferito dal programmatore, anche se è il meno preferito dall'utente, e molti di questi arresti verranno visualizzati con una procedura di test sana prima ancora che raggiungano l'utente.

Ad ogni modo, queste sono le differenze tra GC e gestione manuale della memoria. Per rispondere alla tua domanda immediata, direi che la gestione manuale della memoria è difficile, ma ha ben poco a che fare con le perdite, e sia GC che le forme manuali di gestione della memoria sono ancora molto difficili quando la gestione delle risorse non è banale. Il GC ha probabilmente un comportamento più complicato qui in cui il programma sembra funzionare bene ma sta consumando sempre più risorse. Il modulo manuale è meno complicato, ma andrà in crash e brucerà alla grande con errori come quello mostrato sopra.


-1

Ecco un elenco di problemi che devono affrontare i programmatori C ++ quando hanno a che fare con la memoria:

  1. Il problema di scoping si verifica nella memoria allocata nello stack: la sua durata non si estende al di fuori della funzione in cui è stata allocata. Esistono tre soluzioni principali a questo problema: memoria heap e spostamento del punto di allocazione verso l'alto nello stack di chiamate o allocazione dall'interno degli oggetti .
  2. La dimensione del problema è nello stack allocata e allocata dall'interno dell'oggetto e in parte nella memoria allocata dell'heap: la dimensione del blocco di memoria non può cambiare in fase di esecuzione. Le soluzioni sono array di memoria heap, puntatori e librerie e contenitori.
  3. Il problema dell'ordine di definizione è quando si allocano da oggetti interni: le classi all'interno del programma devono essere nell'ordine corretto. Le soluzioni limitano le dipendenze a un albero e riordinano le classi e non utilizzano dichiarazioni forward, puntatori e memoria heap e utilizzano dichiarazioni forward.
  4. Il problema interno-esterno è nella memoria allocata oggetto. L'accesso alla memoria all'interno degli oggetti è diviso in due parti, parte della memoria è all'interno di un oggetto e altra è all'esterno di esso, ei programmatori devono scegliere correttamente di utilizzare la composizione o i riferimenti in base a questa decisione. Le soluzioni stanno prendendo la decisione correttamente o puntatori e memoria heap.
  5. Il problema degli oggetti ricorsivi è nella memoria allocata agli oggetti. La dimensione degli oggetti diventa infinita se lo stesso oggetto viene inserito al suo interno e le soluzioni sono riferimenti, memoria heap e puntatori.
  6. Il problema di tracciamento della proprietà è nella memoria allocata dell'heap, il puntatore contenente l'indirizzo della memoria allocata dell'heap deve essere passato dal punto di allocazione al punto di deallocazione. Le soluzioni sono memoria allocata in stack, memoria allocata da oggetti, auto_ptr, shared_ptr, unique_ptr, contenitori stdlib.
  7. Il problema di duplicazione della proprietà è nella memoria allocata dell'heap: la deallocazione può essere eseguita una sola volta. Le soluzioni sono memoria allocata in stack, memoria allocata per oggetto, auto_ptr, shared_ptr, unique_ptr, contenitori stdlib.
  8. Il problema con i puntatori null è nella memoria allocata dell'heap: i puntatori possono essere NULL e causare il crash di molte operazioni in fase di esecuzione. Le soluzioni sono la memoria dello stack, la memoria allocata per oggetto e un'attenta analisi delle aree e dei riferimenti dell'heap.
  9. Il problema di perdita di memoria è nella memoria allocata dell'heap: Dimenticare di chiamare l'eliminazione per ogni blocco di memoria allocato. Le soluzioni sono strumenti come valgrind.
  10. Il problema di overflow dello stack è per le chiamate di funzione ricorsive che utilizzano la memoria dello stack. Normalmente la dimensione dello stack è completamente determinata al momento della compilazione, ad eccezione del caso degli algoritmi ricorsivi. Anche la definizione errata della dimensione dello stack del sistema operativo causa spesso questo problema poiché non è possibile misurare la dimensione richiesta dello spazio dello stack.

Come puoi vedere, la memoria heap sta risolvendo molti problemi esistenti, ma causa ulteriore complessità. GC è progettato per gestire parte di quella complessità. (scusate se alcuni nomi di problemi non sono i nomi corretti per questi problemi - a volte è difficile capire il nome corretto)


1
-1: non una risposta alla domanda.
Sjoerd,
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.