In che modo un garbage collector impedisce che l'intera memoria venga scansionata su ogni raccolta?


16

Alcuni (almeno i Mono e .NET) i garbage collector hanno un'area di memoria a breve termine che scansionano spesso e un'area di memoria secondaria che scansionano meno spesso. Mono chiama questo un asilo nido.

Per scoprire quali oggetti possono essere eliminati, scansionano tutti gli oggetti a partire da root, stack e registri e dispongono di tutti gli oggetti a cui non viene più fatto riferimento.

La mia domanda è: come impediscono a tutti i ricordi della memoria in uso di essere scansionati ad ogni raccolta? In linea di principio, l'unico modo per scoprire quali oggetti non sono più in uso è scansionare tutti gli oggetti e tutti i loro riferimenti. Tuttavia, ciò impedirebbe al sistema operativo di scambiare memoria anche se non è in uso dall'applicazione e sembra una grande quantità di lavoro che deve essere fatto, anche per "Nursery Collection". Non sembra che stiano vincendo molto usando un asilo nido.

Mi sto perdendo qualcosa o il Garbage Collector sta effettivamente scansionando ogni oggetto e ogni riferimento ogni volta che fa una raccolta?


1
una bella panoramica è in un articolo The Art of Garbage Collection Tuning scritto da Angelika Langer. Formalmente, si tratta del modo in cui viene fatto in Java, ma i concetti presentati sono praticamente agnostici del linguaggio
moscerino

Risposte:


14

Le osservazioni fondamentali che consentono alla garbage collection generazionale di evitare la scansione di tutti gli oggetti di vecchia generazione sono:

  1. Dopo una raccolta, tutti gli oggetti ancora esistenti saranno di una generazione minima (ad es. In .net, dopo una raccolta Gen0, tutti gli oggetti sono Gen1 o Gen2; dopo una raccolta Gen1 o Gen2, tutti gli oggetti sono Gen2).
  2. Un oggetto, o parte di esso, che non è stato scritto da una collezione che ha promosso tutto alla generazione N o superiore non può contenere riferimenti a oggetti di generazioni inferiori.
  3. Se un oggetto ha raggiunto una determinata generazione, non è necessario identificarlo come raggiungibile per garantirne la conservazione durante la raccolta di generazioni inferiori.

In molti framework GC, è possibile che il garbage collector contrassegni oggetti o parti di essi in modo tale che il primo tentativo di scriverli attiverà un codice speciale per registrare il fatto che sono stati modificati. Un oggetto o parte di esso che è stato modificato, indipendentemente dalla sua generazione, deve essere scansionato nella raccolta successiva, poiché può contenere riferimenti a oggetti più recenti. D'altra parte, è molto comune che ci siano molti oggetti più vecchi che non vengono modificati tra le raccolte. Il fatto che le scansioni di generazione inferiore possano ignorare tali oggetti può consentire a tali scansioni di completarsi molto più rapidamente di quanto farebbero altrimenti.

Si noti, tra l'altro, che anche se non si riesce a rilevare quando gli oggetti vengono modificati e si dovrebbe scansionare tutto su ogni passaggio GC, la garbage collection generazionale potrebbe comunque migliorare le prestazioni dello stage "sweep" di un collettore di compattazione. In alcuni ambienti embedded (in particolare quelli in cui vi è poca o nessuna differenza di velocità tra accessi di memoria sequenziali e casuali), spostare i blocchi di memoria è relativamente costoso rispetto ai riferimenti di tagging. Di conseguenza, anche se la fase "mark" non può essere accelerata utilizzando un collector generazionale, può essere utile accelerare la fase "sweep".


spostare i blocchi di memoria è costoso in qualsiasi sistema, quindi migliorare lo sweep è un guadagno anche sul tuo sistema di CPU quad Ghz.
gbjbaanb,

@gbjbaanb: In molti casi, il costo della scansione di tutto per trovare oggetti vivi sarebbe significativo e discutibile anche se lo spostamento degli oggetti fosse totalmente gratuito. Di conseguenza, si dovrebbe, quando possibile, evitare di scansionare vecchi oggetti. D'altra parte, astenersi dal compattare oggetti più vecchi è una semplice ottimizzazione che può essere realizzata anche su semplici framework. A proposito, se si progettasse un framework GC per un piccolo sistema incorporato, il supporto dichiarativo per oggetti immutabili potrebbe essere utile. Controllare se un oggetto mutevole è cambiato è difficile, ma si potrebbe fare bene a ...
supercat

... supponiamo semplicemente che gli oggetti mutabili debbano essere scansionati ad ogni GC pass, ma gli oggetti immutabili no. Anche se l'unico modo per costruire un oggetto immutabile era costruire un "prototipo" in uno spazio mutabile e quindi copiarlo, la singola operazione di copia extra poteva evitare la necessità di scansionare l'oggetto nelle future operazioni GC.
supercat

Per inciso, le prestazioni della garbage collection sulle implementazioni derivate da Microsoft nel 1980 di BASIC per i microprocessori 6502 (e forse anche altri) potrebbero essere notevolmente migliorate in alcuni casi, se un programma che generava molte stringhe che non sarebbero mai cambiate, ha copiato il "prossimo puntatore "allocazione stringa" al puntatore "top of string space". Un simile cambiamento impedirebbe al garbage collector di esaminare una qualsiasi delle vecchie stringhe per vedere se fossero ancora necessarie. Il Commodore 64 era a malapena hi-tech, ma tale GC "generazionale" avrebbe aiutato anche lì.
supercat

7

I GC a cui ti riferisci sono generatori di rifiuti generazionali . Sono progettati per ottenere il massimo da un'osservazione nota come "mortalità infantile" o "l'ipotesi generazionale", il che significa che la maggior parte degli oggetti diventa irraggiungibile molto rapidamente. Effettivamente scansionano a partire dalle radici, ma ignorano tutti i vecchi oggetti . Pertanto, non hanno bisogno di scansionare la maggior parte degli oggetti in memoria, scansionano solo oggetti giovani (a spese di non rilevare vecchi oggetti irraggiungibili, almeno non a quel punto).

"Ma è sbagliato", ti sento urlare, "gli oggetti vecchi possono e fanno riferimento a oggetti giovani". Hai ragione, e ci sono diverse soluzioni a tutto ciò che ruotano attorno all'acquisizione di conoscenza, rapida ed efficiente, quali vecchi oggetti devono essere controllati e quali sono sicuri da ignorare. Si riducono praticamente a registrare oggetti, o piccoli (più grandi degli oggetti, ma molto più piccoli dell'intero mucchio) di memoria che contengono indicazioni per le generazioni più giovani. Altri li hanno descritti molto meglio di me, quindi ti darò solo un paio di parole chiave: segnare le carte, set ricordati, scrivere barriere. Esistono anche altre tecniche (compresi gli ibridi), ma queste comprendono gli approcci comuni di cui sono a conoscenza.


3

Per scoprire quali oggetti della scuola materna sono ancora vivi, il collezionista deve solo scansionare il set di root e tutti i vecchi oggetti che sono stati mutati dall'ultima raccolta , poiché un vecchio oggetto che non è stato mutato di recente non può puntare a un oggetto giovane . Esistono diversi algoritmi per mantenere queste informazioni a vari livelli di precisione (da un set esatto di campi mutati a un set di pagine in cui potrebbe essersi verificata una mutazione), ma generalmente implicano una sorta di barriera di scrittura : codice che gira su ogni riferimento mutazione sul campo di tipo tipico che aggiorna la contabilità del GC.


1

La più vecchia e semplice generazione di garbage collector ha effettivamente scansionato tutta la memoria e ha dovuto interrompere tutte le altre elaborazioni mentre lo faceva. Gli algoritmi successivi sono migliorati in questo modo in vari modi, rendendo la copia / scansione incrementale o eseguita in parallelo. La maggior parte dei moderni bidoni della spazzatura separa gli oggetti in generazioni e gestisce con attenzione i puntatori intergenerazionali in modo che le nuove generazioni possano essere raccolte senza disturbare quelle più vecchie.

Il punto chiave è che i garbage collector lavorano in stretta collaborazione con il compilatore e con il resto del runtime per mantenere l'illusione che stia guardando tutta la memoria.


Non sono sicuro degli approcci di garbage collection utilizzati nei minicomputer e nei mainframe prima della fine degli anni '70, ma il garbage collector di Microsoft BASIC, almeno su 6502 macchine, avrebbe impostato il suo puntatore "stringa successiva" in cima alla memoria, e quindi avrebbe cercato tutti i riferimenti di stringa per trovare l'indirizzo più alto che si trovava sotto il "puntatore di stringa successivo". Quella stringa verrebbe copiata appena sotto il "puntatore della stringa successiva" e quel puntatore verrebbe parcheggiato proprio sotto di essa. L'algoritmo sarebbe quindi ripetere. È stato possibile per il codice eseguire il jinx dei puntatori per fornire ...
supercat

... qualcosa come la collezione generazionale. A volte mi chiedevo quanto sarebbe stato difficile patchare BASIC per implementare la raccolta "generazionale" semplicemente mantenendo gli indirizzi dei vertici di ogni generazione e aggiungendo alcune operazioni di scambio puntatore prima e dopo ogni ciclo GC. Le prestazioni del GC sarebbero comunque piuttosto negative, ma in molti casi potrebbero essere ridotte da decine di secondi a decimi di secondo.
supercat

-2

Fondamentalmente ... GC usa "secchi" per separare ciò che è in uso e ciò che non lo è. Una volta che lo fa controllare, cancella le cose che non sono in uso e sposta tutto il resto alla seconda generazione (che viene controllato meno spesso della prima generazione) e quindi sposta le cose che sono ancora in uso nella seconda tana nella terza generazione.

Quindi, le cose in terza generazione sono di solito oggetti che rimangono bloccati per qualche motivo e GC non controlla molto spesso.


1
Ma come fa a sapere quali oggetti sono in uso?
Pieter van Ginkel,

Tiene traccia di quali oggetti possono essere raggiungibili dal codice raggiungibile. Una volta che un oggetto non è più raggiungibile da alcun codice che può essere eseguito (diciamo, codice per un metodo che è tornato), il GC sa che è sicuro raccogliere
JohnL

Entrambi state descrivendo come i GC sono corretti, non come sono efficienti. A giudicare dalla domanda, OP lo sa perfettamente.

@delnan sì, stavo rispondendo alla domanda su come sa quali oggetti sono in uso, che è quello che era nel commento di Pieter.
JohnL

-5

L'algoritmo solitamente utilizzato da questo GC è il mark-and-sweep naïf

dovresti anche essere consapevole del fatto che questo non è gestito dal C # stesso, ma dal cosiddetto CLR .


Questa è la sensazione che ho avuto dalla lettura del bidone della spazzatura di Mono. Tuttavia, ciò che non capisco è il motivo per cui se eseguono la scansione dell'intero set di lavoro su ogni raccolta, hanno un collettore generazionale con cui la raccolta GEN-0 è molto veloce. Come può mai essere veloce con un set funzionante di dire 2 GB?
Pieter van Ginkel,

bene, il vero GC per mono è Sgen, dovresti leggere questo mono-project.com/Generational_GC o alcuni articoli online schani.wordpress.com/tag/mono infoq.com/news/2011/01/SGen , il punto è che queste nuove tecnologie come CLR e CLI hanno un design davvero modulare, il linguaggio diventa solo un modo per esprimere qualcosa per il CLR e non un modo per produrre codice binario. La tua domanda riguarda i dettagli dell'implementazione e non gli algoritmi, poiché un algoritmo non ha ancora un'implementazione, dovresti semplicemente leggere documenti tecnici e articoli da Mono, nessun altro.
user827992

Non ho capito bene. La strategia che utilizza un Garbage Collector non è un algoritmo?
Pieter van Ginkel,

2
-1 Smettere di confondere OP. Che il CG faccia parte del CLR e non sia specifico per la lingua non è affatto rilevante. Un GC è principalmente caratterizzato dal modo in cui espone l'heap e determina la raggiungibilità, e quest'ultimo riguarda interamente gli algoritmi utilizzati per questo. Mentre ci possono essere molte implementazioni di un algoritmo e non dovresti rimanere impigliato nei dettagli dell'implementazione, l'algoritmo da solo determina quanti oggetti vengono scansionati. Un GC generazionale è semplicemente un algoritmo + layout heap che tenta di utilizzare l '"ipotesi generazionale" (che la maggior parte degli oggetti muore giovane). Questi non sono ingenui.

4
Algorithm! = Implementazione in effetti, ma un'implementazione può solo deviare molto prima che diventi un'implementazione di un algoritmo diverso. Una descrizione dell'algoritmo, nel mondo GC, è molto specifica e include cose come non scansionare l'intero mucchio nella raccolta dei vivai e come trovare e conservare i puntatori intergenerazionali. È vero che un algoritmo non ti dice quanto tempo richiederà un passaggio specifico dell'algoritmo, ma questo non è affatto rilevante per questa domanda.
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.