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