performance dei sistemi di calcolo

34
Chapter 1 Performance dei sistemi di calcolo 1.1 Misura dal costo computazionale di un al- goritmo Quando si studia un nuovo algoritmo ` e di fondamentale importanza stimare il numero di operazioni necessarie alla sua esecuzione, in funzione della dimensione del problema. L’unit` a di misura minima per il costo computazionale ` e il 1 FLOP = Floating Point Operation ovvero, 1 FLOP equivale ad un’operazione di somma o di moltiplicazione float- ing point. I multipli di quest’unit`a di misura sono: 1 MFLOP = 10 6 FLOPS 1 GFLOP = 10 9 FLOPS 1 TFLOP = 10 12 FLOPS Esempio 1. Si consideri il seguente frammento di codice: float sum = 0.0f; for (i = 0; i < n; i++) sum = sum + x[i]*y[i]; Ad ogni iterazione viene eseguita una somma e una moltiplicazione floating point, quindi in totale il costo computazionale ` e di 2n FLOPS. Esercizio 1. Si mostri che il costo computazionale di un prodotto matrice per vettore ` e di 2n 2 FLOPS. Esercizio 2. Si mostri che il costo computazionale di un prodotto riga per colonna tra due matrici ` e di 2n 3 FLOPS. 1

Upload: majong-devjfu

Post on 24-May-2015

1.809 views

Category:

Documents


1 download

TRANSCRIPT

Page 1: Performance dei sistemi di calcolo

Chapter 1

Performance dei sistemi di

calcolo

1.1 Misura dal costo computazionale di un al-

goritmo

Quando si studia un nuovo algoritmo e di fondamentale importanza stimare ilnumero di operazioni necessarie alla sua esecuzione, in funzione della dimensionedel problema. L’unita di misura minima per il costo computazionale e il

1 FLOP = Floating Point Operation

ovvero, 1 FLOP equivale ad un’operazione di somma o di moltiplicazione float-ing point. I multipli di quest’unita di misura sono:1 MFLOP = 106 FLOPS1 GFLOP = 109 FLOPS1 TFLOP = 1012 FLOPS

Esempio 1. Si consideri il seguente frammento di codice:

float sum = 0.0f;

for (i = 0; i < n; i++)

sum = sum + x[i]*y[i];

Ad ogni iterazione viene eseguita una somma e una moltiplicazione floatingpoint, quindi in totale il costo computazionale e di 2n FLOPS.

Esercizio 1. Si mostri che il costo computazionale di un prodotto matriceper vettore e di 2n2 FLOPS.

Esercizio 2. Si mostri che il costo computazionale di un prodotto riga percolonna tra due matrici e di 2n3 FLOPS.

1

Page 2: Performance dei sistemi di calcolo

2 CHAPTER 1. PERFORMANCE DEI SISTEMI DI CALCOLO

1.2 Misura della velocita di elaborazione

La velocita di elaborazione di un sistema si misura in numero di operazioni float-ing point che possono essere eseguite in 1 secondo, ovvero i FLOP/S. I multiplicomunemente utilizzati sono:1 MFLOP/S = 106 FLOPS al secondo1 GFLOP/S = 109 FLOPS al secondo1 TFLOP/S = 1012 FLOPS al secondo

Esempio 2. La velocita di punta di un processore Intel Pentium D 3GHze di 24 GFLOP/S. Infatti questo processore e dotato di 2 cores che operanocontemporaneamente. Ciascun core e in grado di eseguire in un ciclo clock4 operazioni floating point sfruttando le istruzioni SIMD. Di conseguenza, ilnumero totale di operazioni al secondo e di: 2 · 4 · 3 · 109 = 24 GFLOP/S

1.3 Utilizzo della memoria

Un fattore fondamentale per determinare le prestazioni di un sistema e lalarghezza di banda della memoria (memory bandwith), ovvero la velocita con cuie possibile trasferire i dati tra la memoria e il processore. Si misura in numerodi bytes che si possono trasferire in un secondo. I multipli che solitamente siusano sono i Mb/sec, Gb/sec e Tb/sec.

Vediamo ora perche questa misura e cosı importante: si consideri l’operazione

A = B * C

Per eseguire questa operazione, il processore dovra compiere i seguenti 4 passi:

- leggere dalla memoria il dato B

- leggere dalla memoria il dato C

- calcolare il prodotto B * C

- salvare il risultato in memoria, nella posizione della variabile A.

Si vede quindi che ad una singola operazione floating point, possono cor-rispondere fino a 3 accessi alla memoria. Se la memoria non e in grado difornire i dati al processore in modo sufficientemente rapido, il processore non ein gradi di eseguire le operazioni alla sua massima velocita, perch deve rimanerein attesa che gli arrivino i dati su cui operare dalla memoria. Consideriamo dinuovo l’Esempio 2: abbiamo visto che per ogni operazione floating point occor-rono fino a 3 accessi alla memoria. Siccome la dimensione del dato e di 32 bit,ovvero 4 bytes, ad ogni operazione corrisponde un trasferimento di 12 bytes.Nell’eseguire 24 GFLOP/S occorrera quindi una larghezza di banda di

24 · 12 = 288Gb/s.

Page 3: Performance dei sistemi di calcolo

1.4. CACHE DEL PROCESSORE 3

Purtroppo, tale processore ha una larghezza di banda approssimativa di 12Gb/sec,quindi il processore non riesce a ricevere dalla memoria i dati in modo sufficien-temente rapido e ad ogni operazione deve rimanere in attesa (wait state) dellamemoria. Anche se l’esempio ha preso come modello un processore PentiumD,il problema e molto comune su tutti processori moderni ed e il principale collodi bottiglia nelle prestazioni. Volendo fare un paragone, e come avere unamacchina sportiva con un motore potentissimo, ma non riuscire a fornire almotore abbastanza benzina per farlo funzionare.

1.4 Cache del processore

Il modo con cui i processori attuali riducono le limitazioni della larghezza dibanda della memoria e la cache.

La cache della CPU e una area di memoria ad alta velocita di accesso e didimensioni piuttosto piccole, rispetto alla memoria primaria, situato tra questae il microprocessore . In genere si tratta di memoria di tipo statico, senzala necessita di refresh, assai piu costosa di quella dinamica, ma con tempi diaccesso molto ridotti, dell’ ordine del singolo ciclo clock. Puo essere sia esternache interna al chip del processore e puo essere situata a diversi livelli logico/fisici,a seconda delle funzioni svolte. La cache contiene i dati utilizzati con maggiorfrequenza dal microprocessore nelle operazioni correnti e questo contribuisceall’ incremento delle prestazioni, poich tali dati non devono essere richiamatiogni volta dalla piu lenta memoria RAM. Le cache possono contenere istruzioni(codici), dati o entrambi i tipi di informazione. Se la CPU deve cercare un datoo una istruzione, la ricerca per primo nella cache; se e presente, questa situazionedi chiama cache hit e il dato e immediatamente disponibile senza dover attenderela memoria. Se non e presente (cache miss), la preleva dalla RAM e ne fa ancheuna copia nella cache; in questo caso occorre attendere la memoria; generalmentel’ordine di grandezza dell’attesa e la decina di cicli clock per ogni cache miss..Anche se sembrerebbe che quanto piu grande e la cache , tanto piu grandee il numero di informazioni che possono essere gestiti con efficienza, questaaffermazione e vera relativamente, in quanto, aumentando la cache oltre certilimiti, il rapporto prezzo/prestazioni diventa non conveniente. La cache puoessere incorporata nel microprocessore allo scopo di accrescerne pi la velocit diaccesso che la dimensione: la cache nel chip, data la riduzione delle distanze diinterconnessione e la maggior possibilit di integrazione delle funzioni di scambio,comunica piu rapidamente con il microprocessore, solitamente lavorando allastessa velocit della CPU, mentre le cache esterne funzionano con clock ridotti.In genere sono definiti due tipi di cache, dette L1, interna al chip del processoree di dimensioni che variano tra 16Kb e 32Kb, e L2 , spesso esterna di dimensionitra 512Kb e 2Mb, mentre sono state realizzate anche strutture con pi livelli (adesempio L3 di AMD). Un sistema senza cache ha prestazioni nettamente ridotterispetto ad uno con cache; la differenza rilevabile facilmente disabilitando lecache interne dal setup del BIOS.

Vediamo ora come e possibile sfruttare al meglio la cache per massimizzare

Page 4: Performance dei sistemi di calcolo

4 CHAPTER 1. PERFORMANCE DEI SISTEMI DI CALCOLO

le prestazioni dei programmi che scriveremo. La cache sfrutta due principifondamentali che si chiamano localita di spazio e localita di tempo.

Localita di spazio: Se un dato viene utilizzato in un dato istante, e probabileche dati posizionati in celle di memoria adiacenti vengano anch’essi richiestientro breve. Ad esempio, si consideri il seguente frammento di codice:

float sum = 0.0f;

for (i = 0; i < n; i++)

sum = sum + x[i]*y[i];

I valori contenuti nei vettori x e y sono letti sequenzialmente: quando adesempio si legge il valore x[i], nelle iterazioni successive occorrera accede a x[i+1],x[i+2]... che sono tutti dati vicini al valore appena letto. Questo tipo di accessoalla memoria e tipico di molti algoritmi e di conseguenza e sfruttato dalla cache.Vediamo in che modo: la cache e un’area di memoria organizzata a blocchi(tipicamente di 32 bytes). Quando il processore legge un dato dalla memoria,vengono letti anche i suoi dati successivi fino a riempire un blocco della cache.In pratica la cache effettua una speculazione supponendo che i dati successivi aquello letto potranno servire entro breve. Se nelle istruzioni successive questosuccede, il processore trova gia i dati disponibili nella cache (cache hit) e nondeve attendere la lenta lettura del dato dalla memoria centrale (cache miss).Da qui si deduce che per sfruttare al meglio la cache occorre organizzare i datiin modo che possano essere acceduti il piu possibile in sequenza. Al contrario,un accesso ”random” o a salti alla memoria genera inevitabilmente una serie dicache miss. Inoltre accessi random alla memoria ”inquinano” la cache (cachepollution), ovvero la riempiono di dati che non possono essere sfruttati, rubandospazio ai dati in cache che possono essere letti nell’ordine corretto.

Localita di tempo: se un dato viene referenziato in un dato istante, e prob-abile che lo stesso dato venga nuovamente richiesto entro breve. Nell’esempiodi prima, le variabili sum, i e n vengono utilizzate tante volte quante sono leiterazioni del ciclo. Seguendo il flusso del programma, esistera un punto in cuiqueste variabili sono utilizzate per la prima volta. In quel momento entranonella cache e siccome sono utilizzate continuamente nel ciclo, non lasciano maila cache e rimangono disponibili per un loro rapido accesso.

Esempio 3. Si consideri la seguente funzione che esegue il prodotto matriceper vettore:

#define N 1024

// Esegue c = A*b, dove A \‘e una matrice N*N

// b e c sono due vettori di N elementi

void MatVec(double c[], double A[][N], double b[])

{

int i, j;

for (i = 0; i < N; i++)

{

Page 5: Performance dei sistemi di calcolo

1.4. CACHE DEL PROCESSORE 5

c[i] = 0.0;

for (j = 0; j < N; j++)

c[i] += A[i][j] * b[j];

}

}

Prima di tutto, notiamo che questa funzione e scritta per operare con unadimensione fissa N delle matrici. Questo approccio e caldamente sconsigliato, inquanto si vincola il funzionamento ad un caso particolare. Ricordando che nellinguaggio C le matrici sono memorizzate per righe, si puo riscrivere la funzionein modo piu generico:

void MatVec(int n, double *c, double *A, double *b)

{

int i, j;

for (i = 0; i < n; i++)

{

c[i] = 0.0;

for (j = 0; j < n; j++)

c[i] += A[i*n + j] * b[j];

}

}

Notiamo che la matrice A e rappresentata in un vettore in cui sono mem-orizzare in sequenza le righe della matrice. Per accedere ad un dato elemento(i, j) della matrice, viene calcolata la posizione all’interno del vettore A con i*n+ j. Questa operazione non compromette le performances, dato che comunqueverrebbe generata intrinsecamente dal compilatore, qualora venisse utilizzatoil costrutto matrice come nella prima versione della funzione. In questo casopossiamo vedere come tutti gli accessi alle 3 aree di memoria (la matrice A e ivettori b e c) avvengono tutti in modo sequenziale, ed in questo modo e sfruttatala localita di spazio della cache.

Esercizio 3. Si consideri la seguente funzione che esegue il prodotto rigaper colonna tra due matrici A e B, scrivendo il risultato in C.

// Esegue C = A*B, dove A, B e C sono matrici n*n

void MatMat(int n, double *C, double *A, double *B)

{

int i, j, k;

for (i = 0; i < n; i++)

for (j = 0; j < n; j++)

{

C[i*n + j] = 0.0;

for (k = 0; k < n; k++)

C[i*n + j] += A[i*n + k] * B[k*n + j];

Page 6: Performance dei sistemi di calcolo

6 CHAPTER 1. PERFORMANCE DEI SISTEMI DI CALCOLO

}

}

Possiamo notare come gli accessi agli elementi delle matrici C ed A siano insequenza, mentre gli elementi della matrice B sono letti a salti di n elementi.Considerando che il tipo di dato e un double, che occupa 8 bytes, le lettureavvengono a salti di 8n bytes. Ogni accesso alla matrice B genera quindi uncache miss. Questo e il modo meno ottimizzato per calcolare un prodotto tramatrici.

Si scriva un programma in C che esegua il prodotto tra due matrici di 1024elementi con la funzione di cui sopra e si cronometri il tempo di esecuzione. Siscrivano poi due diverse implementazioni della funzione che adottino le seguentistrategie:

- trasposizione della matrice B, in modo da poter accedere sequenzialmenteai suoi elementi.

- Calcolo del prodotto a blocchi. Si renda parametrizzabile la dimensione delblocco in modo da lavorare con blocchi che possano essere contenuti nellacache L1 del processore. Quest’ultima strategia e la base della libreria dialgebra lineare ATLAS.

Si cronometrino queste due nuove versioni e si confrontino i risultati con laprima versione non ottimizzata.

1.5 Metrica delle prestazioni parallele

Sia T (p) il tempo di esecuzione in secondi di un certo algoritmo su p processori.Di conseguenza sia T (1) il tempo di esecuzione del codice parallelo su 1 proces-sore. Sia inoltre T (1) il tempo di esecuzione dell’algoritmo su un processore conil migliore algoritmo sequenziale.

Si definisce misura di performance di un algoritmo parallelo su p proces-sori rispetto al miglior algoritmo seriale, eseguito su un processore:

S(p) =T (1)

T (p)

Si definisce misura di scalabilita o speedup relativo di un algoritmoparallelo eseguito su p processori rispetto all’esecuzione dello stesso algoritmosu un processore:

S(p) =T (1)

T (p)

In un sistema ideale, in cui il carico di lavoro potrebbe essere perfettamentepartizionato su p processori, lo speedup relativo dovrebbe essre uguale a p. Talespeedup si definisce lineare. Nella pratica non si ha quasi mai uno speeduplineare, a causa di vari fattori:

Page 7: Performance dei sistemi di calcolo

1.5. METRICA DELLE PRESTAZIONI PARALLELE 7

• sbilanciamento del carico di lavoro tra i processori: alcuni processori hannoun carico maggiore di lavoro rispetto ad altri; i processori con meno caricodi lavoro, terminano prima il loro compito e si fermano in attesa dei risul-tati degli altri processori, rimanendo cosı inutilizzati.

• presenza di parti di codice non parallelizzabili che devono essere eseguiteda tutti i processori.

• tempi di comunicazione e sincronizzazione.

In alcuni rari casi si ha la situazione

S(p) > p.

In tale caso lo speedup si dice superlineare e generalmente si ottiene poichein un sistema a memoria distribuita, ad un aumento dei processori corrispondeanche un aumento della memoria totale disponibile. Una maggiore quantita dimemoria puo consentire il salvataggio di risultati intermedi ed evitare di doverliricalcolare in parti successive nell’algoritmo. In questo modo si puo ridurreil numero di calcoli eseguiti rispetto ad un’esecuzione con meno processori edottenere cosı speedup superlineari.

Flatt e Kennedy hanno proposto un modello che descrive qualitativamentelo speedup di un sistema. Sia Tser il tempo di esecuzione della parte seriale diun algoritmo, Tpar il tempo di esecuzione della parte di codice che puo essereparallelizzata e T0(p) il tempo per le comunicazioni e le sincronizzazioni tra i pprocessori. Valgono le relazioni:

T (1) = Tser + Tpar

T (p) = Tser +Tpar

p+ T0(p)

Supponendo che T0(p) sia proporzionale a p, ovvero T0(p) = Kp, possiamoscrivere lo speedup come:

S(p) =Tser + Tpar

Tser +Tpar

p+ T0(p)

=p(Tser + Tpar)

pTser + Tpar + Kp2

La figura 1.5 descrive l’andamento dello speedup al variare del numero di pro-cessori. Si vede come la curva e inizialmente prossima ad uno speedup libeare,poi presenta una flessione fino ad un valore massimo detto punto di satu-razione ed infine comincia a decrescere quando i costi di comunicazione T0(p)cominciano a prevalere sulgli altri costi. Infatti:

limp→∞

S(p) = 0.

Questo significa che e c’e una soglia (che dipende all’algoritmo e dall’ ar-chitettura del sistema) oltre la quale e controproducente aumentare il numerodei processori. e come se per compiere un determinato lavoro si assumesserepiu persone: se le persone sono troppe, si intralciano a vicenda ed occorre piutempo a portare a termine il compito.

Page 8: Performance dei sistemi di calcolo

8 CHAPTER 1. PERFORMANCE DEI SISTEMI DI CALCOLO

0 10 20 30 40 50 60 700

2

4

6

8

10

12

14

16

S(p)

p

Figure 1.1: Andamento dello speedup S(p) in funzione del numero di processori

1.6 Altre misure di prestazioni parallele

Si definisce Penalizzazione dovuta all’uso di p processori la grandezza

Q(p) = pT (p).

Si definisce efficienza il rapporto

E(p) =S(p)

p.

Idealmente, se l’algoritmo avesse uno speedup lineare, si avrebbe E(p) = 1.Nella pratica E(1) = 1 e E(p) per p > 1 e una funzione decrescente. Maggior-mente l’efficienza si allontana da 1, peggio stiamo sfruttando le risorse di calcolodisponibili nel sistema parallelo.

La figura 1.6 descrive l’andamento dell’efficienza al variare del numero diprocessori.

Come si puo determinare il numero ottimale di processori per un certo algo-ritmo? Osserviamo che:

- E(p) ha il massimo per p = 1;

- S(p) ha il massimo in corrispondenza del punto di saturazione, in cui perol’efficienza e piuttosto bassa.

Kuck ha introdotto la funzione

F (p) = E(p)S(p)

Page 9: Performance dei sistemi di calcolo

1.7. CONSIDERAZIONI SULLA PARALLELIZZAZIONE DEGLI ALGORITMI9

0 10 20 30 40 50 60 700.1

0.2

0.3

0.4

0.5

0.6

0.7

0.8

0.9

1

E(p)

p

Figure 1.2: Andamento dell’efficienza E(p) in funzione del numero di processori

detta funzione di Kuck. Questa funzione unisce due esigenze contrapposte:avere il massimo speedup e avere la massima efficienza. Il numero ottimale diprocessori pF con cui eseguire un algoritmo si puo determinare mediante

pF = argmaxF (p)

ovvero pF e il numero di processori in corrispondenza del massimo della funzionedi Kuck. Si veda la figura 1.6 per vedere la funzione di Kuck dell’esempioprecedente.

1.7 Considerazioni sulla parallelizzazione degli

algoritmi

Di seguito sono riportati i punti chiave che occorre sempre valutare nello sviluppodi algoritmi paralleli:

• Identificazione di blocchi computazionali indipendenti.

• Ridurre o eliminare le dipendenze. Sono i due principi base checonsentono di parallelizzare un algoritmo. Se non esistono calcoli indipen-denti e le operazioni dipendono strettamente dai risultati delle precedenti,non e possibile distribuire i calcoli tra processori diversi. Ad esempio, siconsideri un programma che vuole calcolare l’n-esimo numero della serie

Page 10: Performance dei sistemi di calcolo

10 CHAPTER 1. PERFORMANCE DEI SISTEMI DI CALCOLO

0 10 20 30 40 50 60 700

1

2

3

4

5

6

7

F(p)

p

Figure 1.3: Funzione di Kuck F (p) al variare del numero di processori

di Fibbonacci. Sappiamo che la serie e’ definita come:

f0 = 1f1 = 1fn = fn−2 + fn−1 (1.1)

Per calcolare l’elemento n-esimo occorre conoscere i due elementi prece-denti. Si dice cioe’ che il calcolo di fn dipende da fn−2 e da fn−1. Non e’possibile parallelizzare questo algoritmo perche’ non esistono operazioniche possano essere eseguite indipendentemente l’una dall’altra.

• Bilanciare il carico di lavoro. Il carico di lavoro distribuito tra i pro-cessori deve essere il piu possibile uniforme. Se non fosse cosı, i processoriche hanno meno lavoro (o che terminano prima degli altri), devono ri-manere inoperosi in attesa degli ultimi, sprecando risorse di calcolo. Talecondizione si chiama idle.

Un esempio di carico di lavoro sbilanciato e’ il seguente. Supponiamodi voler calcolare il prodotto matrice-vettore con una matrice triangolaresuperiore. Se suddividiamo la matrice a blocchi di righe, il primo pro-cessore avra’ quasi tutti gli elementi non nulli, mentre l’ultimo processoreavra’quasi tutti gli elementi nulli, eccetto un piccolo triangolo. L’ultimoprocessore dovra’ quindi eseguire pochissime operazioni per moltiplicareil proprio blocco, terminera’ molto prima del primo processore e rimarra’cosi’ in idle, finche’ il primo processore non avra’ finito.

Page 11: Performance dei sistemi di calcolo

Chapter 2

Algoritmi paralleli

2.1 Ordinamento di un vettore

Uno degli approcci che piu comunemente si utilizzano per parallelizzare un al-goritmo e detto divide and conquer, conosciuto anche con l’espressione latinadivide et impera. Questo approccio si basa sul seguente regionamento: si haun problema di dimensione n e di suppone di saper risolvere un problema dellostesso tipo di dimesione n/2. Quindi si divide il problema originale in due sot-toproblemi indipendenti di meta dimensione e si risolvono le due meta; se si ein grado di sfruttare queste soluzioni parziali in modo effeiciente per ottenere lasoluzione del problema iniziale, allora e possibile trovare un’efficiente paraleliz-zazione dell’algoritmo. Infatti e sufficiente continuare ricorsivamente la divisionein sottoproblemi piu piccoli fino a quando non si e generato un sottoproblemaindipendente per ogni processore.

Come primo esempio di questo principio, consideriamo il problema dell’ordinamentodi un vettore di interi. Sia I = {ai : i = 0 · · ·n − 1, ai ∈ N} un vettore din interi. Suddividiamo I in due sottoinsiemi I0 = {ai : i = 0 · · · n

2− 1} e

I1 = {ai : i = n2· · ·n − 1}.

Ora consideriamo i due problemi indipendenti di ordinare i vettori I0 e I1.Se si hanno a disposizione due processori, e possibile ordinare I0 sul primoprocessore e I1 sul secondo processore in modo indipendente. L’ordinamentopuo essere effettuato conunqualunque algoritmo di ordinamento sequenziale,quale ad esempio il quick sort. Una volta terminato l’ordinamento dei duevettori, e possibile ottenere l’insieme I ordinato fondendo i due insiemi I0 e I1

ordinati esattamente come avviene nell’algoritmo merge sort.

Quindi, la procedura per ordinare un vettore con due processori e:

• Il processore P0 fornisce a P1 gli elementi di I1;

• Ogni processore ordina la sua sottosequenza: P0 ordina I0 e P1 ordina I1;

• P1 comunica a P0 il vettore I1 ordinato;

11

Page 12: Performance dei sistemi di calcolo

12 CHAPTER 2. ALGORITMI PARALLELI

Figure 2.1:

• P0 fonde gli insiemi I0 e I1 precedentemente ordinati per ottenere lasoluzione.

Come si puo generalizzare questa procedura al caso in cui si hanno piu di2 processori? Ogni volta che un processore deve ordinare la sua sottosequenza,possiamo riutilizzare questa procedura e dividere cosı ulteriormente per dueil sottoinsieme. Questa procedura puo essere ripatuta ricorsivamente fino aquando l’insieme di partenza non e stato suddiviso in tante parti quanti sono iprocessori disponibili.

Si veda la figura 2.1 per un esempio nel caso di 8 processori.In questo caso piu generale, la procedura per ordinare un vettore con p

processori, dove p = 2i con i intero, e:

• Il processore P0 ha a disposizione il vettore I da ordinare;

• Comincia la suddivisione del proble a in sottoproblemi di dimensione viavia dimezzata, finche tutti i p procesori non hanno un insieme Ip di di-mensione n/p;

• Ogni processore ordina il suo sottoinsieme;

Page 13: Performance dei sistemi di calcolo

2.2. DECOMPOSIZIONE LU 13

• Per ogni coppia di processori, il secondo comunica al primo il suo sot-toinsieme ordinato; il primo processore riceve il sottoinsieme dal secondoprocessore e lo fonde al suo sottoinsieme.

• Il punto precedente viene ripetuto finche non si ha una sola coppia diprocessori e la fusione produce come risultato l’insieme I ordinato.

Osserviamo che questo algoritmo e molto efficiente e, se non ci fossero icosti di comunicazione, il suo speedup sarebbe superlineare. Infatti, nel caso di1 processore, il costo di ordinamento con un algoritmo di tipo ”veloce” comeil quick sort e di nlog(n). In presenza di p processori, ogni processore deveordinare un vettore di dimensione n/p e di conseguenza il suo costo di calcoloe n

plog n

p= n

p(log(n)− log(p)) che e minore del costo sequenziale nlog(n) diviso

per il numero di processori. Inoltre, per il fatto che i processori lavorano suvettori piu piccoli, e piu probabile che i dati possano essere mantenuti nellacache del processore e questo incrementa ulteriormente l’efficienza del metodo.

2.2 Decomposizione LU

Un sistema di n equazioni con n incognite si puo scrivere come:

a00x0 + a01x1 + · · · + a0n−1xn−1 = b0

a10x0 + a11x1 + · · · + a1n−1xn−1 = b1

...an−1,0x0 + an−1,1x1 + · · · + an−1,n−1xn−1 = bn−1

In forma matriciale diventaAx = b.

La decomposizione LU e una metodo che, a partire da una matrice A ∈ Rnxn

genera due matrici L, U ∈ Rnxn, con L triangolare inferiore e U triangolaresuperiore, tali che A = LU . In questo modo e possibile risolvere il sistema lineareAx = b risolvendo due sistemi lineari triangolari: Ly = b e successivamenteUx = y.

L’algoritmo della decomposizione LU si basa sull’eliminazione di Gauss: ilprincipio e quello di ”eliminare” progressivamente delle variabili dal sistema,finche non rimane una sola variabile. Presa ad esempio la i-esima equazione nelsistema, possiamo eliminare un’incognita da un’altra equazione j, moltiplicandol’equazione i per il termine costante −aji/aii e aggiungendo l’equazione risul-tante all’equazione j. L’obiettivo e quello di eliminare tutti i termini che stannonel triangolo inferiore della matrice, procedendo per colonne, ed ottenendo cosıla matrice triangolare superiore U. I coefficienti −aji/aii, per cui vengono molti-plicate le equazioni, sono memorizzti nel triangolo inferiore della matrice L. Laversione sequenziale dell’algoritmo si puo scrivere in questo modo:

// Esegue la decomposizione LU della matrice "a" scrivendo in

// "a" il triangolo superiore e in "l" il triangolo inferiore

Page 14: Performance dei sistemi di calcolo

14 CHAPTER 2. ALGORITMI PARALLELI

void ludcmp(double a[][N], double l[][N], int n)

{

int i, j, k;

double m;

for (i = 0; i < (n-1); i++)

{

for (j = i+1; j < n; j++)

{

m = a[j][i] / a[i][i];

for (k = i; k < n; k++)

a[j][k] -= m*a[i][k];

l[j][i] = m;

}

l[i][i] = 1.0;

}

}

Il costo computazionale dell’algoritmo e proporzionale a O(n3

6). L’accesso

ai dati nel ciclo piu interno avviene per righe, di conseguenza si ha un buonosfruttamento della cache.

Vediamo ora come e possibile parallelizzare questo algoritmo. Supponiamodi avere un processore per ogni riga della matrice, ovvero p = n. Ogni processoreavra’ memorizzato gli elementi della riga a lui associata, ovvero il processore P0ha gli elementi della riga a0, il processore P1 ha gli elementi della riga a1 e cosi’via.

L’algoritmo parallelo avra’ n-1 iterazioni: nella prima iterazione, il proces-sore P0 invia a tutti gli altri, tramite una comunicazione broadcast, gli elementidella propria riga. Tutti gli altri processori Pi : i > 0 potranno cosi’azzerare ilprimo elemento della propria riga. Nella seconda iterazione, il processore P1invia a tutti gli altri, tramite una comunicazione broadcast, gli elementi dellapropria riga, eccetto il primo elemento perche’ e’ gia’ stato azzerato. Tutti glialtri processori Pi : i > 1 potranno cosi’azzerare il secondo elemento della pro-pria riga. In generale, alla j-esima iterazione, il processore Pj comunica in lesue rimanenti n − j variabili ai processori Pi : i > j, i quali azzereranno la loroj-esima variabile.

Si vede chiaramente come molti processori sono idle, ovvero non compiononessuna operazione. Ad esempio, il processore P0, dopo aver comunicato lapropria riga agli altri processori, non ha piu’ compiti da eseguire per tutto ilresto dell’algoritmo. Il carico di lavoro e’ quindi mal bilanciato e lo speedup diquesto algoritmo non potra’ essere buono. Ci si poteva aspettare un risultatosimile, in quanto siamo partiti dall’assunto di avere tanti processori quantesono le righe della matrice. Il numero di processori sarebbe troppo elevato perottenere buoni speedup, come descritto nel paragrafo 1.5.

Analizziamo ora un caso piu’ realistico, ovvero il caso in cui di hanno moltimeno processori rispetto alle righe della matrice. Questo e’ un caso piu’ reale,

Page 15: Performance dei sistemi di calcolo

2.2. DECOMPOSIZIONE LU 15

Figure 2.2:

in quanto generalmente le matrici da fattorizzare hanno dimensioni nell’ordinedelle migliaia, mentre il numero di processori disponibili e’ nell’ordine delladecina. Se distribuissimo la matrice A a blocchi per righe non risolveremmoil problema del cattivo bilanciamento del carico. Il processore P0 avrebbepochissime opeazioni da svolgere, dato che dovrebbe azzerare gli elementi deltriangolo inferiore. L’ultimo processore dovrebbe al contrario operare su quasitutti i suoi elementi.

Per bilanciare il carico di lavoro, e’ allora utile utilizzare una distribuzioneciclica delle righe, ovvero il processore Pj possiede le righe {ai : i mod p = j},dove p e’ il numero totale di processori (vedi Figura 2.2).

Con questa distribuzione, l’algoritmo di decomposizione LU parallelo si puoschematizzare nel seguente modo:

1 Il processore P0 comunica agli altri processori le righe della matrice dafattorizzare, secondo la distribuzione ciclica. La riga i sara inviata alprocessore i mod p.

2 Per ogni riga i: il processore i mod p, ovvero il processore che detiene lariga i-esima, comunica con una broadcast la riga a tutti i processori.

3 Sia Rq = {q + kp : k ∈ N, q + kp < n} l’insieme delle righe possedute dalprocessore q-esimo. Per ciascuna riga in Rq, annulla l’i-esimo elemento,sfruttando la riga ricevuta al passo 2.

Page 16: Performance dei sistemi di calcolo

16 CHAPTER 2. ALGORITMI PARALLELI

4 Memorizzare nella matrice L (anch’essa distribuita ciclicamente) i coeffi-cienti usati per annullare gli elementi di A.

5 Ripetere dal passo 2, per i = 0, . . . , n − 1.

6 Ogni processore comunica a P0 le proprie righe della matrice A (che oraconterra gli elementi annullati) e della matrice L.

E’ possibile osservare che in questo algoritmo e necessario operare un grandenumero di comunicazioni: occorrono n comunicazioni per il passo 1, altre n per ilpasso 2 (una comunicazione ogni iterazione) e n per il passo 6. Quindi, in totale,il numero di comunicazioni aumenta all’aumentare della dimensione del prob-lema. Questa una caratteristica atipica poiche solitamente il numero di comuni-cazione proporzionale al numero di processori, ma non aumenta all’aumentaredella dimensione del problema (aumenta la quantit di dati trasmessi, ma nonil numero di comunicazioni). Inoltre, la distribuzione ciclica produce un bi-lanciamento perfetto del carico di lavoro solo asintoticamente, quindi piu sonopiccole le dimensioni della matrice A, piu si sara lontani da un bilanciamentoottimale. Considerati questi punti, non sara possibile osservare speedup ottimaliper questo algoritmo.

2.3 Risoluzione di un sistema lineare a freccia

In questo paragrafo consideriamo il problema della risoluzione di un sistema lin-eare a freccia (detto anche bordato a blocchi, o block bordered o bidiagonale).E’ molto frequente incontrare questo tipo di sistemi lineari nella risoluzione diproblemi reali composti da blocchi interconnessi tra loro, come la progettazionedi circuiti integrati VLSI o l’ottimizzazione strutturale. Vedremo come e pos-sibile sfruttare le particolarita di questo problema per sviluppare un metodo dirisoluzione parallelo. In generale, un sistema a freccia ha la seguente forma:

A1 B1

A2 B2

. . ....

Ap Bp

C1 C2 · · · Cp As

x1

x2

...xp

xs

=

b1

b2

...bp

bs

det(Ai) #= 0

Introduciamo ora un po’ di notazione che ci permette di scrivere il sistemain una forma piu compatta. Si definiscono:

AI = diag(A1, · · · , Ap)

B =

B1

...Bp

Page 17: Performance dei sistemi di calcolo

2.3. RISOLUZIONE DI UN SISTEMA LINEARE A FRECCIA 17

C = (C1, . . . , Cp)

xI =

x1

...xp

bI =

b1

...bp

Il sistema puo essere riscritto come:

AIxI + Bxs = bI (2.1)

CIxI + Asxs = bs (2.2)

Si noti come la matrice AI e fortemente sparsa, in quanto ha tutti gli ele-menti nulli eccetto i blocchi sulla diagonale. Quando si sviluppa un algoritmoparallelo, il primo passo e quello di identificare se ci sono operazioni indipen-denti che possono essere distribuite tra i vari processori. Apparentemente, inquesto sistema non ci sono operazioni di questo tipo e quindi, a prima vista, sipotrebbe essere portati a risolvere il sistema utilizzando la decomposizione LUparallela oppure un qualunque metodo iterativo. Tuttavia e possibile riscrivereil problema tramite una trasformazione in modo da evidenziare sottoproblemiindipendenti.

Moltiplicando 2.1 per CA−1

I e sottraendo il risultato da 2.2 si ha:

(As − CA−1

I B)xs = bs − CA−1

I bI

ovveroAxs = b

dove

A = As − (C1, . . . , Cp)

A−1

1

. . .

A−1p

B1

...Bp

= As −

p∑

i=1

CiA−1

i Bi

e

b = bs − (C1, . . . , Cp)

A−1

1

. . .

A−1p

b1

...bp

= bs −

p∑

i=1

CiA−1

i bi

In questo modo e stato possibile determinare le componenti incognite xs.Ora, osservando che l’equazione 2.1 e un sistema diagonale a blocchi, e possibilesfruttare xs per calcolare la soluzione risolvendo p sistemi indipendenti.

E’ possibile schematizzare l’algoritmo di risoluzione coi seguenti passi:

Page 18: Performance dei sistemi di calcolo

18 CHAPTER 2. ALGORITMI PARALLELI

1 Invertire le matrici A1, . . . , Ap;

2 Calcolare A = As −∑p

i=1CiA

−1

i Bi e b = bs −∑p

i=1CiA

−1

i bi;

3 Risolvere il sistema Axs = b per determinare xs;

4 Calcolare le restanti componenti del vettore soluzione x risolvendo p sis-temi lineari Aixi = bi − Bixs, i = 1, . . . , p.

Osserviamo che il punto 1 e intrinsecamente parallelo, in quanto devono es-sere risolti i p problemi indipendenti di inversione delle matrici Ai. Il punto 2 puoessere eseguito in parallelo solo parzialmente; infatti ogni processore puo eseguirei prodotti CiA

−1

i Bi e CiA−1

i bi, ma dovra essere eseguita una comunicazione percalcolare la somma, dato che occorre raccogliere in un unico processore tutti iprodotti parziali per sommarli. Il punto 3 non e intrinsecamente parallelo. Puoessere risolto in sequenziale dal processore master o in alternativa puo essererisolto con un algoritmo LU parallelo visto nel paragrafo precedente. Una voltadeterminato xs, e possibile eseguire il punto 4 in parallelo; infatti i p sistemida risolvere sono indipendenti e la loro risoluzione puo essere intrinsecamentedistribuita tra i processori disponibili.

Osserviamo infine che nella pratica, le matrici Ai non verranno mai fisi-camente invertite. Infatti, lo scopo e’ quello di calcolare i prodotti A−1

i Bi eA−1

i bi; notiamo che calcolare A−1

i bi corrisponde a determinare la soulzione delsistema lineare Aix = bi. Analogamente, se si rappresenta la matrice Bi comel’insieme delle sue colonne [Bi1Bi2 · · ·Bin], dove Bin e’ l’n-esima colonna dellamatrice Bi, e’ possibile calcolare le colonne del prodotto A−1

i Bi risolvendo gli nsistemi lineari Aixj = Bij per j = 1, · · · , n. Si noti che e’ sufficiente calcolare ladecomposizione LU di Ai inizialmente e riutilizzarla per risolvere ogni sistemalineare.

Si puo schematizzare l’algoritmo risolutivo parallelo nel seguente modo:

Distribuzione dei dati: Per semplicita si suppone che il numero di pro-cessori coincida con p. Ogni processore i memorizza i seguenti elementi delproblema: Ai, Bi, Ci, bi. Memorizza inoltre le matrici Li e Ui ottenute dalladecomposizione LU della matrice Ai. Il processore master memorizza anche As

e bs.

1 Ogni processore esegue la decomposizione LU della propria matrice Ai

generando le due matrici Li e Ui;

2 Ogni processore i-esimo calcola il termine di = CiA−1

i bi sfruttando ladecomposizione LU calcolata al punto 1;

3 Tramite una comunicazione di tipo Reduce con somma sui termini di, ilprocessore master ottiene la somma

∑p

i=1CiA

−1

i bi e con questa calcola b;

4 Ogni processore i-esimo calcola il termine Di = CiA−1

i Bi mediante larisoluzione degli n sistemi lineari Aixj = Bij ;

Page 19: Performance dei sistemi di calcolo

2.4. AUTOVALORI DI UNA MATRICE TRIDIAGONALE 19

5 Tramite una comunicazione di tipo Reduce con somma sui termini Di, ilprocessore master ottiene la somma

∑p

i=1CiA

−1

i Bi e con questa calcola

A;

6 Il processore master risolve il sistema lineare Axs = b per determinare xs;

7 La soluzione xs viene viene comunicato a tutti i processori tramite unaBroadcast;

8 Ogni processore i-esimo risolve il sistema lineare Aixi = bi − Bixs;

9 Le componenti xi vengono riunite in un unico vettore tramite una co-municazione di tipo Gather. Unite assieme a xs, formano la soluzione alproblema iniziale.

L’algoritmo presentato in questo paragrafo e’ un ottimo esempio di comesia possibile esprimere il parallelismo intrinseco ad un problema, anche quandoquesto non e’ apparenemente evidente.

2.4 Autovalori di una matrice tridiagonale

In questo paragrafo e presentato un algoritmo parallelo per il calcolo degli auto-valore di una matrice tridiagonale. L’algoritmo sara sviluppato secondo il prin-cipio del divide and conquer. Al termine del capitolo si vedra come lo sviluppodi un nuovo algoritmo parallelo puo portare a nuove idee che permettono dimigliorare anche la versione sequenziale del metodo.

Verranno ora dati alcuni cennti preliminari al calcolo degli autovalori. SiaA una matrice quadrata in Rnxn e x ∈ Rn. Tutti i valori λ che soddisfanol’equazione

Ax = λx (2.3)

per un qualche x sono detti autovalori della matrice A. Una matrice di di-mensione n ha n autovalori λ1, . . . , λn. Ogni vettore x che soddisfa l’equazioneprecedente si chiama autovettore della matrice A.

Riscrivendo l’equazione 2.3 come

(A − λI)x = 0

e possibile vedere che gli autovalori di A possono essere calcolati richiedendoche

det(A − λI) = 0.

Questo da origine ad un polinomio di grado n nella variabile λ le cui radici sonogli autovalori cercati. E chiaro che un simile metodo di calcolo e di scarsa utilitapratica, dal momento che non esistono formule esplicite per il calcolo delle radicidi un polinomio di grado superiore al 4 e che il problema e estremamente malcondizionato. La tecnica che viene piu frequentemente adottata e il metodo del

Page 20: Performance dei sistemi di calcolo

20 CHAPTER 2. ALGORITMI PARALLELI

QR iterativo. Esso e un metodo estremamente generico che permette il calcolodegli autovalori di generiche matrici senza una struttura particolare.

In numerose applicazioni pratiche, tuttavia, e spesso necesario il calcolo degliautovalori di una matrice trigiagonale simmetrica, ovvero una matrice con laseguente forma:

T =

d1 β1

β1 d2 β2

β2 d3 β3

. . .

βn−2 dn−1 βn−1

βn−1 dn

Tali matrici derivano solitamente dai problemi di Sturm-Liouville o da prob-lemi di dinamica strutturale. Un’altra applicazione di recente interesse riguardai motori di ricerca, come Google, in cui per determinare il PageRank (ovvero unastima di quanto una pagina e linkata nel web) vengono ricercati gli autovaloripiu grandi della matrice rappresentante il grafo delle connessioni nel web.

L’obbiettivo di questo paragrafo e di analizzare un metodo parallelo per ilcalcolo degli autovalori di T , utilizzando il principio del divide and conquer: sidividera la matrice in due sottomatrici di meta dimensione e si calcoleranno gliautovalori di queste due matrici (eventualmente riapplicando ricorsivamente ilmetodo) in modo completamente indipendente l’uno dall’altro. In questo modoviene espresso un parallelismo. Infine verranno ”unite” le informazioni parzialiottenute da ciascun sottoproblema, per determinare gli autovettori della matriceoriginale T , mediante una tecnica che illustreremo in seguito.

Sia k = n2, β = βk, e1, ek vettori della base canonica di Rk, ovvero:

e1 =

10...0

, ek =

00...1

Se si definiscono

T1 =

d1 β1

β1 d2

. . .

. . .. . . βk−2

βk−2 dk−1 − β

, T2 =

dk − β βk

βk dk+1

. . .

. . .. . . βn−1

βn−1 dn

La matrice T puo essere riscritta come:

Page 21: Performance dei sistemi di calcolo

2.4. AUTOVALORI DI UNA MATRICE TRIDIAGONALE 21

T =

(

T1 00 T2

)

+

0 0 0 0 0 00 0 0 0 0 00 0 β β 0 00 0 β β 0 00 0 0 0 0 00 0 0 0 0 0

=

=

(

T1 00 T2

)

+ β

(

ekeTk ekeT

1

e1eTk e1e

T1

)

=

=

(

T1 00 T2

)

+ β

(

ek

e1

)

(

eTk eT

1

)

Come primo passo del metodo, si considerino le matrici T1 e T2 e se ne cal-colino gli autovalori e gli autovettori. Con questo passo si e spezzato il problemain due sottoproblemi indipendenti; tali problemi possono essere risolti con unmetodo diretto, ciascuno su un processore diverso, oppure si puo avviare unaprocedura ricorsiva di suddivisioni in sottoproblemi sempre piu piccoli, come nelcaso dell’algoritmo di ordinamento visto precedentemente. Una volta terminatoquesto passo, e necessario sviluppare un metodo che permetta di calcolare gliautovalori della matrice originale a partire dall’informazione parziale che si hadai sottoproblemi. Avendo calcolato gli autovalori e gli autovettori, e possibilerappresentare T1 e T2 come

T1 = Q1D1QT1 T2 = Q2D2Q

T2

dove D1 e D2 sono matrici diagonali i cui elementi sono gli autovalori diT1 e T2 mentre Q1 e Q2 sono due matrici unitarie le cui righe sono i rispettiviautovettori di T1 e T2.

Allora la matrice T puo essere scritta come:

T =

(

Q1D1QT1 0

0 Q2D2QT2

)

+ β

(

ek

e1

)

(

eTk eT

1

)

Ricordando le proprieta delle matrici unitarie, osserviamo che Q1QT1 = I.

Inoltre anche la matrice(

Q1 00 Q2

)

e unitaria, essendo Q1 e Q2 unitarie. La precedente equazione diventa allora:

T =

(

Q1 00 Q2

) ((

D1 00 D2

)

+ β

(

QT1 0

0 QT2

) (

ek

e1

)

(

eTk eT

1

)

(

Q1 00 Q2

)) (

QT1 0

0 QT2

)

=

(

Q1 00 Q2

) ((

D1 00 D2

)

+ β

(

q1

q2

)

(

qT1 qT

2

)

) (

QT1 0

0 QT2

)

Page 22: Performance dei sistemi di calcolo

22 CHAPTER 2. ALGORITMI PARALLELI

dove q1 = QT1 ek e q2 = QT

2 e1.Di conseguenza, gli autovalori di T saranno uguali gli autovalori di

(

D1 00 D2

)

+ β

(

q1

q2

)

(

qT1 qT

2

)

Quindi il problema del calcolo degli autovalori di T si riduce al calcolo degliautovalori di

D + βzzT

con D matrice diagonale i cui elementi sono δ1, · · · , δn, β scalare e z vettoredi norma unitaria e con elementi non nulli.

Applicando la definizione di autovalore, e possibile scrivere l’equazione nell’incognitaλ (gli autovalori)

(D + βzzT )x = λx (2.4)

(D − λI)x = −βz(zT x) (2.5)

x = −β(zT x)(D − λI)−1z (2.6)

zT x = −β(zT x)zT (D − λI)−1z (2.7)

1 = −βzT (D − λI)−1z (2.8)

Quindi ogni autovalore di T e autovalore di D + βzzT e si puo determinarerisolvendo l’equazione

1 + βzT (D − λI)−1z = 0

Questa equazione puo essere riscritta in forma scalare:

f(λ) = 1 + β

n∑

i=1

z2i

δi − λ

Gli zeri dell’equazione 2.4, che chiameremo δ1, . . . , δn, godono di un’ottimaproprieta di localizzazione, ovvero e noto a priori che essi sono reali, distinti econtenuti ciascuno nell’intervallo da (δi, δi+1). Piu precisamente, δi ∈ (δi, δi+1)per i < n, e δn > δn. Inoltre, all’interno di tali intervalli, la funzione f(λ)e monotona crescente. Grazie a queste proprieta, risulta possibile utilizzaremolto efficacemente un metodo iterativo per il calcolo degli zeri dell’equazionenon lineare.

La parallelizzazione di questo algoritmo e intrinseca nel metodo: lo schemae di tipo divide and conquer, quindi la parallelizzazione avviene mediante suddi-visione ricorsiva in sottoproblemi fino al raggiungimento nel numero di proces-sori disponibili. La parallelizzazione della fase di ”unione” dei sottoproblemi,ovvero la risoluzione dell’equazione secolare 2.4, e anch’essa intrinsecamenteparallela, in quanto i problemi di determinazione degli zeri δ1, . . . , δn sono in-dipendenti e possono essere assegnati a processori diversi.

Per ricapitolare, i passi per la parallelizzazione dell’algoritmo sono:

Page 23: Performance dei sistemi di calcolo

2.4. AUTOVALORI DI UNA MATRICE TRIDIAGONALE 23

1 Il processore P0 genera le matrici T1 e T2 e comunica a P1 la matrice T2;

2 In parallelo, ogni processore calcola gli autovalori e gli autovettori dellapropria matrice Ti, generando Di e Qi;

3 Ogni processore calcola il vettore qi;

4 Di e qi vengono raccolti e comunicati a tutti i processori mediante unacomunicazione di tipo All Gather;

5 Ogni processore determina un sottoinsieme delle soluzioni δ1, . . . , δn.

Page 24: Performance dei sistemi di calcolo

24 CHAPTER 2. ALGORITMI PARALLELI

Page 25: Performance dei sistemi di calcolo

Chapter 3

Librerie di calcolo

Le librerie di calcolo scientifico sono una vasta collezione di funzioni altamenteottimizzate che svolgono i principali compiti computazionali basilari del calcoloscientifico. Tra questi compiti si possono citare la risoluzione di sistemi linearicon metodi diretti e iterativi, le fattorizzazioni di matrice (LU, Cholesky, QR,SVD, Schur, generalized Schur, ...), il calcolo di autovalori, dei valori singolari,dei minimi quadrati, la stima del condizionamento, ecc...

Le routines delle librerie scientifiche hanno le seguenti caratteristiche:

- sono multipiattaforma;

- esistono delle implementazioni specificamente ottimizzate per una piattaforma(generalmente fornita dal produttore dellhardware), che hanno la stessasintassi della versione standard;

- per ogni routine, esistono versioni specifiche in base al tipo di dato datrattare: singola/doppia precisione, numeri reali/complessi;

- per ogni routine, esistono versioni specifiche in base alla struttura dellamatrice: piena, a banda, sparsa.

Il sito www.netlib.org e il repository ufficiale per le principali librerie dicalcolo. Da questo sito e possibile scaricare le librerie, la documentazione edei programmi di esempio.

3.1 Descrizione delle librerie sequenziali

• BLAS (Basic Linear Algebra Subprograms)E’ la libreria di livello piu basso e contiene le routines basilari per il trat-tamento di vettori e matrici. E’ suddivisa in 3 livelli:

- Level 1 BLAS: contiene le operazioni tra due vettori (somma,prodotto scalare, ...);

25

Page 26: Performance dei sistemi di calcolo

26 CHAPTER 3. LIBRERIE DI CALCOLO

- Level 2 BLAS: contiene le operazioni tra matrice e vettore;

- Level 3 BLAS: contiene le operazioni tra due matrici (prodotto rigaper colonna, somma di matrici)

Tutte le librerie di calcolo si basano sulla BLAS per il trattamento dellematrici e dei vettori, quindi e importante che questa libreria sia partico-larmente ottimizzata. Per questa ragione, i produttori di microprocessoriforniscono spesso una versione della BLAS ottimizzata per le loro CPU.Un’altra possibilita e fornita dalla ATLAS (Automatically Tuning LinearAlgebra Software); e un’implementazione della BLAS che in fase di in-stallazione esegue dei test sul processore per determinare i suoi parametriottimali di funzionamento per quello specifico sistema.

• LINPACK (LINear PACKage)E’ la libreria che contiene le funzioni di algebra lineare relative alla risoluzionedi sistemi lineari e alle fattorizzazioni di matrici. Attualmente e stata sor-passata dalla LAPACK, che unisce le funzioni della LINPACK e dellaEISPACK.

• EISPACKE’ la libreria che contiene le funzioni per il calcolo di autovalori e autovet-tori di matrici. Attualmente e inclusa nella LAPACK

• LAPACK (Linear Algebra PACKage)E’ la principale libreria di calcolo scientifico per architetture sequenziali.E’ nata come unione delle librerie LINPACK e EISPACK e attualmentecomprende sobroutines per: risoluzione di sistemi lineari con metodi di-retti e iterativi, fattorizzazioni di matrice (LU, Cholesky, QR, SVD, Schur,generalized Schur, ...), calcolo di autovalori, di valori singolari, di min-imi quadrati, la stima del condizionamento di matrice. LAPACK e ingrado di trattare matrici dense e a banda, ma non matrici sparse. Il sitohttp://www.cs.colorado.edu/ jessup/lapack/ contiene un motore di ricercaper selezionare la piu appropriata routine LAPACK in base al compito dasvolgere, alla struttura della matrice e al tipo di dato. LAPACK si ap-poggia alla BLAS per il trattamento delle matrici.

• CLAPACK e LAPACK++Sono un front-end scritto rispettivamente in C e C++ della libreria LA-PACK. La libreria LAPACK e scritta in Fortran 77. Per facilitare il suoutilizzo all’interno di codici C o C++ sono state scritte queste librerie checonsistono in funzioni di interfaccia che permettono di chiamare con laconvenzione C e C++ le funzioni Fortran della LAPACK.

• ESSLE’ la versione di LAPACK fornita da IBM, ottimizzata per i propri calco-latori.

Page 27: Performance dei sistemi di calcolo

3.2. DESCRIZIONE DELLE LIBRERIE PARALLELE 27

• MKL (Math Kernel Library)E’ la versione di LAPACK fornita dalla Intel, ottimizzata per i propriprocessori.

3.2 Descrizione delle librerie parallele

• BLACS (Basic Linear Algebra Communication Subprograms)E una libreria wrapper che standardizza le routines di comunicazione mes-sage passing su macchine multiprocessore a memoria distribuita. In prat-ica fornisce un insieme di funzioni standard di comunicazione che sonoindipendenti dalla piattaforma e dal protocollo di comunicazione (MPI,PVM, MPL, NX, ...). Su questa libreria si appoggiano tutte le librerie dicalcolo parallelo, in modo da essere indipendenti dal sistema.

• PBLAS (Parallel Basic Linear Algebra Subprograms)E limplementazione parallela della libreria BLAS; anchessa e divisa nei3 livelli in base al tipo di operazione da svolgere. PBLAS basa le suecomunicazioni sulla BLACS.

• ScaLAPACKE la principale libreria di calcolo scientifico per architetture parallele. Con-tiene tutte le funzioni della libreria LAPACK in versione multiprocessore.Si basa sulla libreria BLAS per le operazioni interne sequenziali e sullaPBLAS per le operazioni interne parallele.

• ParPACKSiccome la ScaLAPACK, come la LAPACK, non e in grado di trattarematrici sparse, e stata creata la libreria ParPACK per il calcolo di auto-valori e autovettori di matrici sparse. Questa libreria contiene sia funzionisequenziali che parallele.

• CAPSS e MFACTSono due librerie, sia sequenziali che parallele, per la risoluzione di sistemilineari sparsi con metodi diretti.

Page 28: Performance dei sistemi di calcolo

28 CHAPTER 3. LIBRERIE DI CALCOLO

Page 29: Performance dei sistemi di calcolo

Chapter 4

Programmazione dei sistemi

a memoria condivisa

In questo capitolo saranno analizzati i sistemi a memoria condivisa: dopo unabreve descrizione introduttiva di tali sistemi, verranno analizzate le analogiee le differenze nello sviluppo di codice parallelo rispetto ai sistemi a memoriadistribuita. Infine verra introdotta la programmazione multithread quale tecnicaper programmare tali sistemi; in particolare saranno introdotti i thread POSIX,detti pthread.

I sistemi a memoria condivisa sono macchine multiprocessore dotate di un’unicamemoria RAM accessibile a tutti i processori. Fino all’anno 2005, questi sistemierano la minoranza rispetto a tutte le macchine multiprocessore. Di solito sitrattava di calcolatori con due processori accoppiati sullo stesso nodo, fino adarrivare a nodi con 8 o 16 processori su alcune architetture IBM. Recentemente,pero, i sistemi a memoria condivisa stanno diventando la maggior parte dellemacchine multiprocessore. Infatti in tutti i processori di moderna generazione,anche quelli utilizzati nei comuni PC domestici, sono presenti almeno 2 o 4cores, ovvero processori fisicamente integrati nello stesso chip, ma in grado dioperare indipendentemente l’uno dall’altro. La tendenza sara quella negli annifuturi di incrementare la potenza dei microprocessori aumentando il numero dicores anziche rendendo piu veloci i singoli cores. Lo stesso vale anche per lenuove categorie di processori: i chip grafici usati nelle schede video ma sfrut-tati anche per applicazioni di calcolo, i cell processor introdotti dalla IBM per isuoi server ma usati anche nella Playstation 3, gli FPGA... Attualmente e raroche su un calcolatore sia necessario eseguire un unico compito specializzato contutta la potenza di elaborazione disponibile. Piu frequentemente le applicazionisvolgono tante operazioni contemporaneamente. Per esempio, quando si navigasu una pagina web, il browser sta contemporaneamente eseguendo i comandidell’utente, caricando una pagina, visualizzando il testo, muovendo tutti gli el-ementi grafici animati e riproducendo i contenuti multimediali. Essendo tanticompiti eseguiti contemporaneamente, ha senso avere a disposizione tanti cores,

29

Page 30: Performance dei sistemi di calcolo

30CHAPTER 4. PROGRAMMAZIONE DEI SISTEMI A MEMORIA CONDIVISA

ognuno dei quali potra svolgere uno o pi di quei compiti. Vedremo tra poco chequesti ”compiti” sono chiamati thread.

Ci sono due differenze fondamentali rispetto ai sistemi a memoria distribuita:

1 all’aumentare del numero di processori, aumenta la potenza di calcolodisponibile, ma non aumenta la quantita di memoria disponibile. Quindile risorse di calcolo sono molto abbondanti, mentre le risorse di memoriatendono ad essere il collo di bottiglia.

2 Le comunicazioni tra i processori sono piu efficienti. Infatti non e neces-sario che vi sia un invio di dati tra due processori (come avviene invece nelparadigma Message Passing) poiche ogni processore puo accedere a tuttala memoria e leggere cosı direttamente i dati di cui ha bisogno. Le comu-nicazioni servono quindi unicamente per sincronizzare i processori, ovveroun processore segnala agli altri quando ha finito di elaborare i suoi dati,in modo che gli altri sappiano quando sono disponibili e possono essereletti dalla memoria comune.

Queste due osservazioni cambiano il modo con cui si programmano i sistemia memoria condivisa rispetto a quelli a memoria distribuita, ma non cambianoi principi di parallelismo, ovvero le idee e le tecniche che si seguono per paral-lelizzare un algoritmo.

4.1 Programmi e processi

Un programma e costituito dal codice oggetto generato dalla compilazione deisorgenti. Esso e un’entite statica, che rimane immutata durante l’esecuzione.Un processo, invece, e un’entite dinamica, che dipende dai dati che vengonoelaborati, e dalle operazioni eseguite su di essi. Il processo e quindi caratter-izzato, oltre che dal codice eseguibile, dall’insieme di tutte le informazioni chene definiscono lo stato, come il contenuto della memoria indirizzata, i thread, idescrittori dei file e delle periferiche in uso. Sostanzialmente, quindi, il processoe la rappresentazione che il sistema operativo ha di un programma in esecuzione.

4.2 Processi e thread

Il concetto di processo e associato a quello di thread (abbreviazione di threadof execution, filo dell’esecuzione), con cui si intende l’unita granulare in cui unprocesso puo essere suddiviso, e che puo essere eseguito in parallelo ad altrithread. In altre parole, un thread e una parte del processo che viene eseguitain maniera concorrente ed indipendente internamente al processo stesso. Iltermine inglese rende bene l’idea, in quanto si rifa visivamente al concetto difune composta da vari fili attorcigliati: se la fune e il processo in esecuzione,allora i singoli fili che la compongono sono i thread.

Un processo ha sempre almeno un thread (se stesso), ma in alcuni casi unprocesso puo avere piu thread che vengono eseguiti in parallelo.

Page 31: Performance dei sistemi di calcolo

4.3. SUPPORTO DEL SISTEMA OPERATIVO 31

Una prima differenza fra thread e processi consiste nel modo con cui essicondividono le risorse. Mentre i processi sono di solito fra loro indipendenti,utilizzando diverse aree di memoria ed interagendo soltanto mediante appositimeccanismi di comunicazione messi a disposizione dal sistema, al contrario ithread tipicamente condividono le medesime informazioni di stato, la memoriaed altre risorse di sistema.

L’altra differenza sostanziale e insita nel meccanismo di attivazione: lacreazione di un nuovo processo e sempre onerosa per il sistema, in quanto devonoessere allocate le risorse necessarie alla sua esecuzione (allocazione di memoria,riferimenti alle periferiche, e cosı via, operazioni tipicamente onerose); il threadinvece e parte del processo, e quindi una sua nuova attivazione viene effettuatain tempi ridottissimi a costi minimi.

Le definizioni sono le seguenti:Il processo e l’oggetto del sistema operativo a cui sono assegnate tutte le

risorse di sistema per l’esecuzione di un programma, tranne la CPU. Il threade l’oggetto del sistema operativo o dell’applicazione a cui e assegnata la CPUper l’esecuzione. In un sistema che non supporta i thread, se si vuole eseguirecontemporaneamente piu volte lo stesso programma, e necessario creare piuprocessi basati sullo stesso programma. Tale tecnica funziona, ma e dispendiosadi risorse, sia perch ogni processo deve allocare le proprie risorse, sia perch percomunicare tra i vari processi e necessario eseguire delle relativamente lentechiamate di sistema, sia perch la commutazione di contesto tra thread dellostesso processo e piu veloce che tra thread di processi distinti.

Avendo piu thread nello stesso processo, si puo ottenere lo stesso risultatoallocando una sola volta le risorse necessarie, e scambiando i dati tra i threadtramite la memoria del processo, che e accessibile a tutti i suoi thread.

Un esempio di applicazione che puo far uso di piu thread e un browser Web,che usa un thread distinto per scaricare ogni immagine in una pagina Web checontiene piu immagini.

Un altro esempio e costituito dai processi server, spesso chiamati servizi odaemon, che possono rispondere contemporaneamente alle richieste provenientida piu utenti.

In un sistema multiprocessore, si possono avere miglioramenti prestazion-ali, grazie al parallelismo fisico dei thread. Tuttavia, l’applicazione deve essereprogettata in modo da suddividere tra i thread il carico di elaborazione. Taleprogettazione e difficile e frequentemente soggetta a errori, e va progettata conmolta cura.

4.3 Supporto del sistema operativo

I sistemi operativi si classificano nel seguente modo in base al supporto cheoffrono a processi e thread:

• Monotasking: non sono supportati n processi n thread; si puo lanciareun solo programma per volta.

Page 32: Performance dei sistemi di calcolo

32CHAPTER 4. PROGRAMMAZIONE DEI SISTEMI A MEMORIA CONDIVISA

• Multitasking cooperativo: sono supportati i processi, ma non i thread,e ogni processo mantiene la CPU finch non la rilascia spontaneamente.

• Multitasking preventivo: sono supportati i processi, ma non i thread,e ogni processo mantiene la CPU finch non la rilascia spontaneamente ofinch il sistema operativo sospende il processo per passare la CPU a unaltro processo.

• Multithreaded: sono supportati sia i processi, che i thread.

4.4 Stati di un thread

In un sistema operativo multitasking, ci sono piu thread contemporaneamentein esecuzione. Di questi, al massimo un numero pari al numero di processoripuo avere effettivamente il controllo di un processore. Quindi i diversi processipossono utilizzare il processore per un numero limitato di tempo, per questomotivo i processi vengono interrotti, messi in pausa e richiamati secondo deglialgoritmi di schedulazione.

Gli stati in cui un thread si puo trovare sono:

• esecuzione (running): il thread ha il controllo di un processore;

• pronto (ready): il thread e pronto ad essere eseguito, ed e in attesa chelo scheduler lo metta in esecuzione ;

• bloccato (suspended): il thread ha eseguito una chiamata di sistema,ed e fermo in attesa del risultato ;

Con commutazione di contesto (Context switch) si indica il meccanismoper cui un thread in esecuzione viene fermato (perch ha eseguito una chiamatadi sistema o perch lo scheduler ha deciso di eseguire un altro thread), e un altropronto viene messo in esecuzione.

4.5 Sincronizzazione di thread

Ci sono due ragioni principali per sincronizzare i thread di un processo. Laprima e insita nella struttura degli algoritmi utilizzati e nella logica di pro-grammazione, ed e la stessa ragione per cui si sincronizzano i processori in unambiente a memoria distribuita: sono quelle comunicazioni che permettono adun processore di informare gli altri che il proprio compito e terminato e che i datiprodotti sono disponibili. Questo tipo di comunicazione avviene per mezzo dieventi, ovvero un meccanismo di comunicazione con cui un processore segnalaagli altri che una certa condizione si e verificata. Ad esempio, nell’algoritmo didecomposizione LU parallela, occorre che i processori inizino contemporanea-mente ad annullare una nuova riga quando la riga precedente e stata annullatada tutti i processori. In ambiente a memoria distribuita, questa sincronizzazione

Page 33: Performance dei sistemi di calcolo

4.5. SINCRONIZZAZIONE DI THREAD 33

avveniva intrinsecamente utilizzando le comunicazioni di MPI nel momento dellaBroadcast. Questa comunicazione non aveva solo il compito di trasferire datitra i processori, ma anche di sincronizzarli; la Broadcast e infatti una comuni-cazione bloccante e quindi termina quando tutti i processori hanno ricevuto idati, ovvero quando tutti i processori sono arrivati a quel punto di esecuzione.Gli eventi funzionano come una qualunque comunicazione sincrona senza chepero vi sia un trasferimento di dati.

La seconda ragione e piu sottile e non si presenta nel caso di sistemi a memo-ria distribuita; la sua funzione e quella di stabilire un ordine ben determinatoa come due o piu processori possono utilizzare la stessa area di memoria o, piuin generale, la stessa risorsa. Per queste situazioni vengono utilizzati i mutexe i semafori. Nei sistemi a memoria condivisa si hanno piu processori, ma unasola memoria, un solo hard disk, un solo monitor, tastiera, mouse, scheda audio(queste sono le risorse); piu processori potrebbero avere la necessita di accedereallo stesso file o a una stessa risorsa. I mutex e i semafori serializzano l’accessoalle risorse, ovvero premettono ad un solo processore alla volta di accedere allarisorse richesta.

Per illustrare la necessita dei mutex consideriamo il seguente esempio: sup-poniamo di avere una variabile v che ogni thread incrementa di un’unita ognivolta che ha eseguito un certo calcolo. Potra capitare la situazione in cui duethread devono incrementare contemporaneamente quella variabile. L’incrementodi una variabile non e un’ operazione atomica, ovvero richiede piu istruzioni peressere eseguita.

clk v thread 1 thread 21 100 Leggi v ...2 100 Somma 1 Leggi v3 100 Scrivi v Somma 14 101 ... Scrivi v5 101 ... ...

Dalla tabella, si puo vedere come il thread 2 ha iniziato ad eseguire l’incrementodi v prima che il thread 1 lo completasse. Quando il thread 2 ha letto il val-ore di v per incrementarlo, il thread 1 non aveva ancora scritto il nuovo valoreincrementato e cosı il thread 2 si trovera ad incrementare un valore vecchio div. Il risultato e che nonostante i due incrementi, la variabile v e passata da dalvalore 100 al valore 101 anziche 102. Occorre quindi un oggetto che permettadi creare un’ordine di accesso alla variabile v. Questo oggetto si chiama mutex(MUTual EXecution); un thread cattura il mutex e gli altri thread non possonocatturarlo finche il primo thread non l’ha rilasciato. La scrittura corretta delcodice per l’incremento parallelo della variabile v e scritto in questo modo:

void *MyThread(void *arg)

{

...

mutex_lock(IncMutex);

Page 34: Performance dei sistemi di calcolo

34CHAPTER 4. PROGRAMMAZIONE DEI SISTEMI A MEMORIA CONDIVISA

v = v + 1;

mutex_unlock(IncMutex);

...

}

Il primo thread che arriva all’istruzione mutex lock cattura il mutex e tutti glialtri thread rimangono bloccati alla loro istruzione mutex lock finche il primothread non l’ha rilasciato con l’istruzione mutex unlock. Ovviamente questecomunicazioni, come i messaggi di MPI, riducono le prestazioni del programmapoiche pongono i processori in uno stato di attesa senza che vengano eseguiteistruzioni utili. Con l’uso dei mutex, la seguenza di istruzioni diventerebbe

clk v thread 1 thread 21 100 mutex lock ...2 100 Leggi v mutex lock3 100 Somma 1 in attesa4 101 Scrivi v in attesa5 101 mutex unlock in attesa6 101 ... Leggi v7 101 ... Somma 18 101 ... Scrivi v9 102 ... mutex unlock

Da questa tabella si vede come l’accesso alla risorsa sia stata serializzataanche se questo ha introdotto degli stati di attesa nel secondo thread.

I Semafori sono un caso piu generico dei mutex. Il mutex consente adun solo thread di accedere ad una risorsa, mentre il semaforo consente ad unmassimo numero predeterminato di thread di accedere alla risorsa protetta.Sono tipicamente utilizzati in ambienti multiutente o client/server quando c’eun server che deve rispondere contemporaneamente ad un grande numero dirichieste (ad esempio un server web) avendo un numero limitato di risorse. Adesempio se un server web riesce a gestire un massimo di 100 connessioni con-temporaneamente, ma ci sono 200 utenti collegati, verra utilizzato un semaforoper serializzare le richieste di 200 utenti (un thread per utente) sulle 100 risorsedisponibili.

Generalmente siamo portati ad abbinare il concetto di thread al concetto diprocessore o core. Ovvero, se abbiamo a disposizione n processori o n cores puosembrare logico creare n threads, in modo che ciascuno venga eseguito su unprocessore. Tuttavia, e utile osservare che tale assunzione non e sempre vera:infatti spesso e conveniente attivare piu thread dei processori disponibili. Se sihanno piu thread per processore e uno o piu di questi thread e bloccato perchefermo in una sincronizzazione o perche il carico di lavoro non e ben bilanciato, ilprocessore puo dedicare le sue risorse agli altri threads e cosı non rimane in statodi attesa. Questa e un’utile tecnica per bilanciare automaticamente il carico dilavoro.