Quando è una buona idea forzare la raccolta dei rifiuti?


135

Quindi stavo leggendo una domanda su come forzare l'esecuzione del Garbage Collector C # in cui quasi ogni singola risposta è la stessa: puoi farlo, ma non dovresti - tranne alcuni casi molto rari . Sfortunatamente, nessuno lì elabora cosa siano questi casi.

Puoi dirmi in quale tipo di scenario è effettivamente una buona o ragionevole idea forzare la raccolta dei rifiuti?

Non sto chiedendo casi specifici per C #, ma piuttosto tutti i linguaggi di programmazione che hanno un garbage collector. So che non puoi forzare GC su tutte le lingue, come Java, ma supponiamo che tu possa farlo.


17
"ma piuttosto, tutti i linguaggi di programmazione che hanno un garbage collector" Lingue diverse (o, più propriamente, implementazioni diverse ) usano metodi diversi per la garbage collection, quindi è improbabile trovare una regola adatta a tutti.
Colonnello Trentadue

4
@Doval Se sei soggetto a vincoli in tempo reale e il GC non fornisce garanzie di abbinamento, ti trovi tra una roccia e un luogo difficile. Potrebbe ridurre le pause indesiderate rispetto a non fare nulla, ma da quello che ho sentito è "più facile" evitare l'allocazione nel normale corso dell'operazione.

3
Avevo l'impressione che se ti aspettavi di avere scadenze in tempo reale, non utilizzeresti mai un linguaggio GC.
GregRos,

4
Non riesco a vedere come puoi rispondere a questa domanda in un modo non specifico della VM. Rilevante per i processi a 32 bit, non rilevante per i processi a 64 bit. .NET JVM e per quello di fascia alta
rwong

3
@DavidConrad puoi forzarlo in C #. Da qui la domanda.
Omega

Risposte:


127

Non puoi davvero fare affermazioni generali sul modo appropriato di utilizzare tutte le implementazioni di GC. Variano selvaggiamente. Quindi parlerò con quello a cui ti riferivi originariamente.

È necessario conoscere il comportamento del GC piuttosto intimamente per farlo con qualsiasi logica o ragione.

L'unico consiglio sulla raccolta che posso dare è: non farlo mai.

Se conosci veramente i dettagli intricati del GC, non avrai bisogno dei miei consigli, quindi non importa. Se non lo sai già con la certezza del 100%, sarà di aiuto, e dovrai cercare online e trovare una risposta come questa: non dovresti chiamare GC.Collect , o in alternativa: dovresti andare a conoscere i dettagli di come funziona il GC dentro e fuori, e solo allora conoscerai la risposta .

C'è un posto sicuro in cui ha senso usare GC.Collect :

GC.Collect è un'API disponibile che puoi utilizzare per la profilazione dei tempi delle cose. Potresti profilare un algoritmo, raccogliere e profilare un altro algoritmo subito dopo sapendo che GC del primo algo non si stava verificando durante il secondo alterando i risultati.

Questo tipo di profilazione è la sola volta che suggerirei di raccogliere manualmente a chiunque.


Esempio contrastato comunque

Un possibile caso d'uso è se carichi cose molto grandi, finiranno nell'Heap degli oggetti grandi che andrà direttamente alla Gen 2, anche se ancora Gen 2 è per oggetti di lunga durata perché raccoglie meno frequentemente. Se sai che stai caricando oggetti di breve durata in Gen 2 per qualsiasi motivo, potresti eliminarli più rapidamente per mantenere la tua Gen 2 più piccola e le sue raccolte più veloci.

Questo è il miglior esempio che potrei trovare, e non è buono - la pressione LOH che stai costruendo qui causerebbe raccolte più frequenti, e le raccolte sono così frequenti come è - è probabile che eliminerebbe il LOH proprio come veloce mentre lo facevi esplodere con oggetti temporanei. Ho semplicemente non mi fido di me presumere una frequenza di raccolta migliore rispetto al GC in sé - messo a punto da persone lontane gran lunga più intelligenti di me


Quindi parliamo di alcune delle semantiche e dei meccanismi nel GC GC ... o ...

Tutto quello che penso di sapere sul GC GC

Per favore, chiunque trovi errori qui - correggimi. Gran parte del GC è ben noto per essere magia nera e mentre cercavo di tralasciare dettagli di cui non ero sicuro, probabilmente ho ancora sbagliato qualcosa.

Di seguito mancano di proposito numerosi dettagli di cui non sono sicuro, oltre a un corpus di informazioni molto più ampio di cui sono semplicemente ignaro. Utilizzare queste informazioni a proprio rischio.


Concetti GC

.NET GC si verifica in momenti incoerenti, motivo per cui si chiama "non deterministico", ciò significa che non si può fare affidamento sul fatto che si verifichi in momenti specifici. È anche un garbage collector generazionale, il che significa che suddivide i tuoi oggetti in quanti passaggi GC hanno attraversato.

Gli oggetti nell'heap di Gen 0 hanno attraversato 0 raccolte, queste sono state appena realizzate, quindi di recente non si è verificata alcuna raccolta dalla loro istanza. Gli oggetti nel tuo heap di Gen 1 hanno vissuto un passaggio di raccolta e allo stesso modo gli oggetti nel tuo heap di Gen 2 hanno vissuto 2 passaggi di raccolta.

Ora vale la pena notare il motivo per cui qualifica queste specifiche generazioni e partizioni di conseguenza. .NET GC riconosce solo queste tre generazioni, perché i passaggi di raccolta che superano questi tre cumuli sono leggermente diversi. Alcuni oggetti potrebbero sopravvivere a passaggi di raccolta migliaia di volte. Il GC lascia semplicemente questi dall'altro lato della partizione heap di Gen 2, non ha senso partizionarli ulteriormente perché sono in realtà Gen 44; il passaggio di raccolta su di essi è uguale a tutto nell'heap di seconda generazione.

Ci sono scopi semantici per queste generazioni specifiche, così come meccanismi implementati che le onorano, e arriverò a quelli in un momento.


Cosa c'è in una collezione

Il concetto di base di un passaggio di raccolta GC è che controlla ogni oggetto in uno spazio heap per vedere se ci sono ancora riferimenti attivi (radici GC) a questi oggetti. Se viene trovata una radice GC per un oggetto, significa che attualmente l'esecuzione del codice può ancora raggiungere e utilizzare quell'oggetto, quindi non può essere eliminato. Tuttavia, se una radice GC non viene trovata per un oggetto, significa che il processo in esecuzione non ha più bisogno dell'oggetto, quindi può rimuoverlo per liberare memoria per i nuovi oggetti.

Ora dopo aver finito di ripulire un gruppo di oggetti e di lasciarne alcuni da soli, ci sarà uno sfortunato effetto collaterale: spazi vuoti tra gli oggetti vivi dove sono stati rimossi quelli morti. Questa frammentazione della memoria, se lasciata sola, sprecherebbe semplicemente memoria, quindi le raccolte in genere fanno ciò che viene chiamato "compattazione" dove prendono tutti gli oggetti vivi rimasti e li stringono insieme nell'heap in modo che la memoria libera sia contigua su un lato dell'heap per Gen 0.

Ora, data l'idea di 3 cumuli di memoria, tutti partizionati dal numero di passaggi di raccolta che hanno vissuto, parliamo del perché esistono queste partizioni.


Collezione Gen 0

Essendo la gen 0 gli oggetti più recenti in assoluto, tende ad essere molto piccola, quindi puoi tranquillamente raccoglierla molto frequentemente . La frequenza assicura che l'heap rimanga piccolo e le raccolte siano molto veloci perché si stanno accumulando su un heap così piccolo. Questo si basa più o meno su un'euristica che afferma: una grande maggioranza di oggetti temporanei che crei sono molto temporanei, così temporanei che non saranno più utilizzati o referenziati quasi immediatamente dopo l'uso, e quindi possono essere raccolti.


Collezione Gen 1

Essendo la 1a generazione oggetti che non rientravano in questa categoria di oggetti molto temporanea, potrebbero avere una vita piuttosto breve, poiché ancora - una vasta porzione degli oggetti creati non viene utilizzata a lungo. Pertanto anche la Gen 1 raccoglie piuttosto frequentemente, mantenendo di nuovo l'heap piccolo e le raccolte sono veloci. Tuttavia, il presupposto è minore dei suoi oggetti sono temporanei rispetto alla Gen 0, quindi raccoglie meno frequentemente della Gen 0

Dirò che sinceramente non conosco i meccanismi tecnici che differiscono tra i passi di raccolta della Gen 0 e quelli della Gen 1, se ce ne sono altri diversi dalla frequenza che raccolgono.


Collezione Gen 2

Gen 2 ora deve essere la madre di tutti i cumuli giusto? Bene, sì, è più o meno giusto. È dove vivono tutti i tuoi oggetti permanenti - l'oggetto in cui Main()vivi, per esempio, e tutto ciò che fa Main()riferimento perché saranno radicati fino al tuo Main()ritorno alla fine del tuo processo.

Dato che Gen 2 è un secchio praticamente per tutto ciò che le altre generazioni non sono riuscite a collezionare, i suoi oggetti sono in gran parte permanenti o almeno longevi. Quindi riconoscere molto poco di ciò che è nella Gen 2 sarà effettivamente qualcosa che può essere raccolto, non ha bisogno di essere raccolto frequentemente. Ciò consente alla raccolta di essere anche più lenta, poiché viene eseguita molto meno frequentemente. Quindi questo è fondamentalmente il punto in cui hanno affrontato tutti i comportamenti extra per scenari strani, perché hanno il tempo di eseguirli.


Mucchio di oggetti di grandi dimensioni

Un esempio dei comportamenti extra di Gen 2 è che esegue anche la raccolta sull'heap di oggetti di grandi dimensioni. Fino ad ora ho parlato interamente di Small Object Heap, ma il runtime .NET alloca cose di determinate dimensioni a un heap separato a causa di ciò che ho definito compattazione sopra. La compattazione richiede lo spostamento di oggetti quando le raccolte finiscono nell'heap di piccoli oggetti. Se c'è un oggetto 10mb vivente nella prima generazione, ci vorrà molto più tempo per completare la compattazione dopo la raccolta, rallentando così la raccolta della prima generazione. In modo che l'oggetto 10mb sia allocato all'heap di oggetti di grandi dimensioni e raccolto durante la Gen 2 che viene eseguito di rado.


finalizzazione

Un altro esempio sono gli oggetti con finalizzatori. È stato inserito un finalizzatore su un oggetto che fa riferimento a risorse oltre l'ambito di .NETs GC (risorse non gestite). Il finalizzatore è l'unico modo in cui il GC può richiedere la raccolta di una risorsa non gestita: implementate il finalizzatore per eseguire la raccolta / rimozione / rilascio manuale della risorsa non gestita per garantire che non fuoriesca dal processo. Quando il GC esegue l'esecuzione del finalizzatore degli oggetti, l'implementazione eliminerà la risorsa non gestita, rendendo il GC in grado di rimuovere l'oggetto senza rischiare una perdita di risorse.

Il meccanismo con cui i finalizzatori eseguono questa operazione è il riferimento diretto in una coda di finalizzazione. Quando il runtime alloca un oggetto con un finalizzatore, aggiunge un puntatore a tale oggetto nella coda di finalizzazione e blocca l'oggetto in posizione (chiamato pinning) in modo che la compattazione non lo sposti, interrompendo il riferimento alla coda di finalizzazione. Man mano che si verificano i passaggi di raccolta, alla fine il tuo oggetto non avrà più una radice GC, ma la finalizzazione deve essere eseguita prima di poter essere raccolta. Quindi, quando l'oggetto è morto, la raccolta sposta il suo riferimento dalla coda di finalizzazione e inserisce un riferimento su quella che è nota come coda "FReachable". Quindi la raccolta continua. In un altro momento "non deterministico" nel futuro, un thread separato noto come thread Finalizer passerà attraverso la coda FReachable, eseguendo i finalizzatori per ciascuno degli oggetti a cui viene fatto riferimento. Al termine, la coda FReachable è vuota e si è capovolta un po 'nell'intestazione di ogni oggetto che dice che non necessita di finalizzazione (Questo bit può anche essere capovolto manualmente conGC.SuppressFinalizeche è comune nei Dispose()metodi), sospetto anche che abbia sbloccato gli oggetti, ma non citarmi su questo. La prossima collezione che si presenta su qualsiasi heap in cui si trova questo oggetto, finalmente lo raccoglierà. Le raccolte Gen 0 non prestano nemmeno attenzione agli oggetti con quel bit necessario per la finalizzazione, li promuove automaticamente, senza nemmeno controllare la loro radice. Un oggetto non rootato che necessita di finalizzazione in Gen 1, verrà lanciato in FReachablecoda, ma la collezione non fa altro, quindi vive in Gen 2. In questo modo, tutti gli oggetti che hanno un finalizzatore e non GC.SuppressFinalizesarà raccolto in Gen 2.


4
@FlorianMargaine sì ... dire qualcosa su "GC" in tutte le implementazioni non ha davvero senso ...
Jimmy Hoffa

10
tl; dr: utilizzare invece pool di oggetti.
Robert Harvey,

5
tl; dr: per tempistica / profilazione, può essere utile.
Kutschkem,

3
@Den dopo aver letto la mia descrizione sopra della meccanica (come le ho capite), quale sarebbe il vantaggio come la vedi? Pulisci un gran numero di oggetti - nel SOH (o LOH?)? Hai appena fatto sospendere altri thread per questa raccolta? Quella collezione ha appena promosso il doppio degli oggetti della seconda generazione rispetto a quelli eliminati? La raccolta ha causato compattazione su LOH (l'hai attivata?)? Quanti heap GC hai e il tuo GC è in modalità server o desktop? GC è un fangoso iceberg, il tradimento è sotto le acque. Stai alla larga. Non sono abbastanza intelligente da collezionare comodamente.
Jimmy Hoffa,

4
Anche i pool di oggetti @RobertHarvey non sono un proiettile d'argento. La generazione 0 del Garbage Collector è già effettivamente un pool di oggetti: di solito è dimensionata per adattarsi al livello di cache più piccolo e quindi i nuovi oggetti vengono generalmente creati nella memoria che è già nella cache. Il tuo pool di oggetti è ora in competizione con il vivaio del GC per la cache, e se la somma del vivaio del GC e del tuo pool è maggiore della cache, avrai ovviamente delle mancate cache. E se prevedi di utilizzare il parallelismo ora devi reimplementare la sincronizzazione e preoccuparti della falsa condivisione.
Doval,

68

Purtroppo, nessuno là elabora su cosa siano tali casi.

Darò alcuni esempi. Tutto sommato è raro che forzare un GC sia una buona idea, ma può valerne la pena. Questa risposta è dalla mia esperienza con la letteratura .NET e GC. Dovrebbe generalizzare bene su altre piattaforme (almeno quelle che hanno un GC significativo).

  • Benchmark di vario genere. Si desidera uno stato heap gestito noto quando inizia un benchmark in modo che il GC non si attivi casualmente durante i benchmark. Quando ripeti un benchmark vuoi lo stesso numero e la stessa quantità di lavoro GC in ogni ripetizione.
  • Rilascio improvviso di risorse. Ad esempio chiudendo una finestra della GUI significativa o aggiornando una cache (e rilasciando così il vecchio contenuto della cache potenzialmente grande). Il GC non è in grado di rilevarlo perché tutto ciò che si sta facendo è impostare un riferimento su null. Il fatto che questo orfano un intero oggetto grafico non è facilmente rilevabile.
  • Rilascio di risorse non gestite che sono trapelate . Questo non dovrebbe mai accadere, ovviamente, ma ho visto casi in cui una libreria di terze parti trapelava roba (come oggetti COM). Lo sviluppatore è stato costretto a volte a indurre una raccolta.
  • Applicazioni interattive come i giochi . Durante i giochi i budget sono molto rigidi per frame (60Hz => 16ms per frame). Per evitare problemi, è necessaria una strategia per gestire i GC. Una di queste strategie è quella di ritardare il più possibile i GC G2 e forzarli in un momento opportuno come una schermata di caricamento o una scena tagliata. Il GC non può sapere quando è il momento migliore.
  • Controllo della latenza in generale. Alcune applicazioni Web disabilitano i GC ed eseguono periodicamente una raccolta G2 mentre vengono disattivate dalla rotazione del bilanciamento del carico. In questo modo la latenza G2 non viene mai visualizzata all'utente.

Se il tuo obiettivo è il throughput, più raro è il GC, meglio è. In quei casi forzare una raccolta non può avere un impatto positivo (ad eccezione di problemi piuttosto artificiosi come l'aumento dell'utilizzo della cache della CPU rimuovendo gli oggetti morti intervallati da quelli attivi). La raccolta in lotti è più efficiente per tutti i collezionisti che conosco. Per l'app di produzione con consumo di memoria in stato stazionario indurre un GC non aiuta.

Gli esempi sopra riportati indicano la coerenza e la limitazione dell'utilizzo della memoria. In questi casi i GC indotti possono avere senso.

Sembra esserci un'idea diffusa che il CG sia un'entità divina che induce una raccolta ogni volta che è davvero ottimale farlo. Nessun GC che conosco è così sofisticato e in effetti è molto difficile essere ottimali per il GC. Il GC sa meno dello sviluppatore. Le sue euristiche si basano su contatori di memoria e cose come la velocità di raccolta e così via. L'euristica di solito è buona ma non cattura improvvisi cambiamenti nel comportamento dell'applicazione come il rilascio di grandi quantità di memoria gestita. È inoltre cieco alle risorse non gestite e ai requisiti di latenza.

Si noti che i costi GC variano in base alla dimensione dell'heap e al numero di riferimenti sull'heap. Su un piccolo mucchio il costo può essere molto piccolo. Ho riscontrato tassi di raccolta G2 con .NET 4.5 di 1-2 GB / sec su un'app di produzione con heap da 1 GB.


Per il caso del controllo della latenza, suppongo che invece di farlo periodicamente, potresti farlo anche per necessità (cioè quando l'utilizzo della memoria supera una certa soglia).
Paŭlo Ebermann,

3
+1 per il penultimo paragrafo. Alcune persone hanno lo stesso sentimento nei confronti dei compilatori e si affrettano a chiamare quasi tutto "ottimizzazione prematura". Di solito dico loro qualcosa di simile.
Honza Brabec,

2
+1 anche per quel paragrafo. Trovo scioccante che la gente pensi che un programma per computer scritto da qualcun altro debba necessariamente comprendere le caratteristiche prestazionali del proprio programma meglio di se stesso.
Mehrdad,

1
@HonzaBrabec Il problema è lo stesso in entrambi i casi: se pensi di sapere meglio del GC o del compilatore, è molto facile farti del male. Se in realtà ne sai di più, allora stai ottimizzando solo quando sai che non è prematuro.
svick,

27

Come principio generale, un garbage collector raccoglierà quando si imbatterà in "pressione della memoria", ed è considerata una buona idea non farlo raccogliere in altri momenti perché potresti causare problemi di prestazioni o anche pause evidenti nell'esecuzione del tuo programma. E infatti, il primo punto dipende dal secondo: per un garbage collector generazionale, almeno, funziona in modo più efficiente quanto più alto è il rapporto tra immondizia e oggetti buoni, quindi al fine di ridurre al minimo il tempo impiegato a mettere in pausa il programma , deve procrastinare e lasciare che la spazzatura si accumuli il più possibile.

Il momento appropriato per invocare manualmente il Garbage Collector, quindi, è quando hai finito di fare qualcosa che 1) è probabile che abbia creato molta spazzatura, e 2) ci si aspetta che l'utente impieghi un po 'di tempo e lasci il sistema non risponde Comunque. Un esempio classico è alla fine del caricamento di qualcosa di grande (un documento, un modello, un nuovo livello, ecc.)


12

Una cosa che nessuno ha menzionato è che, mentre il GC di Windows è incredibilmente buono, il GC su Xbox è spazzatura (gioco di parole intenzionale) .

Quindi, quando si codifica un gioco XNA destinato a essere eseguito su XBox, è assolutamente fondamentale cronometrare la raccolta dei rifiuti in momenti opportuni, oppure si avranno orribili singhiozzi intermittenti FPS. Inoltre, su XBox è comune usare structs way, molto più spesso di quanto si farebbe normalmente, per ridurre al minimo il numero di oggetti che devono essere raccolti.


4

La garbage collection è innanzitutto uno strumento di gestione della memoria. Pertanto, i raccoglitori di rifiuti si raccoglieranno quando c'è pressione della memoria.

I moderni bidoni della spazzatura sono molto buoni e stanno migliorando, quindi è improbabile che tu possa migliorarli raccogliendoli manualmente. Anche se puoi migliorare le cose oggi, potrebbe darsi che un futuro miglioramento del tuo garbage collector scelto renderà la tua ottimizzazione inefficace o addirittura controproducente.

Tuttavia , i garbage collector in genere non tentano di ottimizzare l'uso di risorse diverse dalla memoria. In ambienti garbage collection, le risorse non di memoria più preziose hanno un closemetodo o simile, ma ci sono alcune occasioni in cui questo non è il caso per qualche motivo, come la compatibilità con un'API esistente.

In questi casi può avere senso invocare manualmente la garbage collection quando si sa che viene utilizzata una preziosa risorsa non di memoria.

RMI

Un esempio concreto di ciò è la chiamata a metodo remoto di Java. RMI è una libreria di chiamate di procedura remota. In genere si dispone di un server, che rende vari oggetti disponibili per l'uso da parte dei client. Se un server sa che un oggetto non viene utilizzato da alcun client, quell'oggetto è idoneo per la garbage collection.

Tuttavia, l'unico modo in cui il server lo sa è se il client lo dice e il client dice al server che non ha più bisogno di un oggetto una volta che il client ha raccolto la spazzatura qualunque cosa lo stia usando.

Ciò presenta un problema, poiché il client potrebbe disporre di molta memoria libera, pertanto potrebbe non essere in grado di eseguire la garbage collection molto frequentemente. Nel frattempo, il server potrebbe avere molti oggetti inutilizzati in memoria, che non può raccogliere perché non sa che il client non li sta usando.

La soluzione in RMI è che il client esegua periodicamente la garbage collection, anche quando ha molta memoria libera, per garantire che gli oggetti vengano raccolti prontamente sul server.


"In questi casi può essere sensato invocare manualmente la garbage collection quando si sa che viene utilizzata una preziosa risorsa non di memoria" - se viene utilizzata una risorsa non di memoria, è necessario utilizzare un usingblocco o chiamare in altro modo un Closemetodo per assicurarsi che la risorsa venga eliminata il prima possibile. Fare affidamento su GC per ripulire le risorse non di memoria non è affidabile e causa tutti i tipi di problemi (in particolare con i file che devono essere bloccati per l'accesso, quindi possono essere aperti solo una volta).
Jules,

E come indicato nella risposta, quando closeè disponibile un metodo (o la risorsa può essere utilizzata con un usingblocco), questi sono l'approccio giusto. La risposta riguarda specificamente i rari casi in cui questi meccanismi non sono disponibili.
James_pic,

La mia opinione personale è che qualsiasi interfaccia che gestisca una risorsa non di memoria ma non fornisca un metodo vicino è un'interfaccia che non dovrebbe essere usata , perché non c'è modo di usarla in modo affidabile.
Jules,

@Jules Sono d'accordo, ma a volte è inevitabile. A volte le astrazioni perdono e usare un'astrazione che perde è meglio che non usare l'astrazione. A volte devi lavorare con un codice legacy che ti richiede di fare promesse che sai di non poter mantenere. Sì, è raro e dovrebbe essere evitato, se possibile, e c'è una ragione per cui ci sono tutti questi avvertimenti sul forzare la raccolta dei rifiuti, ma queste situazioni si presentano e l'OP stava chiedendo come sarebbero queste situazioni - a cui ho risposto .
James_pic,

2

La migliore pratica è di non forzare una raccolta dei rifiuti nella maggior parte dei casi. (Ogni sistema su cui ho lavorato aveva forzato la raccolta dei rifiuti, aveva sottolineato problemi che se risolti avrebbero rimosso la necessità di forzare la raccolta dei rifiuti e accelerato notevolmente il sistema.)

Ci sono alcuni casi in cui si sa di più su l'utilizzo della memoria, allora il garbage collector fa. È improbabile che ciò sia vero in un'applicazione multiutente o in un servizio che risponde a più di una richiesta alla volta.

Tuttavia, in alcune elaborazioni di tipo batch , conosci più del GC. Ad esempio, considerare un'applicazione che.

  • Viene fornito un elenco di nomi di file sulla riga di comando
  • Elabora un singolo file, quindi scrive il risultato in un file di risultati.
  • Durante l'elaborazione del file, crea molti oggetti interconnessi che non possono essere raccolti fino al completamento dell'elaborazione del file (ad esempio un albero di analisi)
  • Non mantiene lo stato di corrispondenza tra i file che ha elaborato .

Si può essere in grado di fare un caso (dopo un'attenta) di test che si dovrebbe forzare una garbage collection completa dopo aver processo di ciascun file.

Un altro caso è un servizio che si sveglia ogni pochi minuti per elaborare alcuni elementi e non mantiene alcuno stato mentre è addormentato . Quindi può essere utile forzare una raccolta completa prima di andare a dormire .

L'unica volta che prenderei in considerazione la possibilità di forzare una raccolta è quando so che un sacco di oggetti sono stati creati di recente e pochissimi oggetti sono attualmente referenziati.

Preferirei avere un'API di Garbage Collection quando potrei dare suggerimenti su questo tipo di cose senza dover forzare un GC da solo.

Vedi anche " Rico Mariani's Performance Tidbits "


2

Esistono diversi casi in cui potresti voler chiamare tu stesso gc ().

  • [ Alcune persone dicono che questo non va bene perché può promuovere oggetti nello spazio di vecchia generazione, che sono d'accordo non è una buona cosa. Tuttavia, NON è sempre vero che ci saranno sempre oggetti che possono essere promossi. È certamente possibile che dopo questa gc()chiamata rimangano pochissimi oggetti e tanto meno vengano spostati nello spazio di generazione precedente ] Quando si crea una grande raccolta di oggetti e si utilizza molta memoria. Volete semplicemente liberare quanto più spazio possibile di preparazione. È solo buonsenso. Chiamando gc()manualmente, non ci sarà controllo grafico di riferimento ridondante su parte di quella grande raccolta di oggetti che si sta caricando in memoria. In breve, se si esegue gc()prima di caricare molto in memoria, ilgc() indotto durante il caricamento avviene meno di almeno una volta quando il caricamento inizia creando la pressione della memoria.
  • Dopo aver caricato una grande raccolta digrandeoggetti e difficilmente caricherai più oggetti in memoria. In breve, si passa dalla creazione della fase all'utilizzo della fase. Chiamando in gc()base all'implementazione, la memoria utilizzata verrà compattata, migliorando notevolmente la localizzazione della cache. Ciò comporterà un notevole miglioramento delle prestazioni che non otterrai dalla profilazione .
  • Simile al primo, ma dal punto di vista che se lo fai gc()e l'implementazione della gestione della memoria supporta, creerai una continuità molto migliore per la tua memoria fisica. Questo rende di nuovo la nuova grande raccolta di oggetti più continua e compatta che a sua volta migliora le prestazioni

1
Qualcuno può indicare il motivo del downvote? Io stesso non so abbastanza per giudicare la risposta (a prima vista ha un senso per me).
Omega,

1
Immagino che tu abbia ottenuto un downvote per il terzo punto. Potenzialmente anche per dire "Questo è solo buon senso".
immibis,

2
Quando crei una grande raccolta di oggetti, il GC dovrebbe essere abbastanza intelligente da sapere se è necessaria una raccolta. Lo stesso quando è necessario compattare la memoria. Affidarsi al GC per ottimizzare la localizzazione della memoria degli oggetti correlati non sembra realizzabile. Penso che tu possa trovare altre soluzioni (struct, unsafe, ...). (Non sono il downvoter).
Guillaume,

3
La tua prima idea di un momento giusto è solo un cattivo consiglio secondo me. È molto probabile che ci sia stata una collezione di recente, quindi il tuo tentativo di collezionare di nuovo semplicemente promuoverà arbitrariamente oggetti per le generazioni successive, il che è quasi sempre negativo. Le generazioni successive hanno collezioni che impiegano più tempo per cominciare, aumentando le loro dimensioni dell'heap "per liberare quanto più spazio possibile" rende questo più problematico. Inoltre, se stai per aumentare la pressione della memoria con un carico, è probabile che inizi a indurre comunque le raccolte, che verranno eseguite più lentamente perché aumentato Gen1 / 2
Jimmy Hoffa

2
By calling gc() depending on implementation, the memory in used will be compacted which massively improves cache locality. This will result in massive improve in performance that you will not get from profiling.Se assegni una tonnellata di oggetti in una riga, le probabilità sono già compattate. Se non altro, la raccolta dei rifiuti potrebbe mescolarli leggermente. In entrambi i casi, l'utilizzo di strutture dati che sono dense e che non saltano casualmente nella memoria avrà un impatto maggiore. Se stai usando un ingenuo elenco collegato a un elemento per nodo, nessuna quantità di inganno GC manuale lo compenserà.
Doval

2

Un esempio reale:

Avevo un'applicazione Web che utilizzava un insieme molto ampio di dati che raramente cambiava e ai quali era necessario accedere molto rapidamente (abbastanza veloce per la risposta per battitura tramite AJAX).

La cosa abbastanza ovvia da fare qui è caricare il grafico rilevante in memoria e accedervi da lì anziché dal database, aggiornando il grafico quando il DB cambia.

Ma essendo molto grande, un carico ingenuo avrebbe assorbito almeno 6 GB di memoria con i dati a causa della crescita futura. (Non ho cifre esatte, una volta era chiaro che la mia macchina da 2 GB stava cercando di far fronte ad almeno 6 GB, avevo tutte le misure di cui avevo bisogno per sapere che non avrebbe funzionato).

Fortunatamente, c'era un gran numero di oggetti immutabili con ghiaccioli in questo insieme di dati che erano uguali tra loro; una volta scoperto che un determinato batch era uguale a un altro batch, ho potuto alias un riferimento all'altro consentendo la raccolta di molti dati e quindi adattando tutto in meno di mezzo concerto.

Tutto bene, ma per questo ancora sfornano oltre 6 GB di oggetti nello spazio di circa mezzo minuto per arrivare a questo stato. Lasciato a se stesso, GC non ce la fece; il picco di attività rispetto al solito schema dell'applicazione (molto meno pesante per deallocazioni al secondo) era troppo acuto.

Quindi chiamare periodicamente GC.Collect()durante questo processo di compilazione significava che tutto funzionava senza problemi. Naturalmente, non ho chiamato manualmente GC.Collect()il resto del tempo in cui l'applicazione viene eseguita.

Questo caso del mondo reale è un buon esempio delle linee guida su quando dovremmo usare GC.Collect():

  1. Utilizzare con un caso relativamente raro di molti oggetti resi disponibili per la raccolta (sono stati resi disponibili megabyte di valore, e questa creazione di grafici è stata un caso molto raro per la durata dell'applicazione (circa un minuto a settimana).
  2. Fallo quando una perdita di prestazioni è relativamente tollerabile; questo è accaduto solo all'avvio dell'applicazione. (Un altro buon esempio di questa regola è tra i livelli durante una partita o altri punti in una partita in cui i giocatori non saranno turbati da una pausa).
  3. Profilo per essere sicuro che ci sia davvero un miglioramento. (Abbastanza facile; "Funziona" batte quasi sempre "non funziona").

Il più delle volte quando ho pensato che avrei potuto avere un caso in cui GC.Collect()valesse la pena chiamare, perché i punti 1 e 2 applicati, il punto 3 suggeriva che le cose peggioravano o almeno non rendevano le cose migliori (e con un miglioramento scarso o nullo avrei incline a non chiamare più di chiamare poiché l'approccio ha maggiori probabilità di dimostrarsi migliore nel corso della vita di un'applicazione).


0

Ho un uso per lo smaltimento dei rifiuti che è un po 'poco ortodosso.

C'è questa pratica sbagliata che è purtroppo molto diffusa nel mondo C #, di implementare lo smaltimento degli oggetti usando il brutto, goffo, inelegante ed incline linguaggio di errori noto come IDisposable-disposing . MSDN lo descrive a lungo e molte persone lo giurano, lo seguono religiosamente, passano ore e ore a discutere esattamente come dovrebbe essere fatto, ecc.

(Si noti che ciò che io chiamo brutto qui non è il modello di smaltimento dell'oggetto stesso; quello che io chiamo brutto è il particolare IDisposable.Dispose( bool disposing )linguaggio.)

Questo idioma è stato inventato perché è presumibilmente impossibile garantire che il distruttore dei tuoi oggetti sia sempre invocato dal garbage collector per ripulire le risorse, quindi le persone eseguono la pulizia delle risorse all'interno IDisposable.Dispose()e, nel caso in cui dimenticano, ci provano ancora all'interno del distruttore. Sai, per ogni evenienza.

Ma allora IDisposable.Dispose()potresti avere sia oggetti gestiti che non gestiti da ripulire, ma quelli gestiti non possono essere ripuliti quando IDisposable.Dispose()vengono invocati dall'interno del distruttore, perché sono già stati curati dal garbage collector in quel momento, quindi è questa necessità di un Dispose()metodo separato che accetta un bool disposingflag per sapere se sia gli oggetti gestiti che quelli non gestiti devono essere ripuliti o solo quelli non gestiti.

Mi scusi, ma questo è semplicemente folle.

Seguo l'assioma di Einstein, secondo il quale le cose dovrebbero essere il più semplici possibile, ma non più semplici. Chiaramente, non possiamo omettere la pulizia delle risorse, quindi la soluzione più semplice possibile deve includere almeno quella. La prossima soluzione più semplice prevede di disporre sempre di tutto nel momento esatto in cui dovrebbe essere disposto, senza complicare le cose facendo affidamento sul distruttore come alternativa alternativa.

Ora, a rigor di termini, è ovviamente impossibile garantire che nessun programmatore commetterà mai l'errore di dimenticare di invocare IDisposable.Dispose(), ma ciò che possiamo fare è usare il distruttore per cogliere questo errore. È molto semplice, davvero: tutto ciò che il distruttore deve fare è generare una voce di registro se rileva che il disposedflag dell'oggetto usa e getta non è mai stato impostato true. Pertanto, l'uso del distruttore non è parte integrante della nostra strategia di smaltimento, ma è il nostro meccanismo di garanzia della qualità. E poiché si tratta solo di un test in modalità debug, possiamo posizionare l'intero distruttore all'interno di un #if DEBUGblocco, in modo da non incorrere in alcuna penalità di distruzione in un ambiente di produzione. (Il IDisposable.Dispose( bool disposing )linguaggio lo prescriveGC.SuppressFinalize() dovrebbe essere invocato proprio per ridurre il sovraccarico di finalizzazione, ma con il mio meccanismo è possibile evitare completamente tale sovraccarico nell'ambiente di produzione.)

Ciò a cui si riduce è l'eterno errore duro rispetto all'argomento errore lieve : l' IDisposable.Dispose( bool disposing )idioma è un approccio di errore morbido e rappresenta un tentativo di consentire al programmatore di dimenticare di invocare Dispose()senza che il sistema fallisca, se possibile. L'approccio dell'errore grave dice che il programmatore deve sempre assicurarsi che Dispose()verrà invocato. La penalità solitamente prescritta dall'approccio dell'errore grave nella maggior parte dei casi è l'asserzione fallita, ma per questo caso particolare facciamo un'eccezione e riduciamo la penalità con una semplice emissione di una voce del registro errori.

Quindi, affinché questo meccanismo funzioni, la versione DEBUG della nostra applicazione deve eseguire uno smaltimento completo dei rifiuti prima di uscire, in modo da garantire che tutti i distruttori vengano invocati e quindi catturare tutti gli IDisposableoggetti che abbiamo dimenticato di smaltire.


Now, strictly speaking, it is of course impossible to guarantee that no programmer will ever make the mistake of forgetting to invoke IDisposable.Dispose()In realtà non lo è, anche se non credo che C # sia in grado di farlo. Non esporre la risorsa; invece fornisce un DSL per descrivere tutto ciò che farai con esso (in sostanza, una monade), oltre a una funzione che acquisisce la risorsa, fa le cose, la libera e restituisce il risultato. Il trucco è utilizzare il sistema di tipi per garantire che se qualcuno contrabbandasse un riferimento alla risorsa, non può essere utilizzato in un'altra chiamata alla funzione di esecuzione.
Doval

2
Il problema con Dispose(bool disposing)(che non è definito su IDisposableè che è usato per occuparsi della pulizia di oggetti gestiti e non gestiti l'oggetto ha come campo (o è altrimenti responsabile), che sta risolvendo il problema sbagliato. oggetti non gestiti in un oggetto gestito senza altri oggetti usa e getta di cui preoccuparsi, quindi tutti i Dispose()metodi saranno uno di quelli (se necessario, il finalizzatore deve eseguire la stessa pulizia) o devono disporre solo degli oggetti gestiti da smaltire (non è necessario un finalizzatore affatto) e il bisogno di bool disposingscompare.
Jon Hanna,

-1 cattivo consiglio a causa del funzionamento effettivo della finalizzazione. Sono assolutamente d'accordo con il tuo punto sul fatto che il dispose(disposing)linguaggio sia terribade, ma lo dico perché le persone usano così spesso quella tecnica e i finalizzatori quando hanno solo risorse gestite (l' DbConnectionoggetto ad esempio è gestito , non è pinvoked o com marshalled), e DEVI SOLO REALIZZARE MAI UN FINALIZZATORE CON CODICE NON GESTITO, PINVOKED, COM MARSHALLED O UNSAFE . Ho spiegato in dettaglio nella mia risposta quanto sono finalizzatori terribilmente costosi, non usarli a meno che tu non abbia risorse non gestite nella tua classe.
Jimmy Hoffa,

2
Voglio quasi darti +1 anche se solo perché stai denigrando qualcosa che così tante persone considerano una cosa fondamentale del dispose(dispoing)linguaggio, ma la verità è che è così prevalente perché le persone hanno così paura delle cose GC che qualcosa di non correlato come che ( disposedovrebbe avere zilch a che fare con GC) li merita di prendere semplicemente la medicina prescritta senza nemmeno studiarla. Buono per te per averlo ispezionato, ma ti sei perso il più grande insieme (incoraggia i finalizzatori a farrr più spesso di quanto dovrebbero essere)
Jimmy Hoffa

1
@JimmyHoffa grazie per il tuo contributo. Sono d'accordo che un finalizzatore dovrebbe normalmente essere utilizzato solo per il rilascio di risorse non gestite, ma non saresti d'accordo sul fatto che nella build DEBUG questa regola non sia applicabile e che nella build DEBUG dovremmo essere liberi di usare i finalizzatori per rilevare i bug? Questo è tutto ciò che sto suggerendo qui, quindi non vedo il motivo per cui si mette in discussione con esso. Vedi anche programmers.stackexchange.com/questions/288715/… per una spiegazione più lunga di questo approccio sul lato java del mondo.
Mike Nakis,

0

Puoi dirmi in quale tipo di scenario è effettivamente una buona o ragionevole idea forzare la raccolta dei rifiuti? Non sto chiedendo casi specifici per C #, ma piuttosto tutti i linguaggi di programmazione che hanno un garbage collector. So che non puoi forzare GC su tutte le lingue, come Java, ma supponiamo che tu possa farlo.

Parlando in modo molto teorico e trascurando problemi come alcune implementazioni di GC che rallentano le cose durante i loro cicli di raccolta, lo scenario più grande che mi viene in mente di forzare la raccolta dei rifiuti è un software mission-critical in cui le perdite logiche sono preferibili a penzolare gli arresti anomali dei puntatori, ad esempio perché l'arresto anomalo in momenti inaspettati potrebbe costare vite umane o qualcosa del genere.

Se guardi alcuni dei più deboli giochi indie scritti usando linguaggi GC come i giochi Flash, perdono come un matto ma non si schiantano. Potrebbero essere necessari 20 volte la memoria in 20 minuti per giocare perché una parte della base di codice del gioco ha dimenticato di impostare un riferimento su null o rimuoverlo da un elenco e le frequenze dei fotogrammi potrebbero iniziare a risentirne, ma il gioco continua a funzionare. Un gioco simile scritto usando la scadente codifica C o C ++ potrebbe bloccarsi a causa dell'accesso a puntatori penzolanti a causa dello stesso tipo di errore di gestione delle risorse, ma non perderebbe così tanto.

Per i giochi l'incidente potrebbe essere preferibile nel senso che può essere rapidamente rilevato e risolto, ma per un programma mission-critical, lo schianto in momenti totalmente inaspettati potrebbe uccidere qualcuno. Quindi i casi principali che penso siano scenari in cui il non crash o altre forme di sicurezza sono assolutamente critici e una perdita logica è una cosa relativamente banale in confronto.

Lo scenario principale in cui penso sia male forzare GC è per cose in cui la perdita logica è in realtà meno preferibile di un incidente. Con i giochi, ad esempio, il crash non ucciderà necessariamente nessuno e potrebbe essere facilmente catturato e riparato durante i test interni, mentre una perdita logica potrebbe passare inosservata anche dopo la spedizione del prodotto a meno che non sia così grave da rendere il gioco ingiocabile in pochi minuti . In alcuni domini un arresto facilmente riproducibile che si verifica nei test è talvolta preferibile a una perdita che nessuno nota immediatamente.

Un altro caso che mi viene in mente dove potrebbe essere preferibile forzare GC in una squadra è per un programma di breve durata, come solo qualcosa eseguito dalla riga di comando che fa un compito e poi si spegne. In tal caso, la durata del programma è troppo breve per rendere non banale qualsiasi tipo di perdita logica. Le perdite logiche, anche per grandi risorse, di solito diventano problematiche solo ore o minuti dopo l'esecuzione del software, quindi è improbabile che un software che deve essere eseguito solo per 3 secondi abbia mai problemi con perdite logiche e potrebbe fare molto è più semplice scrivere programmi di così breve durata se il team ha appena usato GC.

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.