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 -