Gestione dello
HEAP
V. Carrega, A. Gorziglia, A. Rossato
Schema della memoria
In generale si puo’ pensare che ogni applicazione abbia a
disposizione una parte di memoria centrale cosi’ ripartita:
Problemi diversi per linguaggi diversi
• Lisp.
Permette solamente di allocare blocchi di dimensione fissa e
omogenei: due campi di egual dimensione permutabili in
puntatori o valori base.
E’ un linguaggio in cui e’ interessante analizzare il problema
del recupero memoria disponibile e gli algoritmi di garbage
collection.
Problemi diversi per linguaggi diversi
• C e Pascal.
La gestione dello heap e’ demandata all’utente e viene concessa
piena liberta’ di memorizzare blocchi eterogenei e di
dimensioni arbitrarie.
Es (in C):
...
malloc(p);
...
free(p);
Es (in Pascal):
...
new(p);
...
dispose(p);
Problemi diversi per linguaggi diversi
• Java.
In questo linguaggio la gestione dello heap e’ trasparente
all’utente.
Es:
...
p = new classe1(...);
q = new classe2(...);
p=q;
La possibilita’ che offre di allocare blocchi di dimensione
differente ci da’ modo di analizzare un altro problema: la
frammentazione della memoria e relativa compattazione.
Recupero memoria: reference counter
Esempio di struttura dinamica di un programma in Lisp
Recupero memoria: reference counter
• per ogni cella si memorizza un
contatore intero che da’ il numero di
oggetti che puntano ad essa
• ogni volta che viene modificato un
puntatore si aggiorna il contatore delle
celle interessate
• se un contatore, per una cella, va a 0
essa si rimette nella lista dei blocchi
disponibili
Recupero memoria: reference counter
limite del reference counter
Recupero memoria: garbage collector
Utilizzando il Pascal costruiamo una struttura dati che
rappresenti lo heap con le nostre semplificazioni:
Var
source: ^ celltype;
memory: array[1..memsize] of celltype;
Recupero memoria: garbage collector
Recupero memoria: garbage collector
Il limite dell’algoritmo visto e’ che non tiene conto di
un problema fondamentale:
quando si deve eseguire il Garbage
Collector la memoria e’ satura e dunque non
c’e’ spazio per allocare chiamate ricorsive.
Recupero memoria: GC non ricorsivo
• Analogamente a prima ricreiamo la
situazione tipica della gestione dell'heap
aggiungendo
pero'
un
campo
enumerazione {L,R} (occupa un bit) ad
ogni oggetto nell'Heap.
• L'algoritmo e' sostanzialmente il
medesimo
ma
esegue
la
DFS
iterativamente utilizzando due soli
puntatori: previous e current.
Recupero memoria: GC non ricorsivo
DFS non ricorsiva:
1. INIZIALIZZAZIONE:
•
•
current = source
previous = nil
Per ogni nodo N appartenente all'albero con radice source si eseguono:
2. ADVANCE (se il nodo N ha puntatore non nullo a sinistra)
•
•
•
•
marchiamo a L il campo enumerazione di N
facciamo puntare il Left Pointer di N a previous
facciamo puntare previous a N
facciamo puntare current al figlio sinistro
Recupero memoria: GC non ricorsivo
Recupero memoria: GC non ricorsivo
DFS non ricorsiva:
3.
SWITCH: se abbiamo finito di visitare il figlio
sinistro ed il nodo N ha figlio destro:
•
•
•
•
4.
marchiamo a R il campo enumerazione di N
il Right Pointer di N assume il valore del Left Pointer
facciamo puntare il Left Pointer di N al current
facciamo puntare current al figlio destro (ci eravamo salvati
l'indirizzo in una var temp.)
RETREAT: se abbiamo finito di visitare il figlio sx e
dx di N:
•
•
•
facciamo puntare il Right Pointer di N a current
facciamo puntare current a N usando previous
facciamo puntare previous al padre di N (ci eravamo salvati
l'indirizzo in una var temp.)
Recupero memoria: GC non ricorsivo
Variante:
Algoritmo di Garbage Collection per Heap ad oggetti
di dimensione omogenea non ricorsivo che non usa il
bit in piu'.
Si nota che l'informazione L/R e' stata codificata
insieme al pattern.
Algoritmi piu’ completi saranno trattati nel seminario
di Mura e Pastorino.
Frammentazione della memoria
Frammentazione della memoria
Fusione di blocchi liberi contigui
Fusione di un blocco libero (S) con il blocco libero alla sua dx.
Ci sono tre tecniche possibili:
1) - scorrere la lista dei blocchi liberi finche' trovo un blocco B
che punta al blocco D (il suo ind. e’ qullo di S + la sua
dim.)
- sommare al count del blocco S la dimensione di D
- far puntare B all'indirizzo puntato da D.
2) Come 1) ma usando blocchi che puntano al precedente nella
lista dei blocchi liberi
•
•
3)
Minor tempo di esecuzione
Maggiore occupazione di spazio
Come 1) usando una lista di blocchi liberi ordinata per
posizione fisica
•
Inserimento/disinserimento costoso
Fusione di blocchi liberi contigui
Fusione di blocchi liberi contigui
Fusione di un blocco libero (D) con il blocco libero alla sua sx.
Ci sono tre tecniche possibili:
1) - scorrere la lista dei blocchi liberi fino a trovare il blocco S
- applicare al blocco S la fusione a dx
2) Mettendo in ogni blocco un puntatore al blocco fisicamente
alla sua sinistra, S viene trovato in tempo costante, poi si
applica la fusione a dx
•
3)
Maggiore occupazione di spazio
Mantenedo una lista delle posizioni dei blocchi liberi
ordinata per posizione fisica, si puo’ trovare S in un tempo
minore non occupando altro spazio
•
Inserimento piu’ costoso
Tecniche di deframmentazione
1)
2)
3)
Ogni volta che si libera un blocco si mette nella lista dei
blocchi liberi, in modo da mantenerla ordinata e poi si fonde
coi blocchi contigui
Ogni volta che si libera un blocco si mette nella lista dei
blocchi liberi doppiamente linkata e poi si fonde coi blocchi
contigui. In piu' si usa in ogni blocco un puntatore al blocco
contiguo a sx per ottimizzarne la fusione.
Solo al momento della richiesta di nuovo spazio si scandisce
la memoria e si genera una nuova lista di blocchi liberi.
Nei casi reali la tecnica piu’ efficiente e’ la 3) in quanto pur
non ottimizzando le operazioni le esegue solo quando ce n’e’
bisogno
Evitare la frammentazione
Problemi:
•
quale blocco libero selezionare
•
che parte di tale blocco usare
Soluzioni:
•
Il secondo problema si risolve in quanto, mettendo i nuovi
dati in fondo al blocco di spazio libero, si risparmiano le
operazioni di aggiornamento dei puntatori
•
Per il primo problema abbiamo due strategie:
•
•
FIRST FIT: Si scandisce la lista dei blocchi liberi e ci si ferma non appena
si trova un blocco che possa ospitare i dati da allocare
BEST FIT: Si scandisce tutta la lista dei blocchi liberi e si mettono i nuovi
dati nel blocco piu’ piccolo che li contenga
Buddy System
•
Strategia per mantenere l'heap deframmentato ed ottimizzare
la fusione di blocchi contigui
•
Si ottiene restringendo le possibili posizioni e dimensioni dei
blocchi liberi:
•
•
LE DIMENSIONI dei blocchi possono essere solo 2^i con i=1..n tc 2^n e'
la dimensione dell'heap
LA POSIZIONE di un blocco di dimensoni 2^i puo' essere solo un
multiplo di 2^i
Buddy System
Buddy System
Passi per allocare spazio:
1 – all'inizio si suppone che l'heap sia un enorme blocco libero di
dimensione 2^n;
2 – per allocare un dato di dimensione k tc 2^(i-1) < k <=2^i si
scorre la memoria negli indirizzi multipli di 2^i fino a trovare
un blocco libero
SE si trova
ALLORA si alloca il dato
ALTRIMENTI si scandisce la memoria a multipli di 2^(i+1) ed
una volta trovato il blocco si divide in due, in questo modo
durante la nuova scansione per blocchi grossi 2^i si trovera'
un blocco libero
Buddy System
Fusione di blocchi contigui:
con due blocchi contigui di dim. 2^i si cancellano le
informazioni del blocco a dx e si aggiorna il BLOCK ID sx.
Scorrendo ora la memoria a blocchi di dimensione 2^i il
blocco a dx non risulta piu‘, mentre ne risulta uno grosso il
doppio scorrendo la memoria a blocchi di dim. 2^(i+1).
Buddy System
Questa politica però in generale spreca molto spazio:
i metodi precedenti dunque non si escludono affatto
dalle applicazioni pratiche, dando modo di analizzare
un ulteriore problema...
Compattazione della memoria
Anche se lo spazio libero e‘ sufficiente, puo’ essere diviso in piu'
segmenti non contigui.
Ci sono due strategie per risolvere questo problema:
1-
Ogni dato da allocare puo' essere memorizzato in piu'
blocchi, ognuno della stessa dimensione e composto da
uno spazio per il dato e uno spazio per il puntatore al
prossimo blocco dove e‘ memorizzata l'altra parte del
dato (il puntatore sara' nullo per l'ultimo blocco)
2-
Una volta che ci si trova nella situazione di
frammentazione si spostano i blocchi occupati a sinistra
nello heap, in modo da avere tutto lo spazio libero a
destra.
Compattazione della memoria
Compattazione della memoria
Schema di possibile algoritmo di compattazione:
1- scandire tutti i blocchi, sia liberi che occupati, da sinistra a
destra
2- tenere il conto della quantita' di spazio libero
3- per ogni blocco occupato calcolare il "forwarding address",
sottraendo lo spazio libero che il blocco ha a sinistra al suo
indirizzo. Questo indirizzo rappresenta la posizione in cui
finira' il blocco con la compattazione.
4- per ogni puntatore ad un qualche blocco B, rimpiazzare il
puntatore con il "forwarding address" trovato nel blocco B
5- spostare tutti i blocchi nelle posizioni indicate dai
"forwarding address"
Compattazione della memoria
Algoritmo di Morris
• permette di spostare i blocchi senza usare il
"forwarding address", esso richiede pero' un bit di
endmarker per ogni puntatore ed uno per ogni
blocco.
•
creare una catena di puntatori che parte da una
posizione fissa in ogni blocco occupato e linka tra di
loro tutti i puntatori al blocco stesso.
Compattazione della memoria
Compattazione della memoria
Consideriamo un puntatore p a un blocco B:
12-
3-
4-
Se l'endmarker nel blocco B e' 0, allora p e' il primo puntatore
trovato che punta a B
Si fa in modo che B punti a p creando uno spazio per questo
puntatore togliendo dei dati che salveremo nella parte di p che
prima puntava a B
Settiamo l'endmarker di B a 1 e quello di p a 0. Ora
consideramo come blocco B il puntatore p e come p il
prossimo puntatore a B
Iteriamo il procedimento finche' non otteniamo una catena di
puntatori per ogni blocco. L'ultimo di questi conterra' i dati
tolti a B
Compattazione della memoria
56-
7-
Spostiamo i blocchi a sinistra come visto precedentemnte
per ogni blocco scandiamo la sua catena di puntatori, facendo
in modo che ognuno punti al blocco nella nuova posizione
Quando incontriamo la fine della catena, recuperiamo il dato
di B contenuto nell'ultimo puntatore e settiamo l'endmarker di
b a 0.
Compattazione della memoria
Problemi Odierni
Le problematiche viste sono state semplificate e comunque non
trattano alcuni problemi presenti oggigiorno:
•
i record possono essere eterogenei e dunque richiedereanno
informazioni che il GC dovra‘ usare per accedere ai vari
campi
•
•
i record possono avere a loro volta dei puntatori e questo
complica un pochino gli algoritmi di deframmentazione
•
•
noi abbiamo trattato tutti algoritmi di GC che prevedevano blocchi
omogenei sia come dimensioni che struttura
noi non abbiamo visto cio', comunque non e' una complicazione
esclusivamente meccanica ed e' relativamente semplice espandere gli
algoritmi in questa direzione
i record possono essere oggetti
•
in realta' sono anch'essi record con campi specializzati dunque le
considerazioni da fare sono simili al punto 2
Problemi Odierni
Ci si trova inoltre ad affrontare problemi dovuti alla gestione di
oggetti/record molto grandi (spesso sono componenti di Data
Bases) ed alla loro gestione in programmazione concorrente:
•
esecuzione dei GC mentre le applicazioni lavorano in quanto,
vista la dimensione degli oggetti questa operazione e' molto
costosa
•
•
protezione dei dati da eventuali crash del sistema
•
•
gli algoritmi visti non si occupano minimamente di cio‘
minimizzazione costo dell'I/O nel caso di oggetti su disco
•
•
finora supponevamo di bloccare una applicazione per gestire l'heap
finora ci siamo occupati solo di memoria principale
gestione della concorrenza
problematiche derivano da:
•
•
medesimo
heap
queste
Uso dei thread cioè processi che condividono la medesima memoria
Programmazione distribuita (trattata nel seminario di Mura e Pastorino)