Davide Sormani Revisori: Marco D. Santambrogio Data
Transcript
Davide Sormani Revisori: Marco D. Santambrogio Data
Autori: Davide Sormani Revisori: Marco D. Santambrogio Data: 20/07/2005 Versione: 2.0 Stato: Final Profiling di codice finalizzato a scelte implementative Diffusione del documento Documento interno al laboratorio di Microarchitetture, Dipartimento di Elettronica e Informazione, Politecnico di Milano. 2 Profiling di codice finalizzato a scelte implementative Revisioni Data Versione Stato Commento 20/04/2005 1.0 Draft Prima stesura del documento 10/06/2005 1.1 Draft Correzione e aggiornamento della versione 1.0 15/07/2002 1.2 Draft Correzione e aggiornamento della versione 1.1 22/07/2002 2.0 Final Stesura Finale 3 Profiling di codice finalizzato a scelte implementative Indice Diffusione del documento _________________________________________2 Revisioni ______________________________________________________3 Indice ________________________________________________________4 Introduzione ___________________________________________________5 Obiettivo _________________________________________________________ 5 Descrizione del progetto __________________________________________6 Fasi del progetto ___________________________________________________ 6 Le cartelle del progetto ______________________________________________ 6 I . Le specifiche _________________________________________________7 Algoritmo di Huffman per la compressione di file___________________________ 7 Un esempio________________________________________________________ 8 II . Realizzazione del codice C _____________________________________10 Componenti principali dell’algoritmo ___________________________________ 10 Comportamento del codice scritto _____________________________________ 10 Esecuzione del programma___________________________________________ 11 III . Profiling del codice C ________________________________________12 Tempo totale di esecuzione e durata di ogni ciclo _________________________ 12 Fattori che influenzano i rilevamenti _______________________________________________ 13 Osservazioni sui risultati __________________________________________________________ 14 Cammini più frequenti ______________________________________________ 16 Cammini più frequenti ______________________________________________ 17 Osservazioni sui risultati __________________________________________________________ 18 IV . Scelta della porzione di codice da tradurre in VHDL e traduzione _______19 Specifiche del componente___________________________________________ 19 V . Simulazione e sintesi del VHDL__________________________________22 Simulazione ______________________________________________________ 22 Sintesi del VHDL ___________________________________________________ 24 Conclusioni ___________________________________________________25 Conclusioni ___________________________________________________26 Bibliografia ___________________________________________________27 4 Profiling di codice finalizzato a scelte implementative Introduzione Obiettivo Scopo di questo progetto è mostrare alcuni dei criteri utilizzati per realizzare il “profiling” di un segmento più o meno esteso di codice che implementa a livello software un certo algoritmo. In questo modo si potrà decidere in maniera più consapevole e vantaggiosa una sua eventuale traduzione in un Hardware Description Language (HDL), nel caso ciò implicasse un miglioramento delle prestazioni (principalmente in termini di tempo). 5 Profiling di codice finalizzato a scelte implementative Descrizione del progetto Fasi del progetto Per realizzare quanto proposto, si è deciso di seguire il seguente schema di lavoro: I. Descrizione delle specifiche del programma da realizzare; II. Realizzazione del codice in C; III. Analisi del codice: due metodi di analisi per il profiling temporale; IV. Scelta della porzione di codice da tradurre in una descrizione VHDL; V. Simulazione del codice e sua sintesi. Le cartelle del progetto La suddivisione delle cartelle del progetto è stata così organizzata: • “Codice C”: file HUFF.C che implementa l’algoritmo di Huffman, file deHUFF.C che esegue l’operazione inversa e il file README.doc, con una spiegazione più precisa sull’utilizzo degli stessi; • “Documentazione”: questa relazione e la relativa presentazione; • “Test – Risultati”: raccolta dei risultati delle misurazioni effettuate, e codice C modificato per poter eseguire i diversi test; • 6 “VHDL”: descrizione VHDL della parte di codice scelta per la traduzione. Profiling di codice finalizzato a scelte implementative I . Le specifiche Algoritmo di Huffman per la compressione di file La specifica da implementare è il noto algoritmo di Huffman, che realizza la compressione di file, basandosi sulla frequenza dei simboli in un dato contesto. Vediamo di capirne il funzionamento: Dato un alfabeto, sappiamo che la frequenza dei simboli in un certo linguaggio costruito su di esso non è quasi mai uniforme; alcuni simboli ricorrono in maniera più frequente di altri (eg: pensiamo all’italiano o all’inglese, in cui le vocali si presentano molto più spesso delle consonanti). Nel linguaggio binario, ogni carattere viene codificato con una sequenza di 8 bit, per un totale di 256 simboli diversi. Se noi decidessimo di non codificarli tutti con lo stesso numero di bit, avremmo ovviamente delle sequenze più corte per alcuni caratteri ed altre più lunghe, col vantaggio che le codifiche più brevi, corrispondenti ai caratteri più frequenti, si presenterebbero in maniera più ricorrente. Otterremmo così un complessivo risparmio di spazio. L’algoritmo di Huffman formalizza questa intuizione, ottenendo una codifica di lunghezza variabile. Dovendo noi applicare l’algoritmo a file diversi, non abbiamo a disposizione una tabella in cui le frequenze dei caratteri sono fissate, perciò dobbiamo innanzitutto costruire questa tabella. Si scandisce il file da comprimere e si conta la frequenza di ogni simbolo. A questo punto possiamo costruire l’albero di Huffman, necessario per realizzare la codifica. L'albero è binario, con un nodo foglia per ogni simbolo. Data la tabella, si scelgono i due caratteri con la minor frequenza. Questi diventano i due figli di un nuovo nodo. A questo nodo, a sua volta, è assegnata la somma delle frequenze dei figli. Si considera quindi questo nodo insieme con i restanti simboli nella tabella delle frequenze, e si selezionano di nuovo i due elementi con frequenza minore. Notare che i due elementi possono essere due nodi foglia, un nodo interno ed uno foglia, oppure due nodi interni. Le operazioni si 7 Profiling di codice finalizzato a scelte implementative reiterano fino al completamento dell’albero. Ovviamente la radice avrà come frequenza la somma delle frequenze di tutti i caratteri. A questo punto si associa ad ogni ramo destro il valore 0 e a quello sinistro 1 o viceversa, codificando in tal modo ogni simbolo. I simboli usati più di frequente avranno un minor numero di bit e quelli più rari di più. Un esempio Esempio - alfabeto composto dalle sole lettere Z, K, F, C, U, D, L, E: Lettera Z K F C U D L E Frequenza 2 7 24 32 37 42 42 120 Data questa tabella delle frequenze, l’albero finale risulterà così costruito e associo ‘1’ e ‘0’ rispettivamente ai rami Sinistro e Destro: 306 1 0 E=120 186 0 1 79 107 1 0 U=37 1 D=42 33 1 L=42 0 C=322 0 9 1 65 1 0 F=24 0 K=7 Z=2 Lettera Z K F C U D L E Codifica 001110 001111 00110 0010 011 010 000 1 8 Profiling di codice finalizzato a scelte implementative Ora possiamo codificare le parole con le nuove sequenze di bit: Parola Codifica ASCII Codifica Huffman DEED 01000100010001010100010101000100 01011010 DUCK 01000100010101010100001101001011 0100110010001110 LUCK 01001100010101010100001101001011 0000110010001110 Come si può notare, il risparmio è variabile e dipende dalle parole che vengono codificate. 9 Profiling di codice finalizzato a scelte implementative II . Realizzazione del codice C Componenti principali dell’algoritmo Il problema viene scomposto in tre parti ben distinte: scansione del file da comprimere e creazione della tabella delle frequenze (1), costruzione dell’albero (2) e infine codifica del file (3). Il main avrà perciò l’aspetto di figura 1. int main(int argc, char *argv[]) { /*apertura file ed inizializzazione variabili…*/ creaTabella(); creaAlbero(); comprimiFile(); } figura 1 Perciò le tre funzioni principali sono: • creaTabella(), • creaAlbero(), che richiama estraiMin() • comprimiFile(), che richiama crea() Il codice completo si trova, opportunamente commentato, all’interno del file HUFF.C. Comportamento del codice scritto Il comportamento del codice è il seguente: a. Al main viene passato il nome dell’eseguibile (huff), il percorso e il nome del file da comprimere ed il percorso ed il nome che si vuol dare al file compresso. b. Il file di ingresso viene aperto e viene invocata la creaTabella(), che legge il file carattere per carattere ed incrementa la posizione corretta nella tabella. c. Dopo che la tabella è stata creata si invoca la funzione creaAlbero(), la parte centrale dell’algoritmo di Huffman: − Si prendono tutti gli elementi non nulli della tabella e li si pone in una lista, composta da elementi di tipo nodo di un albero binario; − Si estraggono i due a frequenza più bassa mediante estraMin(); 10 Profiling di codice finalizzato a scelte implementative − Si istanzia un nuovo elemento di tipo nodo e si assegnano i due estratti come figli destro e sinistro, aggiornando opportunamente i campi frequenza, isleaf, sX, dX ecc… del nuovo nodo. Inoltre associa ad ‘1’ la variabile ramo del figlio sinistro e a ‘0’ quella del figlio destro; − Si reimmette nella lista l’elemento così ottenuto. − Si reiterano questi tre passaggi fino a che la lista contiene un solo elemento, la radice dell’albero. d. Ottenuto l’albero costruisco il file di uscita. Esso sarà composto da un header (A), dal file codificato (B) e dal trailer (C). (A) L’header è costituito dalle informazioni necessarie alla successiva decodifica (nome del file originale, numero di caratteri diversi codificati, numero di bit che codificano ogni diverso carattere, la codifica di Huffman degli stessi…) (B) Una volta realizzato l’header si passa alla scrittura del file vero e proprio. Creo un vettore di stringhe che ha tanti elementi quanti sono i caratteri codificati e per ciascuno abbiamo la codifica. Si fa questa cosa tramite la funzione crea() a cui passo l’albero di Huffman costruito prima. Percorrendolo alla ricerca di ciascun carattere, si costruisce la sequenza di ‘0’ ed ‘1’ memorizzata nei rami. Si inizia a scrivere il file in uscita. Se la codifica è minore o uguale a 8 bit, leggo il carattere successivo e continuo il riempimento di una variabile tampone. Una volta che essa è piena, viene scritta sul file di uscita e resettata. Si continua così fino all’ultimo byte in ingresso. Se a questo punto l’ultimo byte in uscita non è completo, si aggiungono degli zeri fittizi e conto quanti ne aggiungo. (C) Il trailer sarà costituito dal conteggio di questi zeri aggiuntivi ed è l’informazione che chiude il file di uscita. Esecuzione del programma Per eseguire il programma sarà sufficiente digitare da prompt di DOS: “huff x:\...\cartellasorgente\dacomprimere.xyz x:\...\cartellaoutput\nomecompresso.ijk” In questo modo, “huff.exe” crea il file compresso, con estensione “*.ijk”. 11 Profiling di codice finalizzato a scelte implementative III . Profiling del codice C Si può ora passare all’analisi temporale del codice. Si è deciso di utilizzare due precisi metodi di analisi al fine di evidenziare le parti di codice più critiche per quanto riguarda il tempo di esecuzione del programma. 1) Tempo totale di esecuzione e durata di ogni singolo ciclo 2) Valutazione dei cammini più frequenti Tempo totale di esecuzione e durata di ogni ciclo Per quanto riguarda il cronometraggio dei tempi di esecuzione di una serie di istruzioni, il metodo più intuitivo è stato quello di calcolare il Δt di tempo intercorso tra la prima e l’ultima istruzione a cui siamo interessati. Va evidenziato che la libreria “time.h” contiene la funzione clock(), che restituisce un intero indicante il tempo trascorso dall’inizio del programma. La sua risoluzione è però dell’ordine dei millisecondi, insufficiente per rilevare tempi anche dell’ordine di pochi microsecondi. Per questo motivo si sono utilizzate due funzioni contenute nella libreria “windows.h”: - QueryPerformanceFrequency(&Freq); - QueryPerformanceCounter(&t0). La prima finzione scrive nella variabile “Freq” la frequenza di clock a cui è eseguito il programma, mentre la seconda valuta il numero di colpi di clock intercorsi dall’inizio di esso. Freq e t0 sono delle variabili di tipo LARGE_INTEGER, cioè interi a 64 bit. La risoluzione è dei microsecondi, perciò una semplice divisione mi permette di valutare dei tempi molto più piccoli della clock(). Infatti è sufficiente modificare opportunamente il codice, inserendo la funzione QueryPerformanceCounter() dove necessario. Il template è ⎛ e1 − s1 ⎞ ⎟⎟ ⋅ 10 6 quello in figura 2. Il valore Δt che mi interessa sarà: Δt = ⎜⎜ ⎝ Freq ⎠ NB: la moltiplicazione per 106 è fatta per ottenere il tempo in microsecondi. 12 Profiling di codice finalizzato a scelte implementative #include <windows.h> ... LARGE_INTEGER s1, e1; LARGE_INTEGER Freq; ... QueryPerformanceFrequency(&Freq); QueryPerformanceCounter(&s1); ... /*Porzione di codice da cronometrare*/ ... QueryPerformanceCounter(&e1); ... figura 2 Fattori che influenzano i rilevamenti Bisogna ora fare una serie di considerazioni per rispondere a questa domanda: cosa influisce nella valutazione dei tempi di esecuzione? I fattori principali risultano essere: Caratteristiche Hardware e Software della macchina su cui si valuta il programma. Sappiamo che la macchina non si occupa in maniera dedicata all’esecuzione del nostro programma, essendo un normale Home Computer. Il processo dovrà condividere risorse di sistema con tutti quelli concorrenti al momento dell’esecuzione dello stesso; Modalità di compilazione del codice. Un particolare compilatore, potrebbe tradurre in codice macchina le stesse istruzioni in maniera diversa rispetto ad un altro, portando a dei tempi di esecuzione diversi a parità di altre condizioni.; La valutazione stessa dei tempi, influisce sulla durata del programma. Infatti l’invocazione delle funzioni che rilevano l’istante di tempo attuale e la frequenza di elaborazione, influiscono sul tempo stesso. Ciò avverrà in maniera non trascurabile se ad esempio le istruzioni sono contenute in un ciclo che viene eseguito un elevato numero di volte. Ecco un esempio in figura 3. main() { QueryPerformanceFrequency(&Freq); ... QueryPerformanceCounter(&s1); for (i=0;i<1000;i++) { QueryPerformanceCounter(&s2); /*Codice del ciclo*/ QueryPerformanceCounter(&e2); } QueryPerformanceCounter(&e1); ... } figura 3 13 Profiling di codice finalizzato a scelte implementative Ovviamente l’invocazione della QueryPerformanceCounter() per ben due volte ogni ciclo, influenzerà pesantemente il valore presente nella variabile e1, che sarà molto maggiore di quello reale. Per questi tre motivi è ovvio che si introdurrà un certo errore nelle misurazioni, più o meno controllabile dall’esterno. Per quanto riguarda l’errore introdotto dal primo fattore, è stato utilizzato lo stesso calcolatore per tutti i test: un Home Computer con un Pentium 4 ad una frequenza di 2,6 GHz. Il programma è stato eseguito sotto Windows XP, tentando di ridurre al minimo i processi concorrenti. Inoltre i test sono stati fatti su un campione di 15 files scelti casualmente, con una dimensione variabile da circa 100 byte fino a circa 100 Mbyte. Utilizzando lo stesso computer, nelle stesse condizioni si può perlomeno affermare che i ritardi introdotti saranno uniformi per tutti i test. Passando al punto due, si è creato l’eseguibile utilizzando tre compilatori diversi: Borland C 5.0, Bloodshed DEV C++ e il compilatore Gcc utilizzato da Cygwin. Per quanto riguarda l’ultimo problema, sono stati valutati separatamente il tempo complessivo ed i singoli cicli, modificando opportunamente il codice ed inserendo le funzioni di misura temporale stando attenti a non averne di annidate. I cicli non annidati, sono stati valutati durante la stessa esecuzione. I cicli interni sono stati calcolati in un’esecuzione successiva. Inoltre, onde prevenire errori sulla singola esecuzione, ogni test è stato eseguito 200 volte, numero sufficientemente elevato per eliminare, tramite la media aritmetica, eventuali errori o misurazioni scorrette. Il file “TimeHUFF.C” contiene le modifiche necessarie al codice. Tutti i risultati sono stati salvati su file di testo ed importati in un foglio excel: “TimeTable.xls” Osservazioni sui risultati I risultati più interessanti ottenuti dalle misurazioni sono riportati nelle tabelle seguenti. I file, numerati da 1 a 15, sono posti in ordine crescente di dimensione e come prevedibile, il tempo impiegato per la loro compressione aumenta linearmente con essa (figura 4). Eventuali variazioni (graficamente non ravvisabili) si hanno nel caso ci sia un buon livello di compressione e quindi la scrittura del file in uscita risulta necessitare meno 14 Profiling di codice finalizzato a scelte implementative tempo. Sin da questa prima tabella si nota la grande disparità di prestazioni ottenuta compilando il codice coi tre differenti tool. Borland C (colonna blu), mantiene i tempi sotto i 30’’ anche nel caso di file da quasi 100 megabyte, mentre il caso pessimo è raggiunto da Gcc, che supera i 3’ e 30’’. Se ora andiamo ad analizzare in maniera più approfondita i “punti critici” del programma, si nota subito in figura 5 che le disparità di prestazioni sono ben localizzate. Ogni gruppo di colonne dell’istogramma individua i tempi di esecuzione di ciascun ciclo. Essi sono stati individuati numerando progressivamente tutti i costrutti iterativi presenti nel codice C, leggendolo dall’inizio alla fine. Volendo essere schematici, possiamo così raggrupparli: 1. Ciclo 1, 4, 6, 17 e 18: accesso a file. I primi tre in lettura, gli altri in scrittura: 2. Ciclo 16: funzione ricorsiva; 3. Ciclo 10: algoritmo di Huffman; 4. Ciclo 9 e 14: accesso a variabili, con allocazione dinamica di memoria; 5. Ciclo 2, 3 , 5, 11, 12, 13 e 15: accesso a variabili. I primi due in lettura, gli altri anche in scrittura. I cicli 7 e 8 e 19 non sono stati presi in considerazione. I primi due in quanto stampe a video, il terzo in quanto impiegante un tempo basso e costante in ogni misurazione (attorno al microsecondo). Confrontando le figure 5 e 6, si evince che le basse prestazioni di DevC e Gcc rispetto all’altro compilatore, si localizzano principalmente in fase di accesso al disco rigido (lettura e scrittura di file) e risultano comunque più lenti nelle allocazioni dinamiche della memoria. Solo nel caso della chiamata ad una funzione ricorsiva (all’interno del ciclo 16) Gcc risulta più veloce anche di Borland C. Si nota come in taluni casi DevC++ risulti peggiore anche di Gcc. 15 16 C C C C C C C ic ic ic ic ic lo lo lo lo lo lo lo 9 5 3 2 17 16 15 14 13 12 11 10 lo lo ic ic ic ic C C lo lo lo C C C C ic lo lo lo lo lo lo lo lo 9 6 5 4 3 2 1 18 17 16 15 14 13 12 11 10 lo lo lo lo lo lo lo lo ic ic ic ic ic ic ic ic ic ic ic ic ic ic C C C C C C C C C C C ic Secondi 1. c 2. pp 3. t bm p 4. ti 5. f ex 6. e bm p 7. xl s 8. pd f 9. p 10 df .m p 11 3 .d o 12 c .p p 13 t .e x 14 e .e x 15 e .d kz Secondi 150 ic ic ic C 200 C C C Microsecondi Profiling di codice finalizzato a scelte implementative 250 figura 4: Tempi totali di esecuzione Borland C 5.0 DEV C++ Cygwin Gcc 100 50 0 30 figura 5: Durata di ogni loop 25 20 15 10 5 0 2500 figura 6: Loop in cui non si accede a file 2000 1500 1000 500 0 Profiling di codice finalizzato a scelte implementative Cammini più frequenti Una tipica valutazione fatta al momento dell’analisi del codice di un programma, è la valutazione del percorso critico (critical path), cioè del percorso più lungo all’interno del diagramma di un algoritmo. Si deve però considerare che al momento dell’esecuzione, non è detto che il percorso critico corrisponda a quello più frequente. È ragionevole pensare che ad esempio, cento istruzioni eseguite una sola volta siano molto meno onerose di cinque o sei istruzioni eseguite diverse centinaia di volte seguendo un altro cammino diverso da quello critico. Il semplice esempio in figura 7 mostra come percorso critico e cammino più frequente non siano la stessa cosa. Sicuramente il ramo “if” farà parte del percorso critico, ma non verrà mai eseguito. Pur essendo parte del percorso critico, non influirà sulle prestazioni del programma. Error! a=1 main() { ... a=1; if (a==0) {op1; ... opn;} else {op1;} ... } cammino critico TRUE cammino più frequente a==0 op1 FALSE op1 opn figura 7 In sostanza è necessario valutare per ogni costrutto decisionale qual è la scelta più frequente. Per valutare i cammini più frequenti ci si è basati in parte su risultati sperimentali ed in parte sul ragionamento. Grazie al secondo criterio, sono state scartate tutte le ramificazioni seguite in caso di errori, presenti solo per irrobustire il programma. Queste non saranno sicuramente il caso più frequente. Negli altri casi e comunque in tutti quelli dubbi, si è semplicemente modificato il codice, stampando a video la scelta del ramo “if” o del ramo “else” corrispondente ad ogni decisione non ancora valutata. Il programma è stato eseguito sfruttando ancora gli stessi 15 file utilizzati precedentemente. 17 Profiling di codice finalizzato a scelte implementative Osservazioni sui risultati In questo caso i risultati non sono stati particolarmente sorprendenti, in quanto il percorso critico si sovrappone esattamente a quello più frequente. In effetti il codice è piuttosto lineare e un esito del genere poteva essere prevedibile. 18 Profiling di codice finalizzato a scelte implementative IV . Scelta della porzione di codice da tradurre in VHDL e traduzione Un’analisi dei risultati ottenuti dal profiling temporale, ci porta a concludere purtroppo che l’algoritmo che abbiamo implementato non si presta in maniera ottimale ad una trasformazione di un suo equivalente Hardware. Il criterio di scelta dovrebbe portarci a scegliere uno dei segmenti del più oneroso da eseguire in termini di tempo, con un grosso numero di computazioni ripetitive. Purtroppo i segmenti individuati non posseggono queste caratteristiche. Le porzioni di codice il cui tempo di esecuzione è più elevato, sono quelle che implicano la scrittura su disco e comunque non sono sicuramente migliorabili col nostro approccio. Anche escludendo questi segmenti, i loop più lunghi non sono agevolmente implementabili in VHDL. La scelta cade quindi su una porzione di codice non conforme ai criteri da noi espressi. Ci limiteremo a realizzare un componente che sostituisca la tabella delle frequenze dei caratteri. Essa possiede un contatore per ciascuno dei 256 diversi caratteri (28 byte diversi) che viene incrementato progressivamente durante la lettura sequenziale del file di ingresso. Specifiche del componente Il componente riceve un segnale di START e dopo due cicli di clock, da un buffer di ingresso a 32 bit, un numero. Il numero indica la dimensione del file, cioè quanti byte verranno ricevuti attraverso lo stesso buffer, a gruppi di 4 alla volta. Per ogni byte ricevuto viene incrementata la relativa riga, in una memoria composta da 256 registri. Terminata la lettura, in uscita vengono inviati in ordine sequenziale, i valori presenti nei registri. Al termine, contemporaneamente all’ultimo valore, viene alzato un segnale DONE fino al clock successivo. Nel caso venga sollevato il segnale RESET in ingresso, i registri vengono svuotati ed il conteggio è quindi azzerato. 19 Profiling di codice finalizzato a scelte implementative La descrizione VHDL che realizza quanto richiesto dalle porta alla realizzazione di questa entità, chiamata freqTab (figura 8), nell’omonimo file “freqTab.vhd”. data_IN clk reset start 32 bit <- 32 bit -> 0 1 2 255 32 bit done figura 8 data_OUT Il componente è stato realizzato come una macchina a stati, con un segnale RESET asincrono. Sommariamente, il comportamento è il seguente: RST s0 s3 start=‘1’ s1 data_IN s2 data_IN s0: è uno stato in cui non succede nulla (data_OUT è 0 e se start è a 1 si passa in s1); s1: è lo stato in cui si valuta il numero in ingresso, che sarà il numero di byte successivamente inviati. Si inizializza un contatore per valutare quante volte considerare gli ingressi e un eventuale resto se il numero di byte non è divisibile per 4; s2: fino a che il contatore non è a 0, si incrementano i contatori della tabella. Altrimenti si passa allo tato successivo; 20 Profiling di codice finalizzato a scelte implementative s3: ad ogni ciclo di clock si mette in uscita il valore di una parola della tabella e si torna infine nello stato s0. 21 Profiling di codice finalizzato a scelte implementative V . Simulazione e sintesi del VHDL Simulazione Utilizzando il tool ModelSim, è stata testata la validità della descrizione VHDL realizzata. I risultati della simulazione sono in linea coi requisiti. Nelle figure successive, i risultati di due testbench, presenti nel file “testBench.vhd”: nel primo si ipotizza un file in ingresso da 17 byte (figure 9 e 10), nel secondo un file da 1048576 byte (figure 11, 12 e 13). In entrambi i casi è presente un’immagine della prima fase e una della fase finale. figura 9 n° byte in ingresso Transizioni di stato Transizione di stato DONE Ultimo valore figura 10 22 Profiling di codice finalizzato a scelte implementative n° byte in ingresso Size figura 11 data_OUT n° delle acquisizioni restanti figura 12 DONE Ultimo valore figura 13 In ogni figura, i primi sei segnali rappresentati sono gli ingressi e le uscite del componente. Il segnale “state” rappresenta lo stato attuale, o meglio, lo stato al ciclo di clock successivo. Infatti le operazioni risultano sfalsate di un ciclo rispetto a questo segnale; “table” indica la tabella delle frequenze; “size”, ”cnt” e ”resto” sono tre variabili che indicano rispettivamente quanti gruppi di 4 byte mancano, la riga della tabella presente sull’uscita e quanti sono i byte da considerare l’ultima volta che si guarda data_IN. Risulta evidente che il tempo di elaborazione è direttamente proporzionale alla dimensione del file. Siccome ad ogni colpo di clock vengono letti 4 byte, il tempo totale sarà sempre ⎛ NumeroDiByte ⎞ pari a Δt = t clock ⋅ ⎜ + 256 + 1⎟ , dove servono 256 colpi di clock per restituire i 4 ⎝ ⎠ valori ed 1 all’inizio per sapere i byte totali inviati al componente. 23 Profiling di codice finalizzato a scelte implementative Se ora valutiamo il tempo necessario a popolare la stessa tabella in C e in VHDL, vediamo che, sulla stessa macchina utilizzata per le misurazioni precedenti, se il tempo di clock impostato per la simulazione è inferiore ai 60 ns, il componente Hardware risulta più veloce della medesima esecuzione via software. Sintesi del VHDL Per effettuare la sintesi è stato usato il sistema di sviluppo ISE, che consente la realizzazione di progetti su dispositivi FPGA e CPLD prodotti dalla Xilinx. Il processo di sintesi permette di ottenere, da una descrizione VHDL, un circuito logico che la rappresenti e le informazioni sull’effettiva occupazione di memoria per una data FPGA. Quest’informazione è espressa (nel file con estensione .syr del report di ISE) tramite la specifica del numero di blocchi logici utilizzati sulla scheda. In particolare l’architettura delle FPGA Virtex-II integra elementi programmabili dall’utente, che comprendono diversi elementi configurabili: • IOB (Input/Output Blocks): blocchi di I/O usati per ingressi/uscite single ended. • CLB (Configurable Logic Blocks): blocchi che forniscono elementi funzionali per la logica combinatoria e sequenziale. Includono 4 slice e due buffer three-state. • SLICE: contenenti due generatori di funzioni, due elementi di memoria (flip-flop o latch), gate logici aritmetici, multiplexer, ecc… • LUT (Look-up tables): particolari configurazioni dei generatori di funzioni degli Slice Purtroppo i risultati della sintesi sono tutt’altro che confortanti. Infatti la sintesi giunge a buon fine, ma il problema risulta nell’occupazione di spazio sulla scheda, in particolare il numero di SLICE e di LUT: la richiesta è largamente superiore alla disponibilità (figura 14). Per questo motivo sì e proceduti ad una riscrittura della descrizione, riducendo sempre di più la dimensione dei contatori. Il primo risultato positivo è stato raggiunto riducendo drasticamente la dimensione delle parole: da 32 bit a 8 bit. Infatti è l’unico caso in cui, lasciando invariato il numero delle parole (256), il numero di SLICE risulta sufficiente. La 24 Profiling di codice finalizzato a scelte implementative quantità di elementi impiegata resta alta, ma questa volta è entro i limiti della scheda Virtex II e quindi fisicamente realizzabile. In figura 15 i risultati della sintesi. Per questo motivo la cartella VHDL contiene due sottocartelle: “freqTable” è l’originale, mentre “new_freqTable” è quella che contiene il VHDL modificato. ========================================================== Device utilization summary: --------------------------Selected Device : 2vp7ff896-5 Number Number Number Number Number of of of of of Slices: 8887 Slice Flip Flops: 8595 4 input LUTs: 17327 bonded IOBs: 67 GCLKs: 1 out out out out out of of of of of 4928 9856 9856 396 16 180%(*) 87% 175%(*) 16% 6% WARNING:Xst:1336 - (*) More than 100% of Device resources are used ========================================================== Timing Summary: --------------Speed Grade: -5 Minimum Minimum Maximum Maximum period: 9.156ns (Maximum Frequency: 109.218MHz) input arrival time before clock: 28.643ns output required time after clock: 4.173ns combinational path delay: No path found figura 14 ========================================================== Device utilization summary: --------------------------Selected Device : 2vp7ff896-5 Number Number Number Number Number of of of of of Slices: 4063 Slice Flip Flops: 2188 4 input LUTs: 7708 bonded IOBs: 43 GCLKs: 1 out out out out out of of of of of 4928 9856 9856 396 16 82% 22% 78% 10% 6% ========================================================== Timing Summary: --------------Speed Grade: -5 Minimum Minimum Maximum Maximum period: 7.796ns (Maximum Frequency: 128.271MHz) input arrival time before clock: 26.189ns output required time after clock: 4.173ns combinational path delay: No path found figura 15 25 Profiling di codice finalizzato a scelte implementative Conclusioni L’obiettivo iniziale di questo progetto è stato creare un codice C che implementasse il noto algoritmo di Huffman per la codifica dei byte in base alla loro frequenza relativa. Realizzato questo obiettivo, si sono applicate due metodologie di profiling per analizzare il codice realizzato, raccogliendo dati attraverso più esecuzioni dello stesso. Sono quindi stati valutati i punti critici dal punto di vista del tempo di esecuzione (tipicamente cicli, o comunque parti di codice ricorsive). Da questo procedimento avrei dovuto ricavare una porzione di codice che si prestasse alla traduzione in un HDL. Purtroppo il codice realizzato mal si è prestato alla traduzione e la scelta è stata sostanzialmente obbligata dalle caratteristiche del programma. La descrizione VHDL realizzata è stata simulata e si è tentato di sintetizzarla. Il componente si è rivelato sintetizzabile ma con limiti sulle dimensioni dello stesso. Un versione “ridotta” è stata risintetizzata ed è risultata realizzabile su FPGA VirtexIIpro. I metodi visti rimangono comunque validi perché offrono un ottimo criterio di scelta nelle porzioni di codice che risulterebbe vantaggioso trasformare in componenti hardware. 26 Profiling di codice finalizzato a scelte implementative Bibliografia [ISE] http://pulsar.diei.unipg.it/PAG_PERS/placidi/placidiweb/Attivitaricerca/digitale/c_telecomun icazioni/area_riservata/sussidi/Tesi/nocentini/nocentini.pdf [Algoritmo di HUFFMAN] http://dida.fauser.edu/sistemi/sistem5/compat.htm [Realizzazione del VHDL] Massimo Ratti, Fabio Rizzato - METODOLOGIA PER LA REALIZZAZIONE DI IP CORE BASATI SU DESCRIZIONI IN LINGUAGGIO C http://www.xilinx.com 27