memoria gestita staticamente: allocata dal compilatore prima dell’esecuzione in una zona fissa della memoria (stabilita dal compilatore) – variabili globali – istruzioni del codice oggetto – costanti – tabelle del compilatore per il supporto a run-time del linguaggio (gestione nomi, type checking, garbage collection) – informazioni locali alle procedure (se non è supportata la ricorsione): variabili locali, parametri indirizzo di ritorno memoria gestita dinamicamente (pila a run-time); blocchi in-line A: { int a = 1; int b = 0; B: { int c = 2; int b = 3; } b = a+1; } push a; push b push c; push b pop pop record di attivazione (frame): lo spazio di memoria allocato sulla pila e dedicato a un blocco in-line (o a una procedura attivata); per i blocchi in-line contiene: – risultati intermedi (risultati senza nome esplicito) – variabili locali (interne al blocco, dimensione nota) – puntatore di catena dinamica (link dinamico o di controllo; punta al precedente RdA sulla pila) per le procedure contiene: – risultati intermedi (risultati senza nome esplicito) – variabili locali (interne al blocco e di dimensione nota non array dinamici, per esempio) – puntatore di catena dinamica (link dinamico o di controllo; punta al precedente RdA sulla pila) – puntatore di catena statica (informazioni per gestire lo scope statico) – indirizzo di ritorno, indirizzo del risultato; – parametri; i nomi sono memorizzati nel RdA ? offset rispetto all’indirizzo del blocco in cui la variabile è memorizzata gestione a run-time della pila di sistema il compilatore inserisce frammenti di codice prima o dopo la chiamata di procedura (o di un blocco) – sequenza di chiamata: codice aggiunto al chiamante ed eseguito prima della chiamata di procedura e dopo la terminazione della procedura chiamata – prologo: codice aggiunto al chiamato ed eseguito subito dopo la chiamata – epilogo: codice aggiunto al chiamato ed eseguito al termine della procedura chiamata come sono distribuite queste attività ? sequenza di chiamata (chiamante) e prologo (chiamato) – modifica del program counter (e salvataggio del vecchio valore per mantenere l’indirizzo di ritorno) – allocazione spazio sulla pila (e modifica del puntatore alla memoria libera sulla pila) – modifica del puntatore al (nuovo) RdA – passaggio dei parametri (fatto dal chiamante) – salvataggio dei registri (fra cui il vecchio puntatore al RdA) – esecuzione codice di inizializzazione epilogo (chiamato) e sequenza di chiamata (chiamante) – ripristino del program counter – restituzione dei valori (parametri o valore funzione) – ripristino registri (fra cui il vecchio puntatore al RdA) – esecuzione codice di finaizzazione – deallocazione spazio sulla pila (il RdA della procedura terminata è rimosso dalla pila) di quali strutture dati ho bisogno per implementare questa gestione? cosa succede se posso allocare e deallocare esplicitamente la memoria? memoria gestita dinamicamente (heap) int *p, *q; p = malloc(sizeof(int)); q = malloc(sizeof(int)); *p = 0; *q = 1; free(p); free(q): le operazioni di deallocazione della memoria sono fatte nello stesso ordine di quelle di allocazione; no gestione LIFO heap: zona di memoria usata per gestire allocazioni esplicite di memoria (con blocchi a dimensione fissa o variabile) heap con blocchi di dimensione fissa (lista libera) heap con blocchi di dimensione variabile se il linguaggio permette l’allocazione a run-time di memoria di dimensione variabile, la LL non è adeguata due esigenze contrastanti: – migliorare l’occupazione della memoria (frammentazione interna ed esterna) – velocizzare la gestione dello heap tecniche di allocazione: – unica lista libera – liste libere multiple unica lista libera - un unico blocco di memoria contiene l’intero heap; se è richiesta l’allocazione di n parole di memoria, il puntatore avanza di n (analogo per le richieste successive) - i blocchi deallocati sono collegati in una lista libera; quando lo heap finisce si passa allo spazio deallocato, in due modi: utilizzando direttamente la lista libera: si cerca nella lista un blocco con dimensione adeguata, seguendo due principi: first fit (miglior tempo di gestione) best fit (migliore occupazione di memoria) compattando la memoria libera: tutti i blocchi attivi spostati all’estremità dello heap (si può fare sempre?) liste libere multiple - più liste libere, ognuna delle quali con blocchi di dimensioni diverse se è richiesta l’allocazione di n parole di memoria, è selezionata la lista che contiene blocchi di dimensione appropriata - le dimensioni dei blocchi possono essere statiche o dinamiche buddy system le dimensioni dei blocchi sono potenze di 2; richiedo un blocco pari a n; se k è tale che 2kn, allora si cerca un blocco libero in lista 2k; altrimenti, si prende un blocco in 2k+1, si divide in due, uno è occupato e l’altro entra nella lista 2k; quando un blocco è liberato, si cerca il suo compagno e si riunificano heap di Fibonacci come sopra, ma con numeri di Fibonacci (che crescono di meno) implementazione regole di scope (come risolvere un riferimento non locale) scope statico o scope annidato più vicino l’ambiente in qualsiasi punto e in qualsiasi momento dipende SOLO dalla struttura sintattica del programma (il RdA collegato dal puntatore di catena dinamica NON è il record in cui cercare per risolvere un riferimento non locale) scope dinamico l’associazione valida per un nome X, in un punto P di un programma, è la più recente associazione creata (in senso temporale) per X che sia ancora attiva quando il flusso di esecuzione arriva a P scope statico: la catena statica A: { int y = 0; B: { int x = 0; void pippo(int n) { x = n+1; y = n+2; } C: { int x = 1; pippo(2); write(x); } } write(x); } scope statico: x (non locale in pippo) è quella dichiarata nel blocco B, non in C scope statico: la catena statica scope statico: la catena statica con la sequenza di chiamate A B C D E C, si ha la gestione della catena statica è fatta nella sequenza di chiamata, nel prologo e nell’epilogo; il chiamante calcola il puntatore di catena statica del chiamato e lo passa al chiamato; due casi: – chiamato all’interno del chiamante: il chiamante passa al chiamato il puntatore al proprio RdA – chiamato all’esterno del chiamante: il chiamante calcola il puntatore di catena statica deferenziando k+1 volte il proprio puntatore di catena statica, con k = distanza di livello di annidamento fra chiamato e chiamante (nota a compile-time) il chiamato riceve il puntatore di catena statica e lo memorizza nel proprio RdA la tabella dei simboli contiene - i livelli di annidamento di blocchi o procedure - in generale, i nomi usati nel programma (il tipo e il numero che indica lo scope che contiene la dichiarazione per tale nome) quindi, si può calcolare a compile-time la distanza fra lo scope della chiamata e lo scope della dichiarazione come risolvere riferimenti non locali senza fare ricerche sulla pila? basta risalire la catena statica di un numero di link pari alla distanza dichiarazione-chiamata si può risolvere un riferimento non locale completamente a compile-time? no, non sappiamo staticamente il numero di RdA sulla pila scope statico: la catena statica A: { int y = 0; B: { int x = 0; void pippo(int n) { x = n+1; y = n+2; } C: { int x = 1; pippo(2); write(x); } } write(x); } il compilatore sa che per usare y deve risalire di due blocchi; basta memorizzare a compile-time questo numero e usarlo a run-time. scope statico: il display nome non locale, dichiarato in un blocco esterno di k livelli k accessi alla memoria (a run-time) per scorrere la catena statica e trovare il RdA in cui è dichiarato display (riduce a 2 il numero di accessi): – vettore di tanti elementi quanti sono i livelli di annidamento dei blocchi – l’elemento k-esimo del display contiene il puntatore al RdA di livello di annidamento k attualmente attivo – se cerco un oggetto non locale dichiarato in un blocco di livello k, accedo alla posizione k del vettore e seguo il puntatore entrando in (o uscendo da) una procedura di livello k, occorre: – aggiornare il display in posizione k con il valore del puntatore al RdA del chiamato – salvare il vecchio valore (nel RdA del chiamato) due casi: – chiamato all’interno del chiamante: se il chiamante è a livello n, entrambi condividono il display, a cui si aggiunge un nuovo valore in posizione n+1; – chiamato esterno al chiamante: se il chiamante è a livello n e il chiamato a livello m (m<n), essi condividono il display fino alla posizione m-1; l’elemento m è aggiornato con il puntatore al RdA del chiamato, ma il vecchio valore deve essere salvato, e servirà quando il chiamato termina; scope dinamico per risolvere un riferimento a un nome x si risale la pila fino a trovare il blocco che contiene x; le associazioni nome-oggetto sono memorizzate nel RdA scope dinamico: lista di associazioni le associazioni nome-oggetto possono essere memorizzate in una A(ssociation)-list, gestita come una pila due inconvenienti: – occorre memorizzare i nomi in strutture presenti a tempo di esecuzione (al contrario dello scope statico); • • è ovvio per la A-list anche per il caso RdA, perche’ uno stesso nome può essere memorizzato in posti diversi in diversi RdA – inefficienza della ricerca a run-time tabella centrale dell’ambiente (limita i due inconvenenti): tutti i blocchi del programma fanno riferimento a una sola tabella (CRT), che contiene: – tutti nomi usati nel programma, e per ogni nome – un flag che indica se l’’associazione per il nome è attiva – un puntatore alle informazioni sull’oggetto – i nomi hanno posizioni fisse in tabella (perché tutti gli identificatori usati nel programma sono noti), quindi l’accesso a run-time è costante – se dal blocco A si entra in B, la CRT è modificata per descrivere il nuovo ambiente di B; le associazioni deattivate devono essere salvate (per quando esco da B) – a ogni nome della CRT è associata una pila che contiene nella prima posizione l’associazione valida e (nelle successive) quelle disattivate – oppure si usa una sola pila nascosta per memorizzare tutte le associazioni deattivate per ogni nome usando la CRT (con pila nascosta o no) per conoscere una associazione dell’ambiente occorre: – accedere alla tabella (diretto) – accedere alla memoria con il puntatore trovato nella tabella – non serve nessuna ricerca a run-time