Structure of a Compiler Scanner Parser Semantic Analyzer Code Generator Optimizer SYMBOL TABLE 2 Ruolo della tabella dei simboli La tabella dei simboli una delle pi importanti strutture dati di un compilatore Creata dal front-end per contenere le informazioni sul programma oggetto IR si riferisce agli oggetti tramite puntatori alle voci corrispondenti nella tabella di simboli Questo fatto fin quasi alla fine della compilazione Cos la tabella dei simboli utilizzata durante lintera trasformazione Il contenuto pu essere modificato nel processo di ottimizzazione Scope Una (sintatticamente definita) allinterno di un programma nella quale gli identificatori (I nomi) possono essere dichiarati.. E.g. funzioni, istruzioni blocco. Gli scope possono essere innestati in alcuni linguaggi 3 Alcune considerazioni .. Static scope in linguaggi strutturati a blocchi Lo scope di un identificatore include il blocco di definizione e i blocchi in esso contenuti che non contengono una ulteriore dichiarazione dellidentificatore Nested declaration di funzioni Direttive per la promozione ad un scope globale Block Scoping of Variables { int x, y, z ; x y z /* some code */ { int y ; . x y z . /* some more code */ { int z ; x = y + z ; */ In generale: al punto p quale dichiarazione di x valida? Come il compilatore tiene traccia di ci? 4 Block Scoping of Variables Blocchi nested Se la variabile x utilizzata nel blocco B e dichiarata in B allora la sua dichiarazione utilizzata. Altrimento se x dichiarata nel blocco C esterno a B ma non in nessun altro blocco allinterno di C ed esterno a B viene utilizzata la dichiarazione di x in C { /* this is block C */ int x, y, z ; x y z /* some code */ { /* this is block D */ int y ; . x y z . /* some code */ { /* this is block B */ int z ; x = y + z ; /* which ones are used??? */ Ogni differente variabile richiede una propria entry nella symbol table. Cio avremo pi entri per x etc. Scope of Variable Declaration Static Scope Lo scope determinato dalle relazioni di nesting del programma sorgente, e.g. C, JAVA, Pascal. Dynamic scope Lo scope determinato dalle relazioni che scaturiscono dalle chiamante a run time. E.g. Lisp. 5 Lexical vs. Dynamic Scoping program whichone (input , output ) ; var r : real ; procedure show ; begin write ( r : 5:3 ) end ; procedure small ; var r : real ; begin r := 0.125 ; show end ; begin r := 0.25 ; show ; small ; writeln ; end Questo programma chiama show ; small ? Lexical vs. Dynamic Scoping program whichone (input , output ) ; var r : real ; procedure show ; begin write ( r : 5:3 ) end ; procedure small ; var r : real ; begin r := 0.125 ; show end ; begin r := 0.25 ; show ; small ; writeln ; end output? ? Con static scoping, 0.250 0.250. Con dynamic scoping, 0.250 0.125. 6 Lifetime of Variable Lifetime is the time during execution of a program when a given variable first becomes visible to when it is last visible. Also called extent. The lifetime of a global variable covers the entire execution (unless it is temporarily superseded in some places). The lifetime of a local or automatic variable is usually an activation of the program unit within which it is declared. Some languages permit variables to be local to blocks also. Lifetime and Scope Examples In C, scope of automatic variable is procedure or block where it is declared to end of program unit In PL/1 scope encompasses entire relevant program unit In Pascal, a variable in outermost scope is visible everywhere in program except where another variable with same name is declared (and in the routines it contains) Fortran has common blocks, static memory that is visible in all routines where it is declared. Scope encompasses all those routines. Local static variables in C and SAVEd variables in Fortran have global lifetime but are only visible in certain regions, so scope may be a file or a procedure Dynamic variables have a lifetime that extends from their point of allocation to the point(s) of their destruction. 7 Interfaccia della tabella dei simboli openScope() closeScope() enterSymbol(nome, tipo) retrieveSymbol(nome) declaredLocally(nome) void builtSymbolTable () {processNode(ASTroot)} void processNode(node) { switch (kind(node) { case block: {symtab.openScope(); break;} case def: {symtab.enterSymbol(node.name, node.type); break;} case ref: {symb = symtab.retriveSymbol(node.name) if (symb == null) errore; break:} } foreach (c : node.getCildren() ) processNode(c) If (kind(node) == block) symtab.coseScope(); } 8 Contenuto della tabella dei simboli La tabella dei simboli memorizza informazioni su: Identificatori Etichette Valori numerici (costanti) Stringhe di caratteri Variabili generate temporaneamente dal compilatore Le tabelle di simboli possono essere possono separate (una per ogni scope o una singola Linguaggi struttuturati a blocchi e scope Regole comuni I nomi accessibili ad un certo punto di un programma sono quelli definiti nel current scope e tutti quelli definiti negli open scope Se un nome dichiarato in pi di un scope valido quello nello scope pi interno (innermost scope) Le nuove dichiarazioni possono essere fatte solo nel current scope. Regole singolari possibile accedere a defiizioni in outrmost global scope (extern in C, public static field in Java) 9 Una o pi tabelle dei sinboli Una tabella per ogni scope Gli scope sono aperti e chiusi con una strategia LIFO Svantaggi: pu essere necessario ricercare in pi tabelle un nome Ununica tabella Insieme al nome viene inserito nella tabella il nome dello scope o il livello di profondit dello scope Pu essere utilizzato uno stack per ogni nome comune, la ricerca di un nome si limita alla ricerca nel top della specifica pila Tecniche delle implementazioni Lista semplice non ordinara: compatta ma lenta in pratica O(n) complessita delle ricerca O (1) complessita dellinserimento Lista semplice ordinata: semplice, buona se le tabelle sono note in anticipo O(log n) complessita delle ricerca binaria O(n) complessita dellinserimento Binary search tree: approccio meno semplice O(log n) complessita media delle ricerca O(n) worst case Ol(og n) complessita dellinserimento Albero binario bilanciato Con un albero bilanciato otteniamo i tempi previsti Su un ingresso arbitrario l'albero non necessariamente bilanciato Che cosa succede se le variabili sono in ordine alfabetico Utilizzare un algoritmo di bilanciamento approssimativa Complica l'attuazione Spazio overhead direttamente proporzionale al numero di elementi nella tabella O 10 Implementazione della tabella dei simboli Implementazioni semplici basare su array sono troppo semplici nei casi reali Lapproccio pi comune usa un hashing aperto (open hashing) La funzione Hash basata su identificativo Un numero di funzioni sono conosciute Gli entry della tabella dei simboli usualmente puntano ad altre strutture per salvare stringhe di caratteri (nome della variabile, const char ) Consente la stessa dimensione per ogni entry Hash table O(1) complessita media delle ricerca O(1) complessita media dellinserimento Caso peggiore molto improbabile Tale approccio adottato nella maggior parte dei compilatori O(n + m) spazio dove n il numero di simboli e m il numero di voci della tabella di hash 11 TABELLE HASH Le tabelle hash sono strutture di dati che consentono la ricerca, linserzione e in certi casi la cancellazione di n chiavi in tempomedio costante, e tempo pessimo (n). Poich tutte le chiavi sono sequenze binarie, esse possono essere interpretate come numer interi. Dimensionare la tabella in base al numero di elementi attesi ed utilizzare una speciale funzione (funzione hash) per indicizzare la tabella Una funzione hash h ha come dominio linsieme C di tutte le possibili chiavi, e come codominio linsieme {0, ..., m-1} per un dato intero m. Essa trasforma quindi ogni possibile chiave K di C in un indirizzo hash h(K) tra 0 e m-1, inteso come posizione per K in un array A[0..m-1]. Idealmente la chiave K sar memorizzata in A[h(K)]. Una funzione hash quindi una funzione che data una chiave k e C restituisce la posizione della tabella in cui lelemento con chiave k viene memorizzato h : C [0, 1, . . .m 1] N.B: la dimensione m della tabella pu non coincidere con la |C|, anzi in generale m < |C| 12 Lidea quella di definire una funzione daccesso che permettadi ottenere la posizione di un elemento in data la sua chiave Con lhashing, un elemento con chiave k viene memorizzato nella cella h(k) Pro riduciamo lo spazio necessario per memorizzare la tabella Contro: perdiamo la corrispondenza tra chiavi e posizioni in tabella le tabelle hash possono soffrire del fenomeno delle collisioni Le chiavi possibili sono in genere moltissime e non note a priori (per esempio i codici di login in un sistema, o le variabili che un utente inserir in un programma, o i cognomi dei clienti di una ditta), quindi si ha |C| >> m. Dunque estremamente complesso definitre una funzione hash che grarantisca una corrispondenza biunivoca fra chiave e indirizzo hash. Cio e difficile avere la garanzia che K1 = K2 h(K1) = h(K2) Nasce quindi un problema di collisioni: (K1 = K2 si pu avere h(K1) = h(K2)) per solo una delle chiavi (tipicamente la prima che si presentata, diciamo K1) potr essere allocata in A[h(K1)], e le altre saranno poste altrove. 13 Collisioni Due chiavi k1 e k2 collidono quando corrispondono alla stessa posizione della tabella, ossia quando h(k1) = h(k2) Soluzione ideale: eliminare del tutto le collisioni scegliendounopportuna (= perfetta) funzione hash. Una funzione hash si dice perfetta se iniettiva, cio se per ogni k1, k2 e C K1 = K2 h(K1) = h(K2) Deve essere |U|<=m Se |U| > m, evitare del tutto le collisioni impossibile (Ad es.supponente di dover inserire un nuovo elemento in una tabella piena) Problemi da affrontare Dobbiamo quindi affrontare tre problemi: 1. scegliere la dimensione m; 2. Scegliere la funzione h; 3. Risolvere le collisioni. 14 Scelta di m e definizione del fattore di carico In ogni istante si indica con n il numero di chiave presenti nella tabella: dunque n varia durante lapplicazione mentre la dimensione m del vettore A fissata allinizio. Si definisce il fattore di carico = n/m e si sceglie m in modo che presumibilmente non superi 0.9: se ci dovesse accadere si raddoppia la dimensione di A e si riallocano tutte le chiavi nel nuovo vettore. In genere m si sceglie come potenza di due o come numero primo Funzione hash Tra tanti metodi ne indichiamo due, usati rispettivamente per m potenza di 2 o per m primo. In entrambi i casi, se si ammette che tutte le sequenze di bit delle chiavi siano equiprobabili, per ogni chiave K lindirizzo hash h(K) ha valore tra 0 e m-1 con pari probabilit 1/m. In questo caso la funzione hash si dice semplicemente uniforme. 1. Poniamo che sia m=2 s , quindi le posizioni di A sono ndirizzate con s bit. La sequenza binaria che rappresenta la chiave K divisa in parti di s bit ciascuna, e tra esse sicalcola lo XOR bit a bit per ottenere una sequenza di s bit che rappresenta h(K). Si noti che tutti i bit della chiave contribuiscono a formare h(K). 2. Poniamo che m sia un numero primo. Per ogni chiave K si pone h(K)=K mod m (cio h(K) il resto della divisione tra K e m). 15 Funzione hash Una buona funzione hash deve: 1. essere facile da calcolare (costo costante) 2. soddisfare il requisito di uniformit semplice: ogni chiave deve avere la stessa probabilit di vedersi assegnata una qualsiasi posizione ammissibile, indipendentemente da altri valori hash gi assegnati Sia P(k) la probabilit che sia estratta una chiave k tale che h(k) = j , allora E k:h(k)=j P(k) = (1/m) per j = 0, . . . ,m 1 Una buona funzione hash Il requisito di uniformit semplice difficile da verificare perch raramente nota la funzione di distribuzione di probabilit con cui vengono estratte le chiave (la funzione Pr) Nella pratica per possibile usare delle euristiche per realizzare delle funzioni hash con buone prestazioni: Metodo della divisione Metodo della moltiplicazione 16 Metodo della divisione Consiste nellassociare alla chiave k il valore hash h(k) = k mod m Semplice e veloce, ma occorre evitare certi valori di m; m non dovrebbe essere una potenza di 2 Se m = 2p, h(k) rappresenta solo i p bit meno significativi di k. Questo limita la casualit di h, in quanto funzione di una porzione (di dimensione logaritmica) della chiave Bisogna rendere la funzione h dipendente da tutti i bit della chiave; una buona scelta per m un numero primo non troppo vicino ad una potenza di due Metodo della moltiplicazione Consiste nellassociare alla chiave k il valore hash h(k) = m (kA kA) kA kA la parte frazionaria di kA Ha il vantaggio che il valore di m non critico; di solito si sceglie m = 2p Per quanto riguarda il valore di A, in letteratura viene suggerito un valore prossimo a ( \5 1)/2 17 Risoluzione delle collisioni Una possibile alternativa: utilizzare una buona funzione hash (per minimizzare le collisioni) e prevedere nel contempo deimetodi di risoluzione delle collisioni Metodi classici di risoluzione delle collisioni: Liste di collisione: gli elementi collidenti sono contenuti in liste esterne alla tabella; T[i ] punta alla lista di elementitali che h(k) = i Indirizzamento aperto: tutti gli elementi sono contenuti nella tabella; se una cella occupata, se ne cerca unaltra libera Risoluzione delle collisioni per concatenazione (chaining) Gli elementi collidenti vengono inseriti nella stessa posizione della tabella in una lista concatenata 18 Costo della ricerca: analisi nel caso peggiore Data una tabella A con m posizioni ed n elementi, quanto tempo richiede la ricerca di un elemento data la sua chiave? Caso peggiore: tutte le chiavi vengono inserite nella stessa posizione della tabella creando ununica lista di collisione di lunghezza n In questo caso il tempo di ricerca (n) (ossia il costo della ricerca nella lista di collisione) + il tempo di calcolo di h Costo della ricerca: analisi del caso medio Si definisce fattore di carico il rapporto tra il numero n degli elementi memorizzati e la dimensione m della tabella o = n/m Nellipotesi di uniformit semplice della funzione hash o il numero medio di elementi memorizzati in ogni lista concatenata o < 1 molte posizioni disponibili rispetto agli elementi memorizzati o = 1 numero di elementi memorizzati uguale alla dimensione della tabella o > 1 situazione attesa: molti elementi memorizzati rispetto alla dimensione della tabella 19 Analisi nel caso medio Il comportamento nel caso medio dipende da come la funzione hash distribuisce le chiavi sulle m posizioni della tabella Ipotesi: uniformit semplice della funzione di hash h(k) calcolata in O(1) cos che il costo della ricerca di un elemento con chiave k dipenda esclusivamente dalla lunghezza della lista A[h(k)] Indirizzamento Aperto La rappresentazione non fa uso di puntatori Le collisioni vengono gestite memorizzando elementi collidenti in altre posizione della tabella Invece di seguire le liste di collisione, calcoliamo la sequenza di posizioni da esaminare Il fattore di carico non pu mai superare 1 Si usa meno memoria rispetto allar appresentazione con liste di collisione perch non ci sono puntatori 20 Indirizzamento Aperto Prevede che si usi solo lo spazio della tabella, senza uso di zone di trabocco, allocando gli elementi che determinano collisioni in posizioni diverse da quella che loro competerebbe Supponiamo di voler inserire un elemento con chiave k e la sua posizione naturale h(k) sia gi occupata Cerchiamo la cella vuota (se c) scandendo le celle secondo una sequenza di indici; ad esempio: c(k, 0) c(k, 1) . . . c(k,m) c(k, 0) = h(k) c(k, 1) = h(k) + 1 . . .c(k,m) = h(k) + m Indirizzamento Aperto Per inserire una nuova chiave si esamina una successione di posizioni della tabella, si esegue una scansione, finch non si trova una posizione vuota in cui inserire la chiave La sequenza di posizioni esaminate dipende dalla chiave che deve essere inserita Estendiamo la funzione hash in modo che possa tener conto anche del numero di posizioni gi esaminate h : C {0, 1, . . . ,m 1} {0, 1, . . . ,m 1} 21 Operazione di Inserimento Hash-Insert(T, k) { i = 0 do { j = h(k, i ) if (A[j] = null || A[j] = deleted} {A[j] = k; return j} else i = i + 1 }while (i != m) error overflow sulla tabella hash Operazione di Ricerca Hash-Search(T, k){ i = 0 do { j = h(k, i ) if (A[j ] = k) return j I = i + 1 } while (A[j ] != null && i != m) return null 22 Tecniche di scansione: Scansione Lineare Sia h : C {0, 1, . . . ,m 1} una funzione hash ordinaria Il metodo di scansione lineare usa la funzione hash (estesa) definita come h(k, i ) =( h(k) + i)mod m h(k, 0) = h(k) mod m h(k, 1) = (h(k) + 1) mod m, h(k, 2) = (h(k) + 2) mod m La scansione lineare presenta un fenomeno conosciuto come agglomerazione primaria Le posizioni occupate della tabella si accumulano per lunghi tratti, aumentando cos` il tempo medio di ricerca Tecniche di scansione: Scansione Lineare Inoltre ... La prima posizione esaminata determina lintera sequenza di scansione; quindi abbiamo solo m sequenze di scansione distinte Il numero ottimo m! ed dato dallipotesi di unformit della funzione hash: ognuna delle m! permutazioni di (h, . . . ,m 1) equiprobabile Siamo molto lontani dal numero ottimo 23 Tecniche di scansione: Scansione Quadratica Sia h : C U {0, 1, . . . ,m 1} una funzione hash ordinaria Il metodo di scansione quadratica usa la funzione hash (estesa) definita come h(k, i ) = (h(k) + c1*i + c2*i 2 )mod m dove, c1 e c2 sono delle costanti ausiliarie (con c2 != 0) Un esempio: h(k, i ) = ( (h(k) + c1*i + c2*i 2 )mod m dove, c1 = c2 = 1 h(k, 0) = h(k), h(k, 1) = h(k) + 1 + 1 = h0(k) + 2, h(k, 2) = h(k) + 2 + 4 = h0(k) + 6, h(k, 3) = h(k) + 3 + 9 = h0(k) + 12, h(k, 4) = h(k) + 4 + 16 = h0(k) + 20 Cosa succede se m = 20? Viene scandita solo una porzione (in realt 1/4) della tabella Elimina il problema dellagglomerazione primaria, ma ... 1. viene usata lintera tabella; solo per alcune combinazioni di c1, c2 ed m; se m = 2 p una buona scelta c1 = c2 = 1/2, perch ivalori h(k, i) per i e [0,m 1] sono tutti distinti 2. h(k1, 0) = h(k2, 0) implica h(k1, i) = h(k2, i) questo porta ad una forma di addensamento (pi lieve rispetto a quella primaria) detta agglomerazione secondaria di nuovo, la prima posizione determina lintera sequenza di scansione ed abbiamo solo m sequenze di scansione distinte 24 Hashing Doppio Lhashing doppio usa una funzione hash (estesa) della forma h(k, i ) = (h 1 (k) + ih 2 (k))mod m dove h 1 , h 2 sono delle funzioni hash (ordinarie) ausiliarie La prima posizione esaminata A[h 1 (k)] mod m; ogni posizione esaminata successivamente distanziata dalla precedente di una quantit h 2 (k) mod m La sequenza di scansione dipende da k in due modi: a seconda della chiave, possono variare sia la posizione iniziale che il passo Lhashing doppio non soffre di fenomeni di agglomerazione perch il passo casuale inoltre ... Ogni possibile coppia (h 1 (k), h 2 (k)) produce una sequenza discansione distinta: abbiamo O(m 2 ) sequenze di scansione distinte ed in questo senso migliore sia della scansione lineare che quadratica Hashing Doppio Il valore di h 2 (k) deve essere primo con la dimensione m della tabella per ogni chiave da cercare Infatti se MCD(h 2 (k),m) = d > 1 per qualche k, allora la ricerca di tale chiave andrebbe ad esaminare solo una porzione (1/d) della tabella Se m una potenza di 2, basta definire h 2 in maniera tale che restituisca sempre un numero dispari Altro modo scegliere m primo e porre h 1 (k) = k mod m, h 2 (k) = 1 + (k mod m0) dove m0 appena pi piccolo di m (ad esempio m0 = m 1 oppure m0 = m 2) 25 Two level hash table Spazio dei nomi Il nome di un simbolo non muta durante la compilazione Bench un scope pu essere aperto o chiuso il simbolo deve persistere in memoria I nomi possono essere di dimenzioni svariate (da 1 a molti caratteri) Una lista ordinata dei nomi pu essere mantenuta Ci fa preferire uno spazio dei nomi logico P R O V A S 1 S 2 X 5 2 2 1 26 Implementazione della tabella dei simboli Per ogni nome sono memorizzati Nome: un riferimento allo spazio dei nomi logico Tipo: informazioni associate con il simbolo Hash: collegamento doppio con i simboli che forniscono lo stesso hash code Var: riferimento alle altre dichiarazioni dello stesso nome Level: collegamento a simboli dello stesso livello Depth: memorizza la profondita del nesting di un simbolo Void openScope() { depth = depth +1; scopeDisplay(depth) = null } Void closeScope() { foreach (symb in scopeDisplay(depth)) do { prevsym = symb.var delete(symb) if (prevsym != null) add(prevsym) } depth = depth +1; } Symnol retrieveSymbol(name) { sym = hashTable.get(name) while (sym != null) { if (sym.name == name) return sym sym = sym.hash } return null } 27 Void enterSymbol () { oldsym =retriveSymbol(name) if (oldsym != null) && oldsym.depth == depth) errore (duplicate symbol) newsym = createNewSymbol(name,tyoe) newsym.level = scopeDisplay(depth) newsym.depth = depth scopeDisplay(depth) = newsym if (oldsym == null) add(newsym) else { delete(oldsym) add(newsym) } newsym.var = oldsym } Esempio fare copia HASHTABLE f function(float, float) v L h z float v L h x float v L h x int v L h f Void function v L h 28 Ancora . Strutturre e record: nomi dei campi Overloanding di funzioni e operatori Dichiarazioni implicite Contenuto della tabella dei simboli Ci sono una variet di generi tipici di identificatori Le variabili scalari, array e strutture (record), le procedure e le funzioni, Alcuni tipi di informazioni memorizzate per identificatori l nome o il valore Il tipo di dati Le Dimensioni e iinformazioni di dimensionalit (per gli array) scoping Tipi di risultati, i parametri (parametri formali), i prototipi Le informazioni salvate dipendono dal genere di oggetto, per tale motivo si ha la necessita che le entri della tabella dei simboli abbiano un formato flessibile 29 Tipo di dato nella tabella dei simboli Ad esempio lentry per un array definito come nel seguito richiede una descrizione che include il tipo degli elementi Array A [1 .. 100] of mytype ; Mytype is a record username: char string ; emailaddress: char string ; acctdetails: array ( 1: 5) of integer usage: pointer to array ( 1:12) of reals Descrittori delle strutture Il descrittore type deve essere un puntatore ad un TypeDescriptor Il TypeDescriptor una struttura dati che consente di modellare i diversi tipi presenti nei linguaggi Esempi di descrittori per alcuni tipi di dato