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)