Data Prefetching su Processori DSP VLIW

annuncio pubblicitario
Proposta per la tesi di dottorato
Data Prefetching su processori
DSP con architetture VLIW
Amelia De Vivo
1
Indice
1. INTRODUZIONE ....................................................................................................................... 3
2. STATO DELL’ARTE ................................................................................................................. 4
3. PROCESSORI DSP .................................................................................................................... 6
4. LA PROPOSTA: DATA PREFETCHING SU PROCESSORI DSP VLIW ......................... 8
4.1.
ESEMPIO DI PREFETCH ........................................................................................................... 11
5. ULTERIORI SVILUPPI .......................................................................................................... 13
BIBLIOGRAFIA .............................................................................................................................. 15
2
1. Introduzione
Man mano che i processori diventano più potenti, sia in termini di frequenza di clock che di
parallelismo a livello di istruzioni, cresce il divario tra la velocità dei processori e quella delle
memorie e, con esso, il collo di bottiglia dovuto agli accessi alla memoria. In genere la soluzione
a questo problema viene dall’utilizzo di più livelli di cache sempre più grandi.
Finché si tratta di processori general-purpose che eseguono applicazioni generiche, è
ampiamente dimostrato che questa soluzione funziona in modo soddisfacente, ma quando si
tratta di codici specifici, come quelli scientifici o di elaborazione dei segnali, la situazione è
diversa. In questi casi, infatti, si ha a che fare con codici che presentano uno scarso, a volte
nullo, fattore di riutilizzo dei dati. Questo si traduce in esecuzioni che generano un cache miss
dietro l’altro e presentano una quantità di computazione troppo bassa per ammortizzare il costo
dei miss stessi. Per limitare gli effetti negativi di questo comportamento si ricorre al data
prefetching, il cui scopo è far arrivare alcuni dati in cache prima che il programma ne abbia
bisogno, in modo da ridurre quanto più è possibile il numero di cache miss che non possono
essere ammortizzati dal rischeduling runtime del processore.
Per quanto riguarda le applicazioni di natura scientifica, Mowry e al. [3] hanno studiato 13
codici appartenenti a varie suite di benchmark, come SPEC e NAS, su un simulatore del MIPS
R4000, un processore a singola emissione, 100 MHz, 8 KB di data cache di primo livello e 256
KB di cache di secondo livello. Il loro lavoro ha messo in evidenza che 8 di questi codici
passano più della metà del tempo di esecuzione in stallo sugli accessi alla memoria.
Relativamente ai codici di elaborazione dei segnali, invece, Ranganathan e al. [1] hanno
recentemente svolto uno studio sulle applicazioni di media processing, una sottoclasse delle
applicazioni di elaborazione dei segnali particolarmente pesante. Gli autori hanno eseguito dei
benchmark su 12 codici, di cui 6 di elaborazione di immagini, 4 di codifica di immagini e 2 di
codifica di video, su simulatori di varie architetture general-purpose, con e senza set di istruzioni
esteso e considerando cache di varie taglie. Il risultato è che 8 di questi codici non hanno tratto
alcun vantaggio dalla presenza della cache, neanche se grande, mentre i restanti 4 avrebbero
bisogno di cache molto più grandi di quelle esistenti per ottenere un aumento di performance
stimato minore di un fattore 1.2.
In entrambi i lavori citati si afferma che i codici studiati hanno ottenuto prestazioni molto più
alte dopo aver eseguito un prefetching sui dati, automatico in [3], a mano in [1]. L’algoritmo
utilizzato è lo stesso ed è stato messo a punto in [3].
-3-
I codici appartenenti a queste classi di applicazioni, oltre a presentare uno scarso fattore di
riutilizzo dei dati, sono generalmente statici, cioè presentano flussi di controllo indipendenti dai
dati. Questo li rende particolarmente adatti ad essere analizzati e trasformati in fase di
compilazione, per cui sono i candidati ideali per il prefetching. Infatti questo di solito viene
implementato all’interno della catena di compilazione dei programmi, a livello di sorgente, a
valle di un’opportuna analisi del codice, che studia i pattern di accesso alla memoria e le
dipendenze tra i dati. In questo modo si tenta di localizzare i punti che in esecuzione
provocheranno dei cache miss e, in base a considerazioni tipo la latenza delle varie istruzioni o
le politiche di caching del processore, si decide dove piazzare l’operazione di prefetch. Questa
può essere realizzata da un’apposita istruzione prefetch, se il processore ne supporta una, oppure
da una qualsiasi istruzione di accesso alla memoria, in modo da generare un miss sul dato che si
vuole caricare in cache con un opportuno anticipo rispetto al momento in cui il dato sarà
richiesto dalla computazione. A questo proposito bisogna tenere presente che, aggiungendo
istruzioni al codice, si crea un overhead, per cui vanno evitati i prefetch che non sono
strettamente necessari.
Inoltre l’analisi delle dipendenze permette di fare delle trasformazioni sulla struttura del
codice, precisamente sui loop, per massimizzare la località dei dati, cosicché, una volta portato
in cache, un dato venga sfruttato il più a lungo possibile. Esempi di queste trasformazioni, sono
il tiling e le trasformazioni unimodulari. Il primo è una ripartizione dello spazio definito dalle
iterazioni di un loop a più indici in blocchi di forma opportuna, detti tile, in modo da processare
interamente un blocco prima di accedere al successivo. Questo permette di sfruttare il riutilizzo
temporale su grosse strutture dati. Le trasformazioni unimodulari, invece, alterano l’ordine di
esecuzione delle iterazioni del loop per avere uno schema d’accesso alla memoria più efficiente.
Sebbene queste trasformazioni siano state ideate nel contesto dei compilatori ottimizzanti e
parallelizzanti, come supporto software al caching nel primo caso, e alla parallelizzazione
automatica dei loop nel secondo, possono essere dei buoni coadiuvanti per il prefetching [3].
2. Stato dell’arte
Le tecniche di data prefetching presenti in letteratura sono tutte relative a processori generalpurpose, per cui presumono la presenza di uno o più livelli di cache con alcune caratteristiche
ben precise. Ad esempio, se la cache non è lookup-free, il prefetching non ha senso perché per
essere efficiente deve avvenire in parallelo con gli altri accessi alla cache, necessari per non
-4-
bloccare l’esecuzione del programma. Inoltre quasi sempre è richiesta una forma di supporto
hardware, tipo un buffer dedicato al prefetching o un’istruzione prefetch.
Alcuni dei primi risultati sul prefetching risalgono a Porterfield, Callahan e Kennedy [4, 5]. Il
loro algoritmo lavorava sugli elementi degli array all’interno dei loop ed emetteva i prefetch
durante l’iterazione precedente a quella che doveva utilizzare i dati, senza prendere in
considerazione nessun modello di costo.
Un miglioramento a questa soluzione è stato proposto da Klaiber e Levy [6]. Nel loro lavoro
viene computata per ogni loop una misura, detta distanza di prefetching, per determinare con
quante iterazioni di anticipo, rispetto a quella che utilizzerà i dati, bisogna emettere le istruzioni
di prefetch. Questa distanza è calcolata in maniera molto semplice come rapporto tra il numero
di istruzioni contenute in un’iterazione del loop e la latenza media sulla memoria espressa in
cicli macchina.
Uno degli algoritmi di prefetching più efficienti tra quelli in letteratura è quello di Mowry, Lam
e Gupta [2, 3], che è stato implementato all’interno del compilatore ottimizzante SUIF (Stanford
University Intermediate Form). Per scegliere i dati su cui fare prefetch, viene eseguita un’analisi
di località, dopodiché viene implementato un software pipelining tra prefetch e computazione.
In pratica da ogni loop del codice originale per il quale è possibile applicare l’algoritmo se ne
ricavano tre, uno per ogni fase del pipelining. Quello corrispondente al prologo fa solo prefetch
per un numero di iterazioni pari alla distanza di prefetching definita in [6], quello corrispondente
al kernel esegue i calcoli per l’iterazione corrente e, contemporaneamente, il prefetch dei dati
che verranno utilizzati nell’iterazione indicata dalla distanza di prefetching, e, infine, quello
corrispondente all’epilogo esegue solo la computazione per le ultime iterazioni.
Una versione modificata dell’algoritmo di Mowry è stata implementata in un compilatore per
PowerPC da Bernstein, Cohen, Freund e Maydan. In [7] viene illustrato il loro algoritmo e sono
riassunti i risultati ottenuti sulla suite di benchmark SPEC92fp, riportando un aumento di
performance che oscilla tra il 5% e il 20%.
In [8], invece, Gornish, Hsu e Santhanam valutano le performance del software prefetching
sull’architettura PA-8000 di Hewlett Packard utilizzando la suite di benchmark SPEC95fp. Il
loro algoritmo ha fatto registrare un incremento medio della performance pari al 26%.
In tutti questi lavori il data prefetching è considerato come parte integrante di un compilatore,
McIntosh, invece, in [9] ha sviluppato un algoritmo indipendente che potrebbe essere utilizzato
in un supporto run-time per un qualunque compilatore commerciale. Qui l’idea è di associare ad
ogni variabile, che potrebbe essere oggetto di prefetch, una sezione dello spazio di indirizzi del
programma, detta regione d’ombra. Il supporto run-time mantiene un mapping tra le regioni
-5-
d’ombra e le variabili, cosicché, quando il programma fa un accesso ad una locazione che è
dentro una zona d’ombra, il supporto run-time genera un prefetch.
3. Processori DSP
I processori DSP sono processori specializzati per il Digital Signal Processing in applicazioni
embedded, quindi costituiscono il cuore tecnologico di molti dispositivi in cui è necessario fare
elaborazione di segnali. Ad esempio è possibile trovarli in stampanti, dischi, modem, sistemi di
controllo, telefoni cellulari, prodotti consumer in genere. Sono ottimizzati per algoritmi DSP,
cioè filtri, finestrature, convoluzioni, trasformate di Fourier, trasformate di Laplace, etc. Questi
lavorano tutti su segnali campionati ed hanno le seguenti caratteristiche: spendono la maggior
parte del tempo di esecuzione in loop che ad ogni iterazione eseguono solo poche istruzioni,
presentano un flusso di controllo indipendente dai dati, fanno molti accessi alla memoria e
presentano uno scarso riutilizzo dei dati.
Siccome operazioni composte, tipo MAC (Multiply And Accumulate), sono frequentissime nei
codici DSP, in genere i processori DSP le possono eseguire in un unico ciclo di clock, grazie ad
un hardware specializzato. Per sfruttare questa capacità, però, occorre garantire un’adeguata
banda passante nei confronti della memoria. Per questo motivo i DSP seguono l’architettura
Harvard, cioè hanno due spazi di memoria separati, a cui si può accedere contemporaneamente
tramite due bus distinti, uno per le istruzioni e uno per i dati. Alcuni presentano anche due data
path, in modo da poter eseguire simultaneamente due operazioni di tipo load/store.
Generalmente, i DSP, viste le caratteristiche dei codici per i quali sono pensati, non hanno
cache per i dati, ma prevedono una piccola memoria interna, in cui il programmatore può
allocare ciò che ritiene più opportuno, in maniera statica o dinamica. In maniera statica bisogna
decidere a priori quali dati vanno nella memoria interna e comunicarlo al linker con opportune
direttive; in maniera dinamica, invece, bisogna esplicitamente programmare il controller del
DMA, che di solito è interno al processore, per gestire trasferimenti di dati tra memoria esterna
ed interna in runtime.
Per la loro natura embedded, questi processori devono rispettare alcuni vincoli su dimensione,
peso e dissipazione di potenza, pur mantenendo basso il costo. Questo ha fatto sì che solo il
progresso tecnologico degli ultimi anni permettesse la nascita di DSP di fascia alta. Per
migliorare le performance di queste architetture, oltre ad aumentare la frequenza del clock, i
-6-
progettisti hanno seguito principalmente due direzioni che mirano entrambe ad ottenere una
maggiore quantità di lavoro per ogni ciclo macchina.
In un caso sono state aggiunte più unità funzionali parallele al data path ed è stato esteso il set
di istruzioni per poter codificare più operazioni in una singola istruzione, seguendo la filosofia
SIMD, un po’ come è avvenuto nel mondo dei general-purpose con le estensioni MMX. L’idea
qui è di suddividere gli insiemi di dati in sottoinsiemi di pochi elementi su cui operare in
parallelo con una sola istruzione capace di gestire più unità contemporaneamente. Alcuni DSP
della linea SHARC della Analog Device seguono questo tipo di architettura.
Nell’altro caso, invece, l’architettura è stata modificata in modo da permettere al processore di
emettere più di un’istruzione per ogni ciclo di clock. Siccome un’architettura superscalare
avrebbe richiesto delle sofisticazioni hardware che non avrebbero permesso di rispettare i
vincoli fisici a cui deve essere soggetto un processore DSP, e siccome i codici DSP, a causa
della loro staticità, non sfrutterebbero in pieno tali sofisticazioni, questa filosofia ha portato alla
realizzazione di architetture VLIW. La sostanziale differenza tra architetture superscalari e
VLIW è che, mentre nella prima un hardware dedicato rischedula in modo dinamico le
istruzioni in runtime, nella seconda lo scheduling è demandato al compilatore e quindi è statico.
Pertanto un’architettura VLIW è più efficiente in termini di consumi ma è più penalizzata
rispetto alla latenza degli accessi in memoria. I DSP della linea C6000 della Texas Instrument
sono un esempio di architettura VLIW.
Di pari passo con l’evoluzione architetturale c’è stata anche un’evoluzione degli strumenti di
sviluppo software. Così, mentre i loro predecessori si programmavano quasi esclusivamente in
assembler, i DSP di ultima generazione iniziano a supportare linguaggi ad alto livello come il C
e ad essere dotati di una serie di tool che avvicinano l’ambiente di programmazione agli
standard a cui è abituato il programmatore medio. Inoltre sono disponibili sul mercato sempre
più librerie di funzioni ottimizzate ad hoc, spesso scritte in assembler ma con la possibilità di
essere chiamate dall’interno di programmi C, sviluppate sia dai produttori che da terze parti.
Questi progressi aprono nuovi orizzonti all’utilizzo di questa categoria di processori. Infatti, da
un lato la maggiore potenza permette di prenderli in considerazione per elaborazioni più pesanti,
dall’altro la migliore programmabilità ne rende possibile l’utilizzo al di fuori del contesto
monoapplicazione per cui erano stati originariamente creati.
-7-
4. La proposta: Data Prefetching su Processori DSP VLIW
Come tesi di dottorato si propone di sviluppare un algoritmo per il data prefetching su
processori DSP con architetture VLIW. Trattandosi di processori dedicati all’elaborazione dei
segnali, ci occuperemo di questo tipo di codici, che, come abbiamo visto, sono particolarmente
adatti ad essere oggetto di tecniche di prefetching e possono trarne grandi vantaggi in termini di
ottimizzazione.
Oltre alle caratteristiche dei codici, il prefetching dei dati è ben adatto anche al tipo di
architettura che stiamo prendendo in considerazione. Infatti questi processori non riescono a
tollerare la variazione di latenza tra accessi a memoria interna ed esterna, per cui di fronte ad un
accesso alla memoria esterna, il processore congela la pipe di esecuzione finché il dato non si
rende disponibile, provocando un crollo delle performance nel caso di accessi frequenti. Ciò si
ripercuote anche su alcuni aspetti della programmabilità di questa categoria di processori, in
quanto lo sviluppatore di applicazioni, nel caso in cui i dati eccedano la capacità della memoria
interna, si trova costretto ad intervenire a basso livello. Infatti per trovare soluzioni efficienti,
dopo aver analizzato il codice a mano, bisogna programmare il controller del DMA per gestire
allocazioni dinamiche dei dati sulla memoria interna. Affinché il programmatore medio possa
scrivere codice efficiente senza sforzo eccessivo, sarebbe opportuno rendere trasparente
l’utilizzo del DMA e le strategie di allocazione e spostamento dei dati.
Quindi per fare data prefetching su processori DSP bisogna tenere presente che le condizioni al
contorno sono profondamente diverse rispetto al caso dei general-purpose. Infatti mentre lì si
tratta di sfruttare meglio la cache, in questo contesto bisogna gestire la memoria dati e il
controller DMA interni.
In questo scenario non bisogna cercare nel codice i riferimenti alla memoria che
provocheranno cache miss, ma, dopo aver valutato quali strutture dati possono essere allocate
staticamente sulla memoria interna, si devono opportunamente suddividere le restanti strutture
dati, allocate in memoria esterna, in blocchi che verranno portati su buffer temporanei nella
memoria interna, in modo da essere disponibili prima che il programma li richieda. Ovviamente
quando viene richiesto un dato che ancora non è disponibile in memoria interna, la CPU dovrà
aspettare che arrivi. Mancando un meccanismo di caching, bisognerà, quindi, gestire
esplicitamente la sincronizzazione tra CPU e DMA in modo da garantire la correttezza del
programma e l’attesa minima da parte della CPU. Inoltre bisognerà anche occuparsi della
coerenza delle informazioni, quindi i dati modificati dovranno essere ricopiati nella memoria
-8-
esterna prima di poter riutilizzare il buffer temporaneo in cui erano stati precedentemente
portati.
Quindi, nel caso DSP, sostanzialmente si tratta di effettuare degli spostamenti di dati tra i vari
livelli della gerarchia di memoria per portarli il più vicino possibile al processore, in previsione
del loro utilizzo. A questo scopo bisogna trasformare il codice sorgente, inserendo delle
chiamate a funzioni che programmano direttamente il controller del DMA e realizzano
trasferimenti di dati in sovrapposizione al calcolo. L’idea è di creare una sorta di pipeline tra la
computazione e il trasferimento dei dati, in modo che il numero di elementi portati in memoria
interna sia tale da tenere il più possibile impegnata la CPU durante il successivo trasferimento.
Chiaramente il nostro algoritmo dovrà anche decidere di volta in volta che taglia devono avere
i blocchi di dati da trasferire. Questo oltre che dal particolare codice che si deve eseguire,
dipende da una serie di altri fattori. Infatti da un lato bisogna mantenere basso il traffico tra i due
livelli di memoria, ma dall’altro i buffer temporanei non possono essere molto grandi perché ci
possono essere dati acceduti spesso che necessitano di essere allocati sulla memoria interna. Ad
esempio, nel caso di una moltiplicazione tra una matrice e un vettore la situazione ideale sarebbe
di poter allocare il vettore sulla memoria interna e di portare di volta in volta un certo numero di
righe della matrice su un buffer temporaneo. Quindi per poter stabilire in che punto del codice
inserire l’istruzione che dà inizio al trasferimento di un blocco, bisogna sviluppare un modello di
costo che tenga conto della latenza delle varie istruzioni, dei tempi di accesso ai due tipi di
memoria, della velocità del DMA, delle dimensioni della memoria interna e del tradeoff tra
dimensione dei buffer temporanei e numero di trasferimenti.
Dunque per eseguire il data prefetching, le strutture dati in uso nel codice vengono partizionate
in blocchi di taglia e forma opportune. Questo equivale, a seconda dei casi, ad eseguire una o
più trasformazioni sul codice originale. Inoltre, come nel caso del prefetching tradizionale, si
potrebbe ricorrere ad altre trasformazioni, come il tiling o le unimodulari, per sfruttare meglio
una eventuale località dei dati. Queste potrebbero, infatti, riarrangiare il codice in modo da
migliorare gli schemi di accesso alla memoria. In questo modo il prefetching risulterà più
efficiente perché, una volta portato un blocco in memoria interna, su di esso verrà eseguita
quanta più computazione possibile, mantenendo basso il traffico tra i due tipi di memoria.Tutte
queste trasformazioni si intendono relative ai loop. Questo non è un fattore limitante perché
ovviamente il prefetching ha senso solo per grosse strutture di dati e queste sono sempre
manipolate da loop.
Per essere sicuri che le trasformazioni effettuate rispettino la semantica del programma,
occorre un’analisi delle dipendenze. A questo scopo bisogna innanzitutto rileggere il codice in
-9-
chiave geometrica. Infatti, dato un loop a n indici, lo si può rappresentare come un poliedro
convesso finito nello spazio iterazione Zn limitato dai suoi estremi inferiori e superiori. Ogni
iterazione del loop corrisponde ad un punto nello spazio iterazione. L’ordine di esecuzione delle
iterazioni è vincolato dalle dipendenze sui dati. Queste sono rappresentate mediante vettori, detti
appunto vettori di dipendenza. Un vettore di dipendenza definisce un insieme di archi tra coppie
di punti nello spazio iterazione. L’iterazione p1 deve essere eseguita prima dell’iterazione p2 se
esiste un vettore di dipendenza d, tale che p2 = p1 + d. I vettori di dipendenza associati alle
variabili presenti in un loop sono sempre lessicograficamente positivi, cioè se d = (d1, …, dn),
allora  i : (di > 0 e  j < i : dj  0). La figura 1 mostra un loop a due indici e il relativo insieme
dei vettori di dipendenza, D.
For I=0 to 5 do
For J=0 to 6 do
A[J+1]=A[j]+A[J+1]+A[J+2]
D =
{(0,1),(1,0),(1, -1)}
Figura 1. Vettori di dipendenza per un loop a due indici
Una volta calcolati i vettori di dipendenza, possiamo controllare che una trasformazione su un
loop sia legale, semplicemente verificando che i vettori di dipendenza trasformati siano ancora
lessicograficamente positivi.
Circa l’implementazione dell’analisi delle dipendenze stiamo attualmente valutando la
possibilità di utilizzare una libreria di funzioni su poliedri, I.R.I.S.A., sviluppata presso
l’Universitè Louis Pasteur di Strasburgo [10]. Versioni precedenti di questa libreria sono state
utilizzate per fare l’analisi delle dipendenze nell’ambito della parallelizzazione automatica dei
loop [11, 12].
Inoltre per avere una maggiore flessibilità, vogliamo sviluppare un algoritmo che possa essere
implementato in un preprocessore a monte della catena di compilazione, in modo da poterlo
utilizzare con qualsiasi compilatore commerciale. Ovviamente il sorgente da mandare poi in
pasto al compilatore deve essere prodotto avendo in mente le tecniche di ottimizzazione
standard usate dai compilatori VLIW, in particolare il software pipeline sui loop. Questa tecnica
è fondamentale per la compilazione ILP (Instruction Level Parallelism) in generale e ancora di
più per quella VLIW, dove tutto lo scheduling delle istruzioni è compito del compilatore. Quindi
- 10 -
bisogna fare attenzione a che le trasformazioni sui loop effettuate dall’algoritmo di prefetching
non inibiscano il pipelining sui loop trasformati.
L’algoritmo sarà poi implementato e sperimentato in un tool per l’ottimizzazione semiautomatica di codici di elaborazione dei segnali sul processore DSP TMS320C6701, prodotto
dalla Texas Instrument e messo a disposizione dalla Quadrics Supercomputers World, che
intende utilizzare tale processore come nodo di calcolo per un supercalcolatore a memoria
distribuita, denominato QUASAR. Tale tool dovrebbe permettere all’utente di scrivere un
codice C, in cui eventualmente si possano inserire delle direttive per aiutare e/o forzare il
comportamento del tool stesso. Inoltre, potrebbe essere considerato come il modulo
fondamentale all’interno di un tool più ampio per l’ottimizzazione globale del posizionamento
dei dati in tutta la gerarchia di memoria della macchina QUASAR, comprendendo la memoria
della macchina host e quella distribuita sui singoli nodi.
Infine, siccome recentemente alcune case produttrici di DSP, tra cui la Texas Instrument,
stanno iniziando a prevedere l’uscita sul mercato di DSP dotati di una piccola cache per i dati,
sarebbe interessante fare un confronto su una suite di benchmark tra le prestazioni ottenute dal
nostro algoritmo e quelle della cache. A questo scopo la Quadrics Supercomputers World
metterà a disposizione il processore DSP TMS320C6711 che sarà la versione con cache del DSP
TMS320C6701 nella linea di produzione C6000 della Texas Instrument.
4.1.
Esempio di prefetch
Come esempio di quello che vogliamo realizzare consideriamo il caso della moltiplicazione tra
una matrice A e un vettore B. Sia C il vettore risultato. Supponiamo che i vettori B e C possano
essere allocati sulla memoria interna. Il codice per eseguire la moltiplicazione è rappresentato
nella parte sinistra della figura 2. Qui si vede facilmente che non ci sono dipendenze sui dati.
Inoltre, supponendo che la matrice A sia allocata per righe, lo schema di accesso alla memoria è
già ottimale perché i suoi elementi vengono acceduti in sequenza. Quindi l’algoritmo di prefetch
può semplicemente trasformare il codice in modo da portare un certo numero di righe di A in
memoria interna prima che la CPU le richieda. Supponiamo che un modello di costo abbia
calcolato che il massimo dell’efficienza si raggiunge con trasferimenti da K righe. Il lato destro
della figura 2 mostra la trasformazione che verrebbe effettuata sul codice. Qui il simbolo & lega
istruzioni che possono essere eseguite in parallelo. La figura 3, invece, mostra i contenuti delle
due memorie all’iterazione t-esima del kernel del prefetching. Qui Ai indica la riga i-esima della
matrice A.
- 11 -
Per ogni riga i
Per ogni colonna j
C(i)=C(i)+A(i,j)*B(j)
Rrologo: Prefetch k righe di A su Buf1
Kernel:Finchè ci sono righe in A
Prefetch k righe di A su Buf2 &
Per ogni riga i su Buf1
Per ogni colonna j
C(i1)=C(i1)+Buf1(i,j)*B(j)
Prefetch k righe di A su Buf1 &
Per ogni riga i su Buf2
Per ogni colonna j
C(i2)=C(i2)+Buf2(i,j)*B(j)
Epilogo: Per ogni riga i su Buf1
Per ogni colonna j
C(i1)=C(i1)+Buf1(i,j)*B(j)
Figura 2. Trasformazione del codice per la moltiplicazione matrice-vettore.
A1
A2
A(2t-1)K+1
A2tK
A(2t-1)K+1
A2tK
Buf2
A2tK+1
A(2t+1)K
Buf1
B
A2tK+1
C
Memoria interna
A(2t+1)K
AN
- 12 -
Memoria esterna
5. Ulteriori sviluppi
In un particolare contesto un processore DSP può rivestire il ruolo di nodo di calcolo di una
macchina parallela a memoria distribuita, specializzata per applicazioni di Digital Signal
Processing particolarmente pesanti, che chiameremo DSP intensive. Queste si basano su
algoritmi DSP tradizionali, ma lavorano su insiemi di dati molto grandi, richiedono una qualità
di elaborazione molto alta e talvolta sono soggette a stringenti vincoli real-time. Esempi di
questa classe di applicazioni sono il SAR (Synthetic Aperture Radar) o lo STAP (Space Time
Adaptive Processing), ma se ne possono individuare molte altre, soprattutto in settori emergenti
come il media processing, la realtà virtuale, le telecomunicazioni, le scienze spaziali, il radar
processing.
In questo ambito, dal punto di vista della programmazione parallela, esistono due possibilità:
scrivere un programma in base al modello SPMD (Single Program Multiple data), oppure
scrivere un programma ibrido. Nel primo caso tutti i processori eseguono lo stesso programma,
ciascuno sulla sua parte di dati, sincronizzandosi solo quando è necessario uno scambio di
informazioni. Nel secondo, invece, c’è un programma sequenziale che gira su una macchina
host e demanda alcune parti di computazione alla macchina parallela mediante un’opportuna
libreria di comunicazione. In entrambi i casi, per mantenere alta l’efficienza del sistema, è
fondamentale una gestione ottimale del sottosistema di memoria del singolo nodo. Per motivi di
programmabilità questa gestione non può essere lasciata a carico dello sviluppatore, che già
deve concentrarsi sulle problematiche tipiche della parallelizzazione.
Per fissare le idee prendiamo ad esempio un’applicazione SAR. L’input è costituito da matrici
di svariati megabyte che rappresentano le immagini radar da elaborare e il nucleo della
computazione è una convoluzione bidimensionale su queste matrici, di solito eseguita nel
dominio delle frequenze. Quindi per ogni immagine input si calcolano FFT, convoluzione
spettrale e FFT inversa per tornare al dominio del tempo. Le proprietà matematiche di queste
funzioni permettono di lavorare su una dimensione per volta, quindi si possono processare prima
le righe e poi le colonne. In termini di parallelizzazione, se immaginiamo le matrici distribuite
tra i vari nodi per righe, ogni nodo eseguirà tante FFT per quante sono le righe che gli sono state
assegnate, una moltiplicazione tra la sua sottomatrice spettrale e un opportuno vettore replicato
- 13 -
su tutti i nodi, e una FFT inversa per ogni riga. Queste operazioni sono tipiche del mondo DSP,
per cui ci dovremmo aspettare una performance locale molto alta. In realtà questo è vero fino a
che i dati allocati sul nodo possono essere contenuti nella sua memoria interna, ma, siccome
un’applicazione SAR è da considerare DSP intensiva proprio per la mole di dati che elabora, ci
sarà un uso pesante della memoria esterna. Questo, se si traduce in una banale allocazione dei
dati su tale memoria, provoca un notevole calo di performance perché colpisce uno dei punti
deboli dell’architettura del processore.
Terminata la fase di calcolo sulle righe, ci sarà una ridistribuzione dei dati, che recapiterà ad
ogni nodo un certo numero di colonne della matrice. Dopo questa trasposizione, detta cornerturn, avrà luogo l’analoga fase di calcolo sulle colonne. Questa situazione di alternanza tra fasi
di elaborazione e fasi di riarrangiamento dei dati è tipica delle applicazioni DSP intensive,
indipendentemente dalla scelta del modello di programmazione, SPMD o ibrido. E’ chiaro che
per mantenere alte le prestazioni si deve intervenire anche sugli spostamenti di blocchi di dati tra
nodi. Infatti, se durante le fasi di calcolo va gestito il traffico dei dati tra la memoria esterna e
quella interna del singolo nodo, durante le fasi di comunicazione va gestito il traffico tra
memoria locale e remota. In più nel caso della programmazione ibrida bisogna occuparsi in
maniera più attenta dei trasferimenti tra memoria host e memorie dei nodi perché in questo
contesto la distribuzione dei dati è semiautomatica. Infatti lo scenario è quello di un programma
sequenziale che fa delle chiamate a funzioni parallele, che possono includere direttive per la
distribuzione dei dati. Inoltre tra due chiamate a funzioni diverse potrebbe essere necessaria una
ridistribuzione.
Quindi nell’ambito di un discorso di ottimizzazione del posizionamento dei dati all’interno di
tutta la gerarchia di memoria della macchina parallela, il tool che proponiamo è il modulo
fondamentale di un tool più ampio che automatizzi del tutto o in parte gli interventi necessari ai
vari livelli. La realizzazione di questo tool di ottimizzazione globale può essere vista come
un’estensione al lavoro che ci riproponiamo di svolgere.
- 14 -
Bibliografia
[1]
Ranganathan, Adve, Jouppi. Performance of Image and Video Processing with Generalpurpose Processors and Media ISA extensions. ISCA-26, pp. 124-135, Maggio 1999
[2]
Mowry. Tolerating Latency through Software-controlled Data Prefetching. PhD thesis,
Stanford University, Marzo 1994
[3]
Mowry, Lam, Gupta. Design and Evaluation of a Compiler Algorithm for Prefetching.
ASPLOS-V, pp. 62-73, Ottobre 1992
[4]
Porterfield. Software Methods for Improvement of Cache Performance. PhD thesis, Rice
University, Maggio 1989
[5]
Callahan, Kennedy, Porterfield. Software Prefetching. ASPLOS-IV, Aprile 1991
[6]
Klaiber, Levy. Architecture for Software-controlled Data Prefetching. ISCA-18, pp. 43-63,
Maggio 1991
[7]
Bernstein, Cohen, Freund, Maydan. Compiler Techniques for Data Prefetching on the
PowerPC. ICPACT, Giugno 1995
[8]
Gornish, Hsu, Santhanam. Data Prefetching on the HP PA-8000. ISCA-24, Giugno 1997
[9]
McIntosh. Compiler Support for Software Prefetching. PhD Thesis, Rice University, Maggio
1998
[10] Loechner. Polylib: A Library for Manipulating Parameterized Polyhedra. Internal Report,
Marzo 1999
[11] Le Verge, Van Dongen, Wilde. Loop Nest Synthesis Using the Polyhedral Library. IRISA Internal Report, No. 830, Maggio 1994.
[12] Marongiu, Palazzari. A New Memory-Saving Technique to Map System of Affine Recurrence
Equations (SARE) onto Distributed Memory Systems.
- 15 -
Scarica