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