Facoltà di Ingegneria Corso di Studi in Ingegneria Informatica Elaborato di Calcolo Parallelo Prodotto Matrice - Vettore in OpenMP Anno Accademico 2011/2012 Professoressa Alessandra D Alessio Studenti Giuffrida Serena M63/000239 Lampognana Francesca M63/000144 Mele Gianluca M63/000145
Definizione del Problema Il punto di partenza del nostro elaborato è il calcolo del prodotto matrice - vettore sull architettura MIMD a memoria condivisa a 64 CPU (16 x Intel Xeon E5410 quadcore@2.33ghz 64 bit) offerta dall Università degli Studi di Napoli Federico II attraverso l infrastruttura S.Co.P.E.. L obiettivo è la valutazione dell efficienza dell algoritmo da noi implementato, valutazione a cui giungeremo raccogliendo i risultati dei diversi test eseguiti (che si differenziano per cifre di precisione richieste e/o per il numero di threads coinvolti) in tre parametri fondamentali: tempo impiegato dall algoritmo parallelo con p threads T(p), speed-up S(p) ed efficienza E(p). Per la realizzazione dell algoritmo abbiamo utilizzato l API OpenMP per gestire il parallelismo shared memory multi threaded, lavorando su un solo nodo dell infrastruttura SCOPE. Tale libreria viene messa a disposizione da quasi tutti i compilatori C ed è uno dei motivi che ci ha spinti ad utilizzare questo linguaggio di programmazione per l algoritmo da noi sviluppato. 2
Descrizione dell algoritmo Nome capitolo Andiamo ora a descrivere in particolare l operazione che vogliamo realizzare. In seguito ci dedicheremo alla descrizione specifica dell algoritmo da noi implementato, evidenziando la parti relative all uso della libreria OpenMP per la parallelizzazione del programma. Vogliamo realizzare il calcolo del prodotto: Ax = y ove A è una matrice quadrata,, mentre x e y sono vettori della stessa dimensione della matrice; quindi Definita la matrice A: E il vettore x: Si vuole ottenere il vettore risultato y così calcolato: 3
Si può notare come questa operazione sia fortemente parallelizzabile osservando la natura stessa dei calcoli che l algoritmo dovrà effettuare. Infatti, ciascun elemento del vettore dei risultati y viene calcolato effettuando i prodotti scalari di ogni riga della matrice A per il vettore x. E quindi chiaro che i prodotti scalari possono essere effettuati in maniera indipendente gli uni dagli altri, è proprio questa l idea alla base della parallelizzazione di quest algoritmo. Passiamo ora alla descrizione dell algoritmo da noi implementato. Il programma inizia con la parte di dichiarazioni delle variabili e, dopo aver istanziato la matrice A e i vettori x e y in maniera dinamica, si passa al riempimento della matrice A e del vettore x. Dopo di questo si passa alla parallelizzazione del calcolo del prodotto matrice vettore, che sarà fatto utilizzando la libreria OpenMp. Useremo la direttiva #pragma omp parallel, che forma un team di thread ed avvia un esecuzione parallela. In particolare, dato che il calcolo del prodotto matrice vettore è realizzato mediante due for innestati, useremo come direttiva una parallel for, la quale specifica che le iterazioni del ciclo contenuto al suo interno devono essere distribuite tra i thread del team. Andremo a parallelizzare solo il ciclo for più esterno, mentre il ciclo interno sarà eseguito singolarmente da ciascun thread. Il numero di thread è specificato mediante la clausola num_threads(/*numero di threads su cui è richiesta l esecuzione*/). Il modo in cui le iterazioni vengono ripartite tra i thread è specificato tramite la clausola schedule(type, chunk). Nel nostro caso, abbiamo scelto di utilizzare lo schedule dynamic, in quanto effettuando diverse prove abbiamo notato che la differenza in termini di tempi d esecuzione era irrisoria e non significativa. Tale schedule a fronte di eventuali rallentamenti dei threads, offre un autonoma gestione del carico di lavoro a fronte di una necessità di sincronizzazione tra gli stessi. Mediante lo schedule dynamic le iterazioni vengono divise in blocchi di dimensione chunk e assegnate dinamicamente ai threads; quando un thread termina un chunk, ne ha assegnato un altro. Di default chunk=1. 4
Ulteriori clausole inserite nella direttiva parallel for sono le clausole shared e private: mediante queste è possibile specificare rispettivamente quali variabili sono condivise tra i thread e quali sono private per gli stessi (ogni thread ne avrà una copia). Tra le variabili che vanno dichiarate come shared vi sono: dimmatrix, dimensione della matrice A e del vettore x, matrice, la matrice A del nostro calcolo, x e y, i vettori. Tali variabili devono poter essere viste da tutti i threads e non saranno modificate da essi. Tra le variabili che vanno dichiarate come private vi sono: l indice j dell iterazione più interna, mentre l indice i dell iterazione interna è settato automaticamente come variabile privata, la variabile result, variabile d appoggio per il calcolo del prodotto matrice vettore che vengono modificate durante le iterazioni del ciclo. Si noti che nel caso non fosse stata inserita la variabile d appoggio result, ma si fosse andato a scrivere direttamente nel vettore risultato si sarebbe andati incontro al fenomeno del false sharing, problema che accade per effetto della cache coerence: si ha traccia della modifiche effettuate su una variabile shared in una cache line, da 16 a 256 bytes, quindi se un thread scrive una parte di una cache line questa è invalidata negli altri thread. Il fenomeno diventa un problema che abbassa le performance nel caso in cui le modifiche vengono effettuate in rapida successione. Per ottenere dati riguardanti il tempo, abbiamo utilizzato la funzione omp_get_wtime() in due differenti punti dell algoritmo e, facendone la differenza, abbiamo ottenuto il tempo di esecuzione. 5
Analisi del software Adesso concentreremo l attenzione sulle tabelle e i grafici relativi al calcolo del tempo di esecuzione T(p), dello speed-up S(p) e dell efficienza E(p). Tempo di esecuzione T(p) Nella tabella che segue sono riportati i valori del tempo di esecuzione registrati al variare del numero di thread P e del numero di righe della matrice A, N. Quindi muovendoci lungo la colonna possiamo capire se c è un vantaggio o meno legato all aumento del numero di threads coinvolti. Tutti i valori del T(p) sono espressi in secondi, mentre ciascuno dei valori in tabella è stato ottenuto eseguendo la media sui campioni ottenuti eseguendo una quindicina di prove; un analogo discorso si estende anche ai valori mostrati nelle tabella del S(p) e dell E(p). N 100 1000 10000 20000 P 1 0,000105 0,008306 0,719983 2,880328 2 0,003038 0,005161 0,366666 1,446087 4 0,003335 0,005802 0,188908 0,728507 8 0,007396 0,005834 0,098917 0,373243 Tabella 1: Tempo di esecuzione T(p) 6
Figura 1: T(p) al variare del numero di threads e del numero di righe della matrice Guardando la Figura 2 e la Tabella 1 possiamo notare che per determinati valori di N non sempre è conveniente l utilizzo di un architettura multithread a memoria condivisa. In particolare per N=100 i tempi di esecuzione peggiorano quando si va ad aumentare il numero di threads che eseguono il programma. Per quanto concerne N=1000 è quasi indifferente l impiego di più threads, mentre iniziamo ad apprezzarne i vantaggi a partire da N=10.000 e ad avere dei tempi particolarmente soddisfacenti per N=20.000. Negli ultimi tre casi citati i tempi migliori si registrano con p=8. Invertendo il punto di vista, fissando quindi il numero di threads p e variando il valore di N, è possibile notare un aumento dei tempi che è ragionevole, considerando la diversa dimensione delle operazioni da svolgere. 7
Speed-up S(p) Nella tabella che segue sono riportati i valori dello speed-up, ricordiamo che esso misura, a parità di n, la riduzione del tempo di esecuzione rispetto all algoritmo su 1 thread. Il valore è stato ottenuto a partire dal T(p) medio calcolato a partire da una decina di campioni. S( p) T (1) T ( p) N 100 1000 10000 20000 P 2 0,034562212 1,609378028 1,96359357 1,991808238 4 0,031484258 1,431575319 3,811289093 3,953741007 8 0,014196863 1,423723003 7,278657865 7,717031532 Tabella 2: Valori dello Speed-up S(p) al variare di N e di P Figura 2: Speed up S(p) al variare di N e di p 8
Il punto di riferimento per giudicare lo speed-up delle nostre esecuzioni è la linea tratteggiata, che rappresenta il valore ideale. Per N=100 e N=1.000 siamo ancora lontani dall approssimarci al miglior caso possibile, come accade invece per gli altri 2 valori della N. Questo grafico conferma le considerazioni fatte in precedenza, mostrando che valori nei dintorni di quelli ideali sono stati registrati per p=2 con N=10.000 e N=20.000, per p=4 con le stesse dimensioni citate prima; con p=8 a partire da N=10.000 traiamo dei benefici dall impiego dell architettura multithread a memoria condivisa, ma non al punto da essere ottimi come nei due casi su menzionati. Efficienza E(p) Nella tabella che segue sono riportati i valori dell efficienza, indicativa di quanto l algoritmo sfrutti il parallelismo del calcolatore. E( p) S( p) p P N 100 1000 10000 20000 2 0,017281106 0,804689014 0,981796785 0,995904119 4 0,007871064 0,35789383 0,952822273 0,988435252 8 0,001774608 0,177965375 0,909832233 0,964628941 Tabella 3: Valori dell Efficienza E(p) al variare di N e p Il valore di riferimento di E(p) è rappresentato nella Figura 4, in rapporto allo stesso scopriamo che i nostri test forniscono un risultato interessante per N=10.000 con p=2 e con p=4, mentre sullo stesso numero di threads per N=20.000 si ottengono valori quasi ideali. In tutti gli altri casi all aumentare del numero di threads l efficienza degrada; ciò in piena aderenza con le osservazioni suggeriteci dai grafici di T(p) ed S(p). 9
Figura 3: Efficienza E(p) al variare di N e di p 10
Esempi d uso e codice In questo capitolo verranno illustrati degli esempi d uso dell algoritmo. Mostreremo in che modo verrà richiamato il programma e le interazioni possibili. Inoltre, mostreremo qui il codice del programma da noi sviluppato, il quale sarà correlato da una documentazione interna che ne spiegherà il funzionamento. Esempi d uso Mostriamo un esempio del risultato che si ottiene a video andando a eseguire il programma fornendo la dimensione dimmatrix=5. Si nota che vengono mostrati a video i risultati dell esecuzione, oltre al tempo di 11
esecuzione. Si può vedere come il risultato sia corretto confrontandolo con il risultato offerto dal software Matlab andando a fornirgli gli stessi input: Codice del programma Riportiamo qui di seguito il codice da noi scritto per la realizzazione di questo programma. Abbiamo utilizzato il linguaggio di programmazione C. Il codice è stato corredato di una documentazione interna che spiega le varie istruzioni utilizzate. 12
#include <stdio.h> #include <omp.h> #include <stdlib.h> int main(int argc, char **argv) { //Dichiarazione variabili int i,j,nt=4,dimmatrix=1000; //numero di righe iniziale della matrice, nt= numero di threads double tempotot,start,end; int result=0; //variabile d'appoggio int* matrice=(int*)calloc(dimmatrix*dimmatrix,sizeof(int*));; int* x=(int*)calloc(dimmatrix,sizeof(int)); int* y=(int*)calloc(dimmatrix,sizeof(int)); printf("dimensione matrice: %d.\n",dimmatrix); //Riempimento della matrice e del vettore x for(i=0;i<dimmatrix;i++){ x[i]=i; // definito come sequenza dei primi dimmatrix-1 numeri for(j=0;j<dimmatrix;j++) matrice[j+i*dimmatrix]=1; // utilizzeremo una matrice costituita da tutti 1 } //Inizio calcolo dei tempi start=omp_get_wtime(); #pragma omp parallel for schedule(dynamic) num_threads(nt) \ parallela shared(dimmatrix,matrice,x,y) private(j,result) for(i=0;i<dimmatrix;i++) { for(j=0;j<dimmatrix;j++){ result=result+matrice[j+i*dimmatrix]*x[j]; } y[i]=result; result=0; } //fine parte parallela //parte end=omp_get_wtime(); //Fine calcolo dei tempi tempotot=1.e6*(end-start); printf("tempo totale : %f\n",tempotot); /*for(i=0;i<dimmatrix;i++) printf("y[%d] = %d\n",i,y[i]); */ free(matrice); free(x); free(y); } return 0; 13