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): heap: zona di memoria usata per gestire allocazioni esplicite di memoria (con blocchi a dimensione fissa o variabile) le operazioni di deallocazione della memoria sono fatte nello stesso ordine di quelle di allocazione; no gestione LIFO
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
utilizzando direttamente la lista libera: 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
x (non locale in pippo) è quella dichiarata nel blocco B, non in C 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); 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); 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