Come fanno i garbage collector a evitare lo straripamento dello stack?


23

Quindi stavo pensando a come funzionano i netturbini e ho pensato a un problema interessante. Presumibilmente i netturbini devono attraversare tutte le strutture allo stesso modo. Non possono sapere che tempo stanno attraversando un elenco collegato o un albero bilanciato o altro. Inoltre non possono usare troppa memoria nella loro ricerca. Un modo possibile, e l'unico modo in cui riesco a pensare di attraversare TUTTE le strutture, potrebbe essere solo quello di attraversarle tutte in modo ricorsivo come faresti con un albero binario. Ciò darebbe comunque uno stack overflow su un elenco collegato o anche solo un albero binario scarsamente bilanciato. Ma tutte le lingue della spazzatura raccolte che io abbia mai usato sembrano non avere problemi a gestire tali casi.

Nel libro dei draghi utilizza una sorta di "coda non analizzata". Fondamentalmente piuttosto che attraversare ricorsivamente la struttura aggiunge solo cose che devono essere contrassegnate anche come una coda e quindi per ogni cosa non contrassegnata alla fine viene eliminata. Ma questa coda non sarebbe molto grande?

Quindi, in che modo i collettori di rifiuti attraversano strutture arbitrarie? In che modo questa tecnica di attraversamento evita il trabocco?


1
GC attraversa tutte le strutture più o meno allo stesso modo, ma solo in un senso molto astratto (vedi risposta). Il modo in cui tengono traccia concretamente delle cose è molto più sofisticato di quanto indicato dalle presentazioni elementari che puoi trovare nei libri di testo. E non usano la ricorsione. Inoltre, con la memoria virtuale, le cattive implementazioni mostrano un rallentamento del GC, raramente un overflow della memoria.
babou,

Ti preoccupi dello spazio necessario per la traccia. Ma per quanto riguarda lo spazio o le strutture necessarie per distinguere la memoria che è stata rintracciata ed è noto per essere in uso, dalla memoria che è potenzialmente recuperabile. Ciò può avere un costo di memoria significativo, possibilmente proporzionale alla dimensione dell'heap.
babou,

Ho pensato che sarebbe stato fatto con un bitvector su una dimensione dell'oggetto maggiore di circa 16 byte, quindi il sovraccarico sarebbe almeno 1000 volte inferiore.
Jake,

Esistono molti modi per farlo (vedi risposta) e possono anche essere usati per la traccia, che risponderebbe alla tua domanda (bitmap o bitmap possono essere usati per la traccia, piuttosto che lo stack o la coda che suggerisci). Non puoi presumere che tutti gli oggetti siano grandi, a meno che tu non intenda sprecare spazio su piccoli oggetti, di cui possono essercene molti, e quindi non dovresti preoccuparti dello spazio. Se sei nella memoria virtuale, lo spazio è spesso molto meno un problema e i problemi sono molto diversi.
babou,

Risposte:


13

Nota che non sono un esperto della raccolta dei rifiuti. Questa risposta fornisce solo esempi di tecniche. Non sostengo che si tratti di una panoramica rappresentativa delle tecniche di raccolta dei rifiuti.

Una coda non analizzata è una scelta comune. La coda può diventare grande, potenzialmente grande quanto la struttura di dati più profonda. La coda viene in genere archiviata in modo esplicito, non nello stack del thread di Garbage Collection.

Una volta che tutti i figli di un nodo tranne uno sono stati scansionati, il nodo può essere rimosso dalla coda non scansionata. Questa è fondamentalmente un'ottimizzazione delle chiamate di coda. I raccoglitori di rifiuti possono includere l'euristica per tentare di scansionare l'ultimo figlio più profondo di un nodo; per esempio un GC per Lisp dovrebbe scansionare il cardi a consprima del cdr.

Un modo per evitare di mantenere una coda non analizzata è modificare i puntatori in posizione, facendo in modo che il bambino punti temporaneamente al genitore. Questa è una tecnica di attraversamento di alberi a memoria costante che viene utilizzata in contesti diversi dai garbage collector. L'aspetto negativo di questa tecnica è che mentre il GC attraversa una struttura di dati, la struttura dei dati non è valida, quindi il GC deve fermare il mondo. Questo non è un grosso problema: molti garbage collector includono una fase che arresta il mondo, oltre a fasi che non lo fanno ma possono perdere la spazzatura di conseguenza.


2
La tecnica descritta nell'ultimo paragrafo è spesso chiamata " inversione del puntatore ".
Wandering Logic,

@WanderingLogic Sì, l' inversione del puntatore è come l'ho chiamato nella mia risposta. È dovuto a Deutsch, Schorr e Waite (1967). Tuttavia, non è corretto affermare che funziona in memoria costante: richiede bit extra per ogni cella con puntatori , sebbene ciò possa essere ridotto usando stack di bit. La risposta accettata a cui fai riferimento non è del tutto corretta o completa per lo stesso motivo. plog2pp
babou,

Io ho utilizzato inversione puntatore in un GC personalizzato senza bisogno di questi bit extra; il trucco era usare una speciale rappresentazione in memoria degli oggetti in memoria. Vale a dire, l'oggetto "header" era nel mezzo, con i campi puntatore prima dell'intestazione e i campi non puntatore dopo; inoltre, tutti i puntatori erano allineati e l'intestazione includeva un campo con il bit meno significativo sempre impostato. Pertanto, durante il backtrack di inversione del puntatore, raggiungere il puntatore successivo e notare che avevamo finito con un oggetto poteva essere fatto senza ambiguità senza i bit extra. Questo layout supportava anche l'ereditarietà OOP.
Thomas Pornin,

@ThomasPornin Penso che le informazioni sui bit debbano essere da qualche parte. La domanda è dove? Possiamo discuterne in chat? Devo partire ora, ma vorrei arrivare in fondo a questo. Oppure c'è una descrizione raggiungibile sul web?
babou,


11

In poche parole : i netturbini non usano la ricorsione. Controllano semplicemente la traccia tenendo traccia di essenzialmente due set (che possono combinarsi). L'ordine di tracciamento ed elaborazione delle celle è irrilevante, il che offre una notevole libertà di implementazione per rappresentare gli insiemi. Quindi ci sono molte soluzioni che in realtà sono molto economiche nell'uso della memoria. Ciò è essenziale poiché il GC viene chiamato proprio quando l'heap esaurisce la memoria. Le cose sono un po 'diverse con grandi memorie virtuali, poiché le nuove pagine possono essere facilmente allocate e il nemico non è mancanza di spazio, ma mancanza di localizzazione dei dati .

Presumo che tu stia considerando di rintracciare i netturbini, non di contare i riferimenti per i quali la tua domanda non sembra valere.

UV

La prima cosa da notare è che tutti i GC di tracciamento seguono lo stesso modello astratto, basato sull'esplorazione sistematica del grafico diretto delle celle in memoria accessibile dal programma, in cui le celle di memoria sono vertici e i puntatori sono i bordi diretti. Utilizza per questo i seguenti set:

  • VVV=UT

  • U

  • T

  • H

VUUT

UV

UcVUcUT

UUV=TVHVV

VUUT

Salto anche i dettagli su ciò che è una cella, che si tratti di una o più dimensioni, come troviamo puntatori in esse, come possono essere compattati e una miriade di altri problemi tecnici che puoi trovare in libri e sondaggi sulla raccolta dei rifiuti .

U

Dove le implementazioni note differiscono è nel modo in cui questi insiemi sono effettivamente rappresentati. Molte tecniche sono state effettivamente utilizzate:

  • bit map: parte dello spazio di memoria viene conservato per una mappa che ha un bit per ogni cella di memoria, che può essere trovata utilizzando l'indirizzo della cella. Il bit è attivo quando la cella corrispondente si trova nell'insieme definito dalla mappa. Se vengono utilizzate solo bit map, sono necessari solo 2 bit per cella.

  • in alternativa, potresti avere spazio per un bit di tag speciale (o 2) in ogni cella per contrassegnarlo.

  • log2pp

  • puoi testare un predicato sul contenuto della cella e sui suoi puntatori.

  • è possibile spostare la cella in una parte libera della memoria destinata a tutte le sole celle appartenenti all'insieme rappresentato.

  • VTTU

  • puoi effettivamente combinare queste tecniche, anche per un singolo set.

Come detto, tutto quanto sopra è stato utilizzato da alcuni garbage collector implementati, strano come alcuni potrebbero sembrare. Tutto dipende dai vari vincoli dell'implementazione. E possono essere piuttosto economici nell'uso della memoria, probabilmente aiutati dall'elaborazione delle politiche degli ordini che possono essere scelte liberamente a tale scopo, poiché non contano per il risultato finale.

Quello che può sembrare il più strano, il trasferimento di celle in una nuova area, è in realtà molto comune: si chiama raccolta di copie. Viene utilizzato principalmente con la memoria virtuale.

Chiaramente non c'è ricorsione e non è necessario utilizzare lo stack dell'algoritmo mutatore.

Un altro punto importante è che molti GC moderni sono implementati per grandi memorie virtuali . Quindi ottenere spazio da implementare e un elenco o uno stack aggiuntivo non è un problema in quanto le nuove pagine possono essere facilmente allocate. Tuttavia, nelle grandi memorie virtuali, il nemico non è la mancanza di spazio ma la mancanza di località . Quindi, la struttura che rappresenta gli insiemi e il loro utilizzo deve essere orientata a preservare la località della struttura dei dati e dell'esecuzione del GC. Il problema non è lo spazio ma il tempo. Le implementazioni inadeguate hanno maggiori probabilità di mostrare un rallentamento inaccettabile rispetto all'overflow di memoria.

Non ho dato riferimenti a molti algoritmi specifici, risultanti da varie combinazioni di queste tecniche, poiché sembra abbastanza lungo.


4

Il modo standard per evitare un overflow dello stack è utilizzare uno stack esplicito (memorizzato come struttura dati nell'heap). Funziona anche per questi scopi. I raccoglitori dell'immondizia spesso dispongono di una lista di lavoro di elementi che devono essere esaminati / attraversati, che ricopre questo ruolo. Ad esempio, la coda "Non analizzata" è un esempio esattamente di questo tipo di modello. La coda può potenzialmente diventare grande, ma non provoca un overflow dello stack, poiché non è memorizzata nel segmento dello stack. In ogni caso, non sarà mai maggiore del numero di oggetti vivi nell'heap.


Quando viene chiamato il GC, l'heap è generalmente pieno. Un altro punto è che succede che lo stack e l'heap crescono da entrambe le estremità dello stesso spazio di memoria ..
babou

4

Nelle descrizioni "classiche" della garbage collection (ad es. Mark Wilson, " Uniprocessor Garbage Collection Techniques ", Int'l Workshop on Memory Management , 1992, ( collegamento alternativo ), o nella descrizione in Modern Compiler Implementation di Andrew Appel (Cambridge University Press, 1998)), i collezionisti sono classificati come "Mark and Sweep" o "Copying".

I collezionisti di Mark e Sweep evitano di aver bisogno di spazio aggiuntivo utilizzando l'inversione del puntatore, come descritto nella risposta di @ Gilles. Appel afferma che Knuth attribuisce l'algoritmo di inversione del puntatore a Peter Deutsch, a Herbert Schorr e WM Waite.

La copia dei raccoglitori di rifiuti utilizza quello che viene spesso chiamato l'algoritmo di Cheyney per eseguire un attraversamento della coda senza bisogno di spazio aggiuntivo. Questo algoritmo è stato introdotto in CJ Cheyney, "Un algoritmo di compattazione dell'elenco non ricorsivo", Comm. ACM , 13 (11): 677-678, 1970.

In un garbage collector di copia hai un pezzo di memoria che stai cercando di raccogliere, chiamato from-space , e un pezzo di memoria che stai usando per le copie chiamate to-space . Lo spazio spaziale è organizzato come una coda con un scanpuntatore che punta al record più vecchio copiato ma non scannerizzato e un freepuntatore che punta alla successiva posizione libera nello spazio. L'immagine di questo dal documento di Wilson è:

Esempio di algoritmo di Cheyney

Mentre scansionate ogni elemento nello spazio, copiate i suoi figli dallo spazio al freepuntatore nello spazio, quindi cambiate il puntatore al figlio dallo spazio alla nuova copia del bambino nello spazio. C'è un trucco in più che devi usare quando le tue strutture dati non sono alberi (quando un bambino può avere più di un genitore). In tal caso, quando si copia un figlio dallo spazio allo spazio, è necessario sovrascrivere la versione precedente del figlio con un puntatore di inoltro alla nuova copia del figlio. Quindi, se mai scansionerai un altro puntatore alla vecchia versione del bambino, ti rendi conto che è già stato copiato e non copiarlo di nuovo.


In realtà, come spiegato nella mia risposta, sia Mark + Sweep che Copy collection sono lo stesso algoritmo grafico astratto. La raccolta MS e Copy differisce solo nel modo in cui vengono implementati i set utilizzati dall'algoritmo astratto ed entrambe le famiglie sono incluse, con molte varianti, in una combinazione delle tecniche di implementazione del set descritte nella mia risposta. Alcune varianti di GC in realtà mescolano MS e Copy nello stesso GC. Separare MS e Copy è visto da alcuni come un modo conveniente per strutturare i libri, ma è una visione arbitraria e, a mio avviso, obsoleta.
babou,

@babou: se si utilizza un algoritmo di copia in cui verrà copiato tutto ciò che viene visitato (lento, ma può essere utile su piccole piattaforme in cui il set di lavoro non è mai così grande), alcuni algoritmi potrebbero essere in qualche modo semplificati poiché si può usare la memoria precedentemente occupata da un oggetto trasferito come blocco per appunti. Si può anche ottenere una capacità limitata di fare in modo che altri thread eseguano accessi in sola lettura agli oggetti durante la raccolta, a condizione che si controlli la validità degli oggetti prima e dopo ogni lettura e si segua il puntatore di inoltro se un oggetto è stato spostato.
supercat

@supercat Non sono sicuro di quello che stai cercando di dire, qual è il tuo intento. Alcune delle tue dichiarazioni sembrano corrette. Ma non capisco come si possa usare from-space prima che il ciclo GC sia terminato (contiene puntatori di inoltro). E sarebbe un gratta e vinci per cosa? Semplificare l'algoritmo come? Per quanto riguarda più thread di mutatori eseguiti durante l'esecuzione di GC, si tratta in gran parte di un problema ortogonale, sebbene possa avere un impatto grave sull'implementazione. Non tenterei di affrontarlo nei commenti. Dovrebbe sollevare meno problemi nell'accesso in sola lettura, ma il diavolo è nei dettagli.
babou,
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.