Garbage collection - WinDizio

Transcript

Garbage collection - WinDizio
http://www.windizio.altervista.org/appunti/
File distribuito con licenza Creative Commons BY­NC­SA 2.5 IT
Copyright © 2007 ­ Michele Tartara
Parte I
Garbage collection
Si deniscono garbage i record allocati nello heap che non siano raggiungibili a partire dallo stack per mezzo di almeno una
catena di puntatori.
Il processo di recuperare tali record e renderli disponibili per l'allocazione di nuovi record, si chiama garbage collection, e non
viene eseguito dal compilatore, ma a runtime, dai programmi di supporto collegati al codice compilato (in quanto, ovviamente,
i record presenti in memoria dipendono dai dati che sono stati inseriti dall'utente).
Per individuare il garbage, l'approccio ideale sarebbe usare la denizione dinamica della liveness. Tuttavia, non essendo
questa computabile, ci dobbiamo basare sull'approssimazione conservativa della raggiungibilità del record. Il compilatore dovrà
garantire che ogni variabile viva è raggiungibile e, al tempo stesso, minimizzare il numero di variabili raggiungibili che non sono
vive.
1 Mark And Sweep
È uno dei metodi usati per eseguire la garbage collection. Si interrompe l'esecuzione del programma e si eseguono le due fasi
seguenti:
Mark Si costruisce tramite un algoritmo di tipo Depth First Search il grafo dei record raggiungibili. Si considerano come radici
del grafo le variabili allocate sullo stack. A partire da queste, seguendo le catene di puntatori, si individuano e segnano
(mark ) tutti i record raggiungibili.
Sweep L'intero heap viene scandito, dal primo all'ultimo indirizzo. Tutti i record non marcati sono da considerare garbage
(in quanto non raggiungibili) e vengono quindi aggiunti alla freelist (una lista a puntatori - linked list - contenente tutti
i record liberi e pronti all'uso). I record marcati vengono conservati e gli viene tolta la marcatura, per prepararli alla
prossima garbage collection.
Al termine delle due fasi, l'esecuzione del programma viene ripresa.
Un buon momento per eseguire una nuova garbage collection è quando la freelist si svuota.
1.1 Costo
Il tempo di esecuzione dell'algoritmo DFS è proporzionale al numero R di nodi segnati (cioè dei record raggiungibili) ed è
proporzionale alla dimensione dello heap H.
L'esecuzione di una garbage collection è quindi c1 R + c2 H .
Il costo per unità di spazio libero sullo heap, o costo ammortizzato di raccolta, è dato dal costo di esecuzione diviso per il
R+c2 H
numero H − R di parole di memoria libere, cioè c1 H−R
.
Se la memoria è quasi piena (R ' H ) il costo è molto elevato. Se la memoria è quasi vuota (H À R), il costo è molto ridotto.
Se al termine del processo di collection R/H è maggiore di 0.5 (o un'altro valore, secondo qualche criterio) il collector dovrebbe
chiedere al sistema operativo di fornire maggiore spazio per lo heap del programma.
1.2 Ottimizzazioni
1.2.1 Stack esplicito
Se si usa un algoritmo DFS standard (ricorsivo), si potrebbe arrivare ad avere H ricorsioni, e quindi H frame sulla pila dei record
di attivazione, ossia il garbage collector occuperebbe più memoria dello HEAP stesso. Ciò è decisamente da evitare, perciò si
preferisce utilizzare un algoritmo iterativo in cui si gestisce esplicitamente una struttura a pila su cui salvare i record marcati.
In tal modo, si otterrà ugualmente una pila di H elementi, ma saranno singole word invece che record di attivazione.
1.2.2 Inversione dei puntatori (pointer reversal)
Per ridurre ulteriormente lo spazio occupato, si può osservare che, dopo che il contenuto di un campo x.fi di un record x è stato
salvato sullo stack, è possibile riutilizzare lo spazio x.fi . In particolare, è possibile utilizzarlo per salvare il puntatore al record a
partire dal quale x era stato raggiunto.
Secondo questo ragionamento, è quindi possibile utilizzare lo stesso grafo dei puntatori per salvare anche lo stack, facendo
sì che, mentre si stanno esaminando i campi di un record (e i campi dei record a cui questi, a loro volta, puntano), ogni record
1
http://www.windizio.altervista.org/appunti/
File distribuito con licenza Creative Commons BY­NC­SA 2.5 IT
Copyright © 2007 ­ Michele Tartara
che si sta esaminando punti al proprio predecessore (cioè al record da cui è puntato) invece che al proprio successore (il record
a cui punta normalmente). Ogni volta che tutti i campi di un record sono stati esaminati, i suoi puntatori vengono ripristinati,
ricostruendo il grafo originale.
Tutto ciò avviene secondo il seguente algoritmo:
function DFS(x)
if (x è un puntatore e il record a cui punta non è marcato)
imposta root come predecessore (t)
marca x
while true
if (c'è ancora un campo x.fi (contenente il valore y ) da esaminare nel record x)
if (y è un puntatore e il record puntato da y non è marcato)
x.fi ← t //fai puntare il campo al suo predecessore (pointer reversal)
t ← x //x è il predecessore del record che stiamo esaminando ora
x ← y //il record corrente (che stiamo esaminando) è quello puntato da y
marca il record corrente x
else
y ← x //salva in y l'indirizzo del record che stavamo esaminando
x ← t //torna ad esaminare il predecessore
if (x=root) tutto il grafo è stato visitato. Fine.
t ← x.fi //il nuovo predecessore è quello il cui valore era stato salvato dall'inversione
x.fi ← y //ripristino del puntatore originale (fine del pointer reversal)
1.2.3 Array di freelist
Per ridurre la frammentazione della memoria libera si può utilizzare, invece di un'unica freelist, un array di freelist, ognuna delle
quali corrisponde ad una certa dimensione dei record ivi contenuti. In tal modo, sarà possibile utilizzare il blocco di memoria
più piccolo di dimensioni sucienti a contenere la variabile da allocare.
Se non ci sono blocchi sucientemente piccoli, è possibile usare il più piccolo disponibile e sprecare una parte dello spazio (se
si vuole mantenere la dimensione dei blocchi), oppure suddividerlo e reimmettere nell'opportuna freelist la parte di spazio non
utilizzata.
La creazione delle diverse freelist è demandata alla fase sweep dell'algoritmo.
L'uso di array di freelist rende anche più veloce anche l'allocazione di nuove variabili nello heap, perchè l'allocatore non dovrà
cercare sequenzialmente in tutta la freelist un blocco di dimensioni sucienti, ma potrà direttamente andarlo a richiedere alle
freelist di dimensioni opportune.
Frammentazione esterna sono presenti tanti piccoli record liberi di dimensione insuciente per l'allocazione che si vuole
eseguire.
Frammentazione interna non è possibile trovare spazio libero a sucienza perchè questo è presente, inutilizzato, all'interno
di record troppo grandi già allocati.
2 Conteggio dei riferimenti (Reference count)
Assieme ad ogni record p, viene memorizzato un numero (reference count ) che indica quanti puntatori x.fi a quel record sono
presenti.
Il compilatore emette istruzioni per far sì che ogni volta che un nuovo x.fi punta a p, il reference count di p viene incrementato,
mentre viene decrementato quello di r a cui puntava in precedenza.
Se decrementando il reference count di r questo assume il valore zero, r viene inserito nella freelist.
Se esistono dei record a cui r puntava, il loro refcount deve essere a sua volta diminuito di uno. Ciò viene fatto non subito,
ma al momento (successivo) in cui r verrà rimosso dalla freelist, per i seguenti motivi:
1. viene diviso in più parti il decremento ricorsivo, rendendo più uida l'esecuzione del programma
2. si risparmia spazio: il codice per il decremento ricorsivo è unico (nell'allocatore). Il codice per vericare se refcount ha
raggiunto zero, tuttavia, deve comunque essere ripetuto per ogni decremento.
2
http://www.windizio.altervista.org/appunti/
File distribuito con licenza Creative Commons BY­NC­SA 2.5 IT
Copyright © 2007 ­ Michele Tartara
2.1 Problemi
1. Non si può rimuovere garbage con riferimenti ciclici: se due record non sono raggiungibili, ma puntano l'uno
all'altro vicendevolmente, il loro reference count rimarrà a uno e non potranno essere rimossi.
Soluzioni
(a) Richiedere esplicitamente che il programmatore elimini tutti i cicli quando ha nito di usare una struttura
(b) Combinare Reference Count con qualche Mark&Sweep, per rimuovere i cicli
2. La gestione dei reference count è computazionalmente pesante! Ogni operazione di aggiornamento di un puntatore
implica l'incremento di un reference count, il decremento di quello della variabile puntata precedentemente e le operazioni
per la verica del raggiungimento di refcount=0. È possibile attuare qualche ottimizzazione tramite analisi dataow, ma
il peso è comunque elevato.
3 Collection tramite copia (Copying collection)
Lo heap viene diviso in due parti, chiamate from-space e to-space. Tutti i dati sono nel from-space. Il to-space è vuoto.
Attraversando il grafo dei record raggiungibili, si copiano man mano tali record nel to-space, occupando senza frammentazione
i primi blocchi di memoria attigui. La copia nel to-space si dice quindi compatta.
I nodi radice (le variabili dello stack) vengono quindi fatti puntare ai dati del to-space, rendendo il from-space non più
raggiungibile.
Alla successiva esecuzione dell'algoritmo, il from-space e il to-space saranno invertiti.
Nel from space sono presenti due puntatori: next indica il punto in cui inizia la parte di memoria libera (e quindi dove, su
richiesta, verranno allocate le prossime aree di memoria), limit indica il termine dello spazio disponibile.
Quando next raggiunge limit, viene eseguita la collection. Durante il normale funzionamento del sistema, next può solo
crescere: viene ridotto unicamente dall'esecuzione della collection.
Quando si comincia l'esecuzione di una collection, next punta alla base del to-space, e viene incrementato di size(p) ogni
volta che un record p viene copiato.
Forwarding È il nome dell'operazione base dell'algoritmo. Si esegue il forwarding di un puntatore p quando, dato un p che
punta al from-space, lo si modica per puntare agli stessi dati nel to-space. Funziona in tre sottocasi diversi:
1. Se p punta ad un record del from-space che è già stato copiato, allora p.f1 è il forwarding pointer che indica dove si trova
la copia nel to-space. È riconoscibile come tale dall'indirizzo, che è interno al to-space.
2. Se p punta al from-space, il contenuto dell'indirizzo puntato viene copiato nel to-space e p.f1 viene fatto puntare alla nuova
locazione del record.
3. Se p non è un puntatore oppure se punta al di fuori dal from-space, il forwarding non fa nulla.
3.1 Algoritmo di Cheney
Esegue la copia tramite una visita breadth-rst. Non necessità di uno stack esterno o dell'inversione dei puntatori perchè
introduce, a anco a next, un puntatore scan che indica quali record sono stati scanditi: i record compresi tra scan e next, sono
stati copiati nel to-space ma non ancora modicati tramite forwarding (contengono riferimenti al fromspace). I record precedenti
a scan, invece, sono già stati modicati dal forwarding (contengono solo riferimenti al to-space).
Al termine del processo di collection, scan raggiunge next.
Località dei riferimenti L'uso di un algoritmo breadth rst crea problemi con la località dei riferimenti, in quanto vengono
memorizzati in aree adiacenti di memoria i record che hanno la stessa distanza dalle radici. Avrebbe invece più senso memorizzare
vicini i record che sono connessi da una catena di puntatori, in quanto è molto più probabile che si dovrà accedere ad essi in
sequenza e, per il funzionamento delle politiche di paginazione della memoria, facilmente in questo modo tali record verranno
caricati assieme riducendo il numero di page fault e cache miss, migliorando sensibilmente le prestazioni.
Una ricerca depth-rst permetterebbe di ottenere maggiore località, ma richiederebbe l'uso dell'inversione dei puntatori,
risultando quindi piuttosto lenta.
Un buon approccio è quello ibrido, che utilizza breadth-rst ma che, dopo aver copiato un oggetto, verica tramite depth-rst
se esiste qualche suo nodo glio da copiargli vicino.
3
http://www.windizio.altervista.org/appunti/
File distribuito con licenza Creative Commons BY­NC­SA 2.5 IT
Copyright © 2007 ­ Michele Tartara
Costi Il costo della garbage collection è proporzionale al numero R di record marcati (cioè raggiungibili). Essendo lo heap (di
dimensione H ) suddiviso in due aree di memoria, lo spazio libero dopo una collection è H
2 − R.
R
Il costo totale ammortizzato della collection è quindi Hc3−R
istruzioni per parola allocata.
2
Per funzionare bene, in una situazione realistica si ha H = 4R. È evidente che l'occupazione di memoria è elevata.
4 Collection generazionale (generational collection )
Riduce sia il tempo sia lo spazio usati dal metodo copying collection.
Si basa sull'osservazione che molti oggetti sono destinati a morire presto, ma se un oggetto è sopravvissuto già ad alcune
collection, è probabile che sopravviva a lungo.
Dividiamo quindi lo heap in generazioni : G0 contiene gli oggetti più giovani. Tutti gli oggetti di G1 sono più vecchi di quelli
di G0 ; gli oggetti in G2 sono più vecchi di quelli di G1 e così via.
Ogni area dovrebbe essere esponenzialmente più grande della precedente (es: G0 = 0.5M B, G1 = 2M B, G2 = 8M B ).
Un oggetto che è sopravvissuto a due o tre collection viene promosso a quella successiva.
Al momento di eseguire una collection (si può usare sia il metodo mark&sweep sia copying collection) la si esegue prima su
G0 e poi, se necessario, sulle generazioni via via successive.
Tuttavia, per trovare gli elementi di G0 , non basta guardare le variabili nello stack: potrebbero esistere anche riferimenti
all'interno di oggetti vecchi che puntano a oggetti più nuovi. Questa condizione (per fortuna rara, perchè richiede l'aggiornamento
di un campo di un oggetto vecchio) renderebbe l'analisi del solo G0 più lenta che non l'attraversamento di tutto il grafo a partire
dalle radici.
Per ovviare al problema facciamo si che il programma compilato ricordi dove ci sono puntatori da oggetti vecchi ad oggetti
nuovi, in modo da poterli trovare immediatamente. Esistono diversi metodi per fare ciò:
Remembered list viene generata una lista di oggetti di cui un campo è stato aggiornato. Al momento della garbage collection,
si cercano all'interno della lista gli oggetti che puntino dentro G0
Remembered set La remembered list potrebbe contenere duplicati di uno stesso oggetto al suo interno. Per evitare il problema,
si codica all'interno dello stesso oggetto un bit per indicare l'appartenenza o meno al remembered set.
Card marking La memoria è divisa in schede di dimensione 2k . Un oggetto può occupare una o più schede, o parte di una
scheda. Ogni volta che un indirizzo b viene modicato, la scheda contenente quell'indirizzo viene marcata (tramite un bit
in un apposito array)
Page marking Simile al precedente, ma invece di introdurre una gestione manuale delle schede, si usa la paginazione della
memoria. Quando un indirizzo viene modicato, un dirty bit viene impostato per quella pagina.
In ogni caso, all'inizio della garbage collection, il set di oggetti ricordati indica quali oggetti potrebbero contenere puntatori a
G0 . Questi oggetti assieme alle variabili dello stack fungono da root variables per l'algoritmo di ricerca scelto.
5 Incremental collection
Per ridurre il tempo di interruzione dell'esecuzione di un programma durante la garbage collection, è possibile usare algoritmi
incrementali, cioè che non conducono l'intero processo di collection in un unico passaggio, ma lo eseguono un pezzo alla volta
inframezzandosi alla normale esecuzione del programma.
In tal caso si parla di collector per l'algoritmo di collection e di mutator per il programma in esecuzione (in quanto è in grado
di modicare i dati su cui si sta lavorando).
Generalmente gli algoritmi incrementali si basano su un processo di segnatura a tre colori per stabilire l'avanzamento
dell'algoritmo. I record possono essere:
Bianchi oggetti non ancora visitati dall'algoritmo di ricerca depth-rst o breadth-rst.
Grigi gli oggetti sono già stati visitati, ma i loro gli no (è come se si trovassero sulla pila, o tra scan e next )
Neri sia gli oggetti sia tutti i loro gli sono già stati visitati e marcati (è come se fossero già stati tolti dalla pila, o se fossero
prima di scan).
A partire dalle radici, si visitano tutti gli oggetti, rendendoli prima grigi, visitando i loro gli e inne rendendoli neri. Quando
non sono più presenti oggetti grigi, tutto ciò che è rimasto bianco è garbage.
Due invarianti sono comuni a tutti gli algoritmi che si basano su questo principio:
4
http://www.windizio.altervista.org/appunti/
File distribuito con licenza Creative Commons BY­NC­SA 2.5 IT
Copyright © 2007 ­ Michele Tartara
1. Gli oggetti neri non possono puntare a oggetti bianchi
2. Ogni oggetto grigio è sulla struttura dati (pila o coda) del collector.
Il mutator, durante il suo lavoro, deve fare attenzione a non rompere tali invarianti, introducendo opportune politiche di
coloratura: ad esempio, nell'algoritmo di Dijkstra, Lamport e altri, quanto il mutator salva un puntatore bianco a dentro ad un
oggetto nero b, deve colorare a di grigio.
6 Layout dei dati
Il collector deve essere in grado di operare su qualunque tipo di dato, anche complesso. Deve quindi essere in grado di determinare
la dimensione occupata dai dati di ogni record, il numero di campi da cui è formato e se questi campi contengono o meno un
puntatore.
La prima word di un oggetto generalmente viene fatta puntare ad un record di descrizione del tipo (o della classe) che contiene
l'indicazione della dimensione e la posizione dei campi puntatore. Tale record deve essere stato creato dal compilatore grazie alle
informazioni ricavate dall'analisi semantica.
Il compilatore deve anche permettere al collector di identicare se qualunque temporary (registro o area di memoria) contiene
un puntatore. Ciò viene fatto compilando un'apposita mappa dei puntatori (pointer map ) ogni volta che potrebbe essere
necessario eseguire una collection, cioè ad ogni operazione alloc.
5