Facoltà di Ingegneria
Transcript
Facoltà di Ingegneria
Facoltà di Ingegneria Corso di Studi in Ingegneria Informatica Elaborato di Calcolo Parallelo Prodotto Matrice - Matrice in OpenMP Anno Accademico 2011/2012 Professoressa Alessandra D’Alessio Studenti Giuffrida Serena M63/000239 Lampognana Francesca M63/000144 Mele Gianluca M63/000145 Prodotto Matrice - Matrice in OpenMP 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 [email protected] 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 Prodotto Matrice - Vettore in OpenMP 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: C = A*B ove A,B e C sono matrici con dimensioni , . Un classico algoritmo sequenziale è, ad esempio, quello che calcola la matrice prodotto componente per componente, effettuando i prodotti scalari di ciascuna riga di A per ciascuna colonna di B, una componente alla volta secondo un ordine prestabilito. Si può notare come questa operazione sia fortemente parallelizzabile osservando la natura stessa dei calcoli che l’algoritmo dovrà effettuare. Infatti, ciascun elemento della matrice risultato C viene calcolato effettuando i prodotti scalari di ogni riga della matrice A per ogni colonna della matrice B. Passiamo ora alla descrizione dell’algoritmo da noi implementato. Il programma inizia con la parte di dichiarazioni delle variabili e, dopo aver istanziato le 3 Prodotto Matrice - Vettore in OpenMP matrici A,B e C in maniera dinamica, si passa al riempimento delle matrici A e B. Dopo di questo si passa alla parallelizzazione del calcolo del prodotto matrice – matrice, 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 – matrice è realizzato mediante tre 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 i cicli interni saranno eseguiti singolarmente da ciascun thread. Esiste anche la possibilità di utilizzare la clausola collapse(n), che ha l’effetto di collassare più cicli for innestati in un unico ciclo, specificando gli n cicli consecutivi da accorpare. In questo caso andrebbe richiamata la collapse(2), ma abbiamo deciso di non utilizzare questo costrutto per poter effettuare una valutazione dei tempi più generale. 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. 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: m, n, h, dimensioni delle matrici (che nei nostri test abbiamo supposto sempre uguali, essendo irrilevanti ai fini dei tempi), A, B, C, le matrici protagoniste del nostro calcolo. Tali variabili devono poter 4 Prodotto Matrice - Vettore in OpenMP essere viste da tutti i threads e non saranno modificate da essi. Tra le variabili che vanno dichiarate come private vi sono: gli indici j e k dell’iterazione più interna, mentre l’indice i dell’iterazione esterna è settato automaticamente come variabile privata, la variabile result, variabile d’appoggio per il calcolo del prodotto matrice – matrice, 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 nella matrice risultato C 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 Prodotto Matrice - Vettore in OpenMP 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 microsecondi, 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 P 1 2 4 8 100 500 1000 2000 9516,590912 7741,596497 6548,484833 7857,403045 1220301,834 615013,799 320195,631 165931,2301 11681392,96 5850715,063 2929531,422 1534184,206 98433849,41 49519046,4 25293304,04 12779117,19 Tabella 1: Tempo di esecuzione T(p) 6 Prodotto Matrice - Vettore in OpenMP 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 non seguono un andamento stabile quando si va ad aumentare il numero di threads che eseguono il programma, anzi in certi casi peggiorano. A differenza di quanto accadeva negli altri casi, in cui per N=1.000 non era ancora possibile apprezzare l’impiego di più threads, in questo caso già possiamo vedere un netto miglioramento con N=500. Questo è sicuramente dovuta alla maggiore complessità dei calcoli da fare. Per N=1.000 ed N=2.000 registriamo notevoli migliore di tempo causate dall’uso di un’architettura multithread. 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. Tale incremento è di notevole entità, a causa della complessità dei calcoli svolti dal programma. 7 Prodotto Matrice - Vettore in OpenMP 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) N P 2 4 8 T (1) T ( p) 100 500 1000 2000 1,229280151 1,453250814 1,211162372 1,984186104 3,811113318 7,354262564 1,996575263 3,98746123 7,614074578 1,987797758 3,89169597 7,702711224 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 Prodotto Matrice - Vettore in OpenMP Il punto di riferimento per giudicare lo speed-up delle nostre esecuzioni è la linea tratteggiata, che rappresenta il valore ideale. Per N=100 siamo ancora lontani dall’approssimarci al miglior caso possibile, ma, confermando le osservazioni fatte in precedenza, possiamo notare che già per N=500 vengono registrati valori dello speedup vicini a quelli ideali. Questo grafico conferma le considerazioni fatte in precedenza, mostrando che valori nei dintorni di quelli ideali sono stati registrati per p=2 e per p=4 a partire da N=500; con p=8 a partire sempre da questo valore di N traiamo dei benefici dall’impiego dell’architettura multithread a memoria condivisa, ma non al punto da essere ottimi come nei 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) N P 2 4 8 S ( p) p 100 500 1000 2000 0,614640076 0,363312703 0,151395297 0,992093052 0,952778329 0,919282821 0,998287631 0,996865307 0,951759322 0,993898879 0,972923993 0,962838903 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=500 con p=2 e con p=4, mentre sullo stesso numero di threads per N=1.000 ed N=2.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 Prodotto Matrice - Vettore in OpenMP Invertendo il punto di vista, fissando il numero di threads p, possiamo notare che per p=4 l’efficienza sia maggiore per N=1.000 rispetto a N=2.000, anche se di poco. Questo può essere giustificato dal fatto che l’aumento della dimensione del problema richiede una parallelizzazione maggiore. Infatti guardando la riga corrispondente a p=8 notiamo, come è corretto pensare, che l’efficienza migliora passando da N=1.000 a N=2.000. Figura 3: Efficienza E(p) al variare di N e di p 10 Prodotto Matrice - Vettore in OpenMP 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 Prodotto Matrice - Vettore in OpenMP 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 Prodotto Matrice - Vettore in OpenMP #include <stdio.h> #include <omp.h> #include <stdlib.h> int main(int argc, char **argv) { //Dichiarazione delle variabili int i,j,k,nt=4,m=5,h=5,n=5; //Dichiarazione delle dimensioni delle matrici e del numero di threads nt double tempotot,start,end; unsigned int result=0; //variabile d'appoggio //Allocazione dinamica delle due matrici da moltiplicare e della matrice risultato C int* A=(int*)calloc(m*h,sizeof(int*)); int* B=(int*)calloc(h*n,sizeof(int*)); int* C=(int*)calloc(m*n,sizeof(int*)); //Riempimento della matrice A for(i=0;i<m;i++) for(j=0;j<h;j++) A[j+i*h]=1; // utilizzeremo una matrice costituita da tutti 1 //Riempimento della matrice B for(i=0;i<h;i++) for(j=0;j<n;j++) B[j+i*n]=1; // utilizzeremo una matrice costituita da tutti 1 //Sezione di stampa a video delle matrici for(i=0;i<m;i++){ for(j=0;j<h;j++){ printf("%d ",A[j+i*h]); } printf("\n"); } printf("\n"); for(i=0;i<h;i++){ for(j=0;j<n;j++){ printf("%d ",B[j+i*n]); } printf("\n"); } //Inizio calcolo dei tempi start=omp_get_wtime(); //Inizio parte parallela #pragma omp parallel for schedule(dynamic) num_threads(nt) shared(m,n,h,A,B,C) private(j,k,result) for(i=0;i<m;i++){ for(j=0;j<n;j++){ result=0; //resetto la variabile d'appoggio for(k=0;k<h;k++) result=result+A[k+i*h]*B[j+k*n]; C[j+i*n]=result; //l'uso di questa variabile permette di evitare il problema del false sharing } 13 Prodotto Matrice - Vettore in OpenMP } end=omp_get_wtime(); tempotot=1.e6*(end-start); //Fine calcolo dei tempi //Stampa finale del risultato printf("\n"); for(i=0;i<m;i++){ for(j=0;j<n;j++){ printf("%d ",C[j+i*n]); } printf("\n"); } printf("Tempo totale : %f\n",tempotot); //Deallocazione delle matrici free(A); free(B); free(C); return 0; } 14