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 free
per un dato malloc
. Succede sicuramente.
Tuttavia, esistono strumenti come il valgrind
rilevamento 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 free
non è 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 free
memorizziamo 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.
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.
... 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).
... 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:
Questi collegamenti blu non gestiscono la vita di Joe. Se vogliamo rimuoverlo dalla faccia della terra, chiediamo manualmente di distruggerlo, in questo modo:
Ora che normalmente ci lascerebbe con puntatori penzolanti dappertutto, quindi rimuoviamo i puntatori a Joe.
... 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.