Chapter 1

Performance dei sistemi di
calcolo

1.1     Misura dal costo computazionale di un al-
        goritmo
Quando si...
2               CHAPTER 1. PERFORMANCE DEI SISTEMI DI CALCOLO

1.2      Misura della velocit` di elaborazione
            ...
1.4. CACHE DEL PROCESSORE                                                            3

Purtroppo, tale processore ha una ...
4              CHAPTER 1. PERFORMANCE DEI SISTEMI DI CALCOLO

le prestazioni dei programmi che scriveremo. La cache sfrutt...
1.4. CACHE DEL PROCESSORE                                                         5

        c[i] = 0.0;
        for (j = ...
6             CHAPTER 1. PERFORMANCE DEI SISTEMI DI CALCOLO

      }
}

    Possiamo notare come gli accessi agli elementi...
1.5. METRICA DELLE PRESTAZIONI PARALLELE                                         7

   • sbilanciamento del carico di lavo...
8                 CHAPTER 1. PERFORMANCE DEI SISTEMI DI CALCOLO


                 16


                 14


            ...
1.7. CONSIDERAZIONI SULLA PARALLELIZZAZIONE DEGLI ALGORITMI9


                  1

                 0.9

                ...
10                  CHAPTER 1. PERFORMANCE DEI SISTEMI DI CALCOLO


                    7


                    6


      ...
Chapter 2

Algoritmi paralleli

2.1     Ordinamento di un vettore
Uno degli approcci che pi` comunemente si utilizzano per...
12                                   CHAPTER 2. ALGORITMI PARALLELI




                                   Figure 2.1:


 ...
2.2. DECOMPOSIZIONE LU                                                           13

   • Per ogni coppia di processori, i...
14                                    CHAPTER 2. ALGORITMI PARALLELI

void ludcmp(double a[][N], double l[][N], int n)
{
i...
2.2. DECOMPOSIZIONE LU                                                           15




                                  ...
16                                          CHAPTER 2. ALGORITMI PARALLELI

     4 Memorizzare nella matrice L (anch’essa ...
2.3. RISOLUZIONE DI UN SISTEMA LINEARE A FRECCIA                                             17

                         ...
18                                          CHAPTER 2. ALGORITMI PARALLELI

     1 Invertire le matrici A1 , . . . , Ap ;
...
2.4. AUTOVALORI DI UNA MATRICE TRIDIAGONALE                                      19

   5 Tramite una comunicazione di tip...
20                                               CHAPTER 2. ALGORITMI PARALLELI

QR iterativo. Esso ` un metodo estremamen...
2.4. AUTOVALORI DI UNA MATRICE TRIDIAGONALE                                                              21


            ...
22                                         CHAPTER 2. ALGORITMI PARALLELI

     dove q 1 = QT ek e q 2 = QT e1 .
         ...
2.4. AUTOVALORI DI UNA MATRICE TRIDIAGONALE                                       23

  1 Il processore P0 genera le matri...
24   CHAPTER 2. ALGORITMI PARALLELI
Chapter 3

Librerie di calcolo

Le librerie di calcolo scientifico sono una vasta collezione di funzioni altamente
ottimizz...
26                                       CHAPTER 3. LIBRERIE DI CALCOLO

          - Level 2 BLAS: contiene le operazioni ...
3.2. DESCRIZIONE DELLE LIBRERIE PARALLELE                                       27

  • MKL (Math Kernel Library)
    E’ l...
28   CHAPTER 3. LIBRERIE DI CALCOLO
Chapter 4

Programmazione dei sistemi
a memoria condivisa

In questo capitolo saranno analizzati i sistemi a memoria condi...
30CHAPTER 4. PROGRAMMAZIONE DEI SISTEMI A MEMORIA CONDIVISA

ognuno dei quali potr` svolgere uno o pi di quei compiti. Ved...
4.3. SUPPORTO DEL SISTEMA OPERATIVO                                              31

    Una prima differenza fra thread e ...
32CHAPTER 4. PROGRAMMAZIONE DEI SISTEMI A MEMORIA CONDIVISA

   • Multitasking cooperativo: sono supportati i processi, ma...
4.5. SINCRONIZZAZIONE DI THREAD                                                  33

avveniva intrinsecamente utilizzando ...
34CHAPTER 4. PROGRAMMAZIONE DEI SISTEMI A MEMORIA CONDIVISA

v = v + 1;
mutex_unlock(IncMutex);
 ...
}

    Il primo threa...
Upcoming SlideShare
Loading in …5
×

Performance dei sistemi di calcolo

2,289 views

Published on

0 Comments
0 Likes
Statistics
Notes
  • Be the first to comment

  • Be the first to like this

No Downloads
Views
Total views
2,289
On SlideShare
0
From Embeds
0
Number of Embeds
6
Actions
Shares
0
Downloads
63
Comments
0
Likes
0
Embeds 0
No embeds

No notes for slide

Performance dei sistemi di calcolo

  1. 1. Chapter 1 Performance dei sistemi di calcolo 1.1 Misura dal costo computazionale di un al- goritmo Quando si studia un nuovo algoritmo ` di fondamentale importanza stimare il e numero di operazioni necessarie alla sua esecuzione, in funzione della dimensione del problema. L’unit` di misura minima per il costo computazionale ` il a e 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` di misura sono: a 1 MFLOP = 106 FLOPS 1 GFLOP = 109 FLOPS 1 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 floating point, quindi in totale il costo computazionale ` di 2n FLOPS. e Esercizio 1. Si mostri che il costo computazionale di un prodotto matrice per vettore ` di 2n2 FLOPS. e Esercizio 2. Si mostri che il costo computazionale di un prodotto riga per colonna tra due matrici ` di 2n3 FLOPS. e 1
  2. 2. 2 CHAPTER 1. PERFORMANCE DEI SISTEMI DI CALCOLO 1.2 Misura della velocit` di elaborazione a La velocit` di elaborazione di un sistema si misura in numero di operazioni float- a ing point che possono essere eseguite in 1 secondo, ovvero i FLOP/S. I multipli comunemente utilizzati sono: 1 MFLOP/S = 106 FLOPS al secondo 1 GFLOP/S = 109 FLOPS al secondo 1 TFLOP/S = 1012 FLOPS al secondo Esempio 2. La velocit` di punta di un processore Intel Pentium D 3GHz a ` di 24 GFLOP/S. Infatti questo processore ` dotato di 2 cores che operano e e contemporaneamente. Ciascun core ` in grado di eseguire in un ciclo clock e 4 operazioni floating point sfruttando le istruzioni SIMD. Di conseguenza, il numero totale di operazioni al secondo ` di: 2 · 4 · 3 · 109 = 24 GFLOP/S e 1.3 Utilizzo della memoria Un fattore fondamentale per determinare le prestazioni di un sistema ` la e larghezza di banda della memoria (memory bandwith), ovvero la velocit` con cui a ` possibile trasferire i dati tra la memoria e il processore. Si misura in numero e di bytes che si possono trasferire in un secondo. I multipli che solitamente si usano sono i Mb/sec, Gb/sec e Tb/sec. Vediamo ora perch` questa misura ` cos` importante: si consideri l’operazione e e ı A = B * C Per eseguire questa operazione, il processore dovr` compiere i seguenti 4 passi: a - 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 ` in grado di e fornire i dati al processore in modo sufficientemente rapido, il processore non ` e in gradi di eseguire le operazioni alla sua massima velocit`, perch deve rimanere a in attesa che gli arrivino i dati su cui operare dalla memoria. Consideriamo di nuovo l’Esempio 2: abbiamo visto che per ogni operazione floating point occor- rono fino a 3 accessi alla memoria. Siccome la dimensione del dato ` di 32 bit, e ovvero 4 bytes, ad ogni operazione corrisponde un trasferimento di 12 bytes. Nell’eseguire 24 GFLOP/S occorrer` quindi una larghezza di banda di a 24 · 12 = 288Gb/s.
  3. 3. 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) della memoria. Anche se l’esempio ha preso come modello un processore PentiumD, il problema ` molto comune su tutti processori moderni ed ` il principale collo e e di bottiglia nelle prestazioni. Volendo fare un paragone, ` come avere una e macchina sportiva con un motore potentissimo, ma non riuscire a fornire al motore abbastanza benzina per farlo funzionare. 1.4 Cache del processore Il modo con cui i processori attuali riducono le limitazioni della larghezza di banda della memoria ` la cache. e La cache della CPU ` una area di memoria ad alta velocit` di accesso e di e a dimensioni piuttosto piccole, rispetto alla memoria primaria, situato tra questa e il microprocessore . In genere si tratta di memoria di tipo statico, senza la necessit` di refresh, assai pi` costosa di quella dinamica, ma con tempi di a u accesso molto ridotti, dell’ ordine del singolo ciclo clock. Pu` essere sia esterna o che interna al chip del processore e pu` essere situata a diversi livelli logico/fisici, o a seconda delle funzioni svolte. La cache contiene i dati utilizzati con maggior frequenza dal microprocessore nelle operazioni correnti e questo contribuisce all’ incremento delle prestazioni, poich tali dati non devono essere richiamati ogni volta dalla pi` lenta memoria RAM. Le cache possono contenere istruzioni u (codici), dati o entrambi i tipi di informazione. Se la CPU deve cercare un dato o una istruzione, la ricerca per primo nella cache; se ` presente, questa situazione e di chiama cache hit e il dato ` immediatamente disponibile senza dover attendere e la memoria. Se non ` presente (cache miss), la preleva dalla RAM e ne fa anche e una copia nella cache; in questo caso occorre attendere la memoria; generalmente l’ordine di grandezza dell’attesa ` la decina di cicli clock per ogni cache miss.. e Anche se sembrerebbe che quanto pi` grande ` la cache , tanto pi` grande u e u ` il numero di informazioni che possono essere gestiti con efficienza, questa e affermazione ` vera relativamente, in quanto, aumentando la cache oltre certi e limiti, il rapporto prezzo/prestazioni diventa non conveniente. La cache pu` o essere incorporata nel microprocessore allo scopo di accrescerne pi la velocit di accesso che la dimensione: la cache nel chip, data la riduzione delle distanze di interconnessione e la maggior possibilit di integrazione delle funzioni di scambio, comunica pi` rapidamente con il microprocessore, solitamente lavorando alla u stessa 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 processore e di dimensioni che variano tra 16Kb e 32Kb, e L2 , spesso esterna di dimensioni tra 512Kb e 2Mb, mentre sono state realizzate anche strutture con pi livelli (ad esempio L3 di AMD). Un sistema senza cache ha prestazioni nettamente ridotte rispetto ad uno con cache; la differenza rilevabile facilmente disabilitando le cache interne dal setup del BIOS. Vediamo ora come ` possibile sfruttare al meglio la cache per massimizzare e
  4. 4. 4 CHAPTER 1. PERFORMANCE DEI SISTEMI DI CALCOLO le prestazioni dei programmi che scriveremo. La cache sfrutta due principi fondamentali che si chiamano localit` di spazio e localit` di tempo. a a Localit` di spazio: Se un dato viene utilizzato in un dato istante, ` probabile a e che dati posizionati in celle di memoria adiacenti vengano anch’essi richiesti entro 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 ad esempio si legge il valore x[i], nelle iterazioni successive occorrer` accede a x[i+1], a x[i+2]... che sono tutti dati vicini al valore appena letto. Questo tipo di accesso alla memoria ` tipico di molti algoritmi e di conseguenza ` sfruttato dalla cache. e e Vediamo in che modo: la cache ` un’area di memoria organizzata a blocchi e (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 a quello letto potranno servire entro breve. Se nelle istruzioni successive questo succede, il processore trova gi` i dati disponibili nella cache (cache hit) e non a deve 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 dati in modo che possano essere acceduti il pi` possibile in sequenza. Al contrario, u un accesso ”random” o a salti alla memoria genera inevitabilmente una serie di cache miss. Inoltre accessi random alla memoria ”inquinano” la cache (cache pollution), ovvero la riempiono di dati che non possono essere sfruttati, rubando spazio ai dati in cache che possono essere letti nell’ordine corretto. Localit` di tempo: se un dato viene referenziato in un dato istante, ` prob- a e abile che lo stesso dato venga nuovamente richiesto entro breve. Nell’esempio di prima, le variabili sum, i e n vengono utilizzate tante volte quante sono le iterazioni del ciclo. Seguendo il flusso del programma, esister` un punto in cui a queste variabili sono utilizzate per la prima volta. In quel momento entrano nella cache e siccome sono utilizzate continuamente nel ciclo, non lasciano mai la cache e rimangono disponibili per un loro rapido accesso. Esempio 3. Si consideri la seguente funzione che esegue il prodotto matrice per 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++) {
  5. 5. 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 ` scritta per operare con una e dimensione fissa N delle matrici. Questo approccio ` caldamente sconsigliato, in e quanto si vincola il funzionamento ad un caso particolare. Ricordando che nel linguaggio C le matrici sono memorizzate per righe, si pu` riscrivere la funzione o in modo pi` generico: u 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 ` rappresentata in un vettore in cui sono mem- e 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 comunque verrebbe generata intrinsecamente dal compilatore, qualora venisse utilizzato il costrutto matrice come nella prima versione della funzione. In questo caso possiamo vedere come tutti gli accessi alle 3 aree di memoria (la matrice A e i vettori b e c) avvengono tutti in modo sequenziale, ed in questo modo ` sfruttata e la localit` di spazio della cache. a Esercizio 3. Si consideri la seguente funzione che esegue il prodotto riga per 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];
  6. 6. 6 CHAPTER 1. PERFORMANCE DEI SISTEMI DI CALCOLO } } Possiamo notare come gli accessi agli elementi delle matrici C ed A siano in sequenza, mentre gli elementi della matrice B sono letti a salti di n elementi. Considerando che il tipo di dato ` un double, che occupa 8 bytes, le letture e avvengono a salti di 8n bytes. Ogni accesso alla matrice B genera quindi un cache miss. Questo ` il modo meno ottimizzato per calcolare un prodotto tra e matrici. Si scriva un programma in C che esegua il prodotto tra due matrici di 1024 elementi con la funzione di cui sopra e si cronometri il tempo di esecuzione. Si scrivano poi due diverse implementazioni della funzione che adottino le seguenti strategie: - trasposizione della matrice B, in modo da poter accedere sequenzialmente ai suoi elementi. - Calcolo del prodotto a blocchi. Si renda parametrizzabile la dimensione del blocco in modo da lavorare con blocchi che possano essere contenuti nella cache L1 del processore. Quest’ultima strategia ` la base della libreria di e algebra lineare ATLAS. Si cronometrino queste due nuove versioni e si confrontino i risultati con la prima 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 con il 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: ¯ T (1) ¯ S(p) = T (p) Si definisce misura di scalabilit` o speedup relativo di un algoritmo a parallelo eseguito su p processori rispetto all’esecuzione dello stesso algoritmo su un processore: T (1) S(p) = T (p) In un sistema ideale, in cui il carico di lavoro potrebbe essere perfettamente partizionato su p processori, lo speedup relativo dovrebbe essre uguale a p. Tale speedup si definisce lineare. Nella pratica non si ha quasi mai uno speedup lineare, a causa di vari fattori:
  7. 7. 1.5. METRICA DELLE PRESTAZIONI PARALLELE 7 • sbilanciamento del carico di lavoro tra i processori: alcuni processori hanno un carico maggiore di lavoro rispetto ad altri; i processori con meno carico di 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 eseguite da 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 poich` e in un sistema a memoria distribuita, ad un aumento dei processori corrisponde anche un aumento della memoria totale disponibile. Una maggiore quantit` di a memoria pu` consentire il salvataggio di risultati intermedi ed evitare di doverli o ricalcolare in parti successive nell’algoritmo. In questo modo si pu` ridurre o il numero di calcoli eseguiti rispetto ad un’esecuzione con meno processori ed ottenere cos` speedup superlineari. ı Flatt e Kennedy hanno proposto un modello che descrive qualitativamente lo speedup di un sistema. Sia Tser il tempo di esecuzione della parte seriale di un algoritmo, Tpar il tempo di esecuzione della parte di codice che pu` essere o parallelizzata e T0 (p) il tempo per le comunicazioni e le sincronizzazioni tra i p processori. Valgono le relazioni: T (1) = Tser + Tpar T T (p) = Tser + par + T0 (p) p Supponendo che T0 (p) sia proporzionale a p, ovvero T0 (p) = Kp, possiamo scrivere lo speedup come: Tser + Tpar p(Tser + Tpar ) S(p) = Tpar = Tser + + T0 (p) pTser + Tpar + Kp2 p La figura 1.5 descrive l’andamento dello speedup al variare del numero di pro- cessori. Si vede come la curva ` inizialmente prossima ad uno speedup libeare, e 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: lim S(p) = 0. p→∞ Questo significa che ` c’` una soglia (che dipende all’algoritmo e dall’ ar- e e chitettura del sistema) oltre la quale ` controproducente aumentare il numero e dei processori. ` come se per compiere un determinato lavoro si assumessere e pi` persone: se le persone sono troppe, si intralciano a vicenda ed occorre pi` u u tempo a portare a termine il compito.
  8. 8. 8 CHAPTER 1. PERFORMANCE DEI SISTEMI DI CALCOLO 16 14 12 S(p) 10 8 6 4 2 0 0 10 20 30 40 50 60 70 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 S(p) E(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 ` una funzione decrescente. Maggior- e mente l’efficienza si allontana da 1, peggio stiamo sfruttando le risorse di calcolo disponibili nel sistema parallelo. La figura 1.6 descrive l’andamento dell’efficienza al variare del numero di processori. Come si pu` determinare il numero ottimale di processori per un certo algo- o ritmo? Osserviamo che: - E(p) ha il massimo per p = 1; - S(p) ha il massimo in corrispondenza del punto di saturazione, in cui per` o l’efficienza ` piuttosto bassa. e Kuck ha introdotto la funzione F (p) = E(p)S(p)
  9. 9. 1.7. CONSIDERAZIONI SULLA PARALLELIZZAZIONE DEGLI ALGORITMI9 1 0.9 0.8 0.7 0.6 E(p) 0.5 0.4 0.3 0.2 0.1 0 10 20 30 40 50 60 70 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 di processori pF con cui eseguire un algoritmo si pu` determinare mediante o pF = argmaxF (p) ovvero pF ` il numero di processori in corrispondenza del massimo della funzione e di Kuck. Si veda la figura 1.6 per vedere la funzione di Kuck dell’esempio precedente. 1.7 Considerazioni sulla parallelizzazione degli algoritmi Di seguito sono riportati i punti chiave che occorre sempre valutare nello sviluppo di algoritmi paralleli: • Identificazione di blocchi computazionali indipendenti. • Ridurre o eliminare le dipendenze. Sono i due principi base che consentono di parallelizzare un algoritmo. Se non esistono calcoli indipen- denti e le operazioni dipendono strettamente dai risultati delle precedenti, non ` possibile distribuire i calcoli tra processori diversi. Ad esempio, si e consideri un programma che vuole calcolare l’n-esimo numero della serie
  10. 10. 10 CHAPTER 1. PERFORMANCE DEI SISTEMI DI CALCOLO 7 6 5 F(p) 4 3 2 1 0 0 10 20 30 40 50 60 70 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 = 1 f1 = 1 fn = 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 operazioni che 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 pi` possibile uniforme. Se non fosse cos` i processori u ı, che hanno meno lavoro (o che terminano prima degli altri), devono ri- manere inoperosi in attesa degli ultimi, sprecando risorse di calcolo. Tale condizione si chiama idle. Un esempio di carico di lavoro sbilanciato e’ il seguente. Supponiamo di voler calcolare il prodotto matrice-vettore con una matrice triangolare superiore. Se suddividiamo la matrice a blocchi di righe, il primo pro- cessore avra’ quasi tutti gli elementi non nulli, mentre l’ultimo processore avra’quasi tutti gli elementi nulli, eccetto un piccolo triangolo. L’ultimo processore dovra’ quindi eseguire pochissime operazioni per moltiplicare il proprio blocco, terminera’ molto prima del primo processore e rimarra’ cosi’ in idle, finche’ il primo processore non avra’ finito.
  11. 11. Chapter 2 Algoritmi paralleli 2.1 Ordinamento di un vettore Uno degli approcci che pi` comunemente si utilizzano per parallelizzare un al- u goritmo ` detto divide and conquer, conosciuto anche con l’espressione latina e divide et impera. Questo approccio si basa sul seguente regionamento: si ha un problema di dimensione n e di suppone di saper risolvere un problema dello stesso tipo di dimesione n/2. Quindi si divide il problema originale in due sot- toproblemi indipendenti di met` dimensione e si risolvono le due met`; se si ` a a e in grado di sfruttare queste soluzioni parziali in modo effeiciente per ottenere la soluzione del problema iniziale, allora ` possibile trovare un’efficiente paraleliz- e zazione dell’algoritmo. Infatti ` sufficiente continuare ricorsivamente la divisione e in sottoproblemi pi` piccoli fino a quando non si ` generato un sottoproblema u e indipendente per ogni processore. Come primo esempio di questo principio, consideriamo il problema dell’ordinamento di un vettore di interi. Sia I = {ai : i = 0 · · · n − 1, ai ∈ N } un vettore di n interi. Suddividiamo I in due sottoinsiemi I0 = {ai : i = 0 · · · n − 1} e 2 I1 = {ai : i = n · · · n − 1}. 2 Ora consideriamo i due problemi indipendenti di ordinare i vettori I0 e I1 . Se si hanno a disposizione due processori, ` possibile ordinare I0 sul primo e processore e I1 sul secondo processore in modo indipendente. L’ordinamento pu` essere effettuato conunqualunque algoritmo di ordinamento sequenziale, o quale ad esempio il quick sort. Una volta terminato l’ordinamento dei due vettori, ` possibile ottenere l’insieme I ordinato fondendo i due insiemi I0 e I1 e 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
  12. 12. 12 CHAPTER 2. ALGORITMI PARALLELI Figure 2.1: • P0 fonde gli insiemi I0 e I1 precedentemente ordinati per ottenere la soluzione. Come si pu` generalizzare questa procedura al caso in cui si hanno pi` di o u 2 processori? Ogni volta che un processore deve ordinare la sua sottosequenza, possiamo riutilizzare questa procedura e dividere cos` ulteriormente per due ı il sottoinsieme. Questa procedura pu` essere ripatuta ricorsivamente fino a o quando l’insieme di partenza non ` stato suddiviso in tante parti quanti sono i e processori disponibili. Si veda la figura 2.1 per un esempio nel caso di 8 processori. In questo caso pi` generale, la procedura per ordinare un vettore con p u 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 via via dimezzata, finch` tutti i p procesori non hanno un insieme Ip di di- e mensione n/p; • Ogni processore ordina il suo sottoinsieme;
  13. 13. 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 secondo processore e lo fonde al suo sottoinsieme. • Il punto precedente viene ripetuto finch` non si ha una sola coppia di e processori e la fusione produce come risultato l’insieme I ordinato. Osserviamo che questo algoritmo ` molto efficiente e, se non ci fossero i e costi di comunicazione, il suo speedup sarebbe superlineare. Infatti, nel caso di 1 processore, il costo di ordinamento con un algoritmo di tipo ”veloce” come il quick sort ` di nlog(n). In presenza di p processori, ogni processore deve e ordinare un vettore di dimensione n/p e di conseguenza il suo costo di calcolo ` n log n = n (log(n) − log(p)) che ` minore del costo sequenziale nlog(n) diviso e p p p e per il numero di processori. Inoltre, per il fatto che i processori lavorano su vettori pi` piccoli, ` pi` probabile che i dati possano essere mantenuti nella u e u cache del processore e questo incrementa ulteriormente l’efficienza del metodo. 2.2 Decomposizione LU Un sistema di n equazioni con n incognite si pu` scrivere come: o a00 x0 + a01 x1 + · · · + a0n−1 xn−1 = b0 a10 x0 + a11 x1 + · · · + a1n−1 xn−1 = b1 . . . an−1,0 x0 + an−1,1 x1 + · · · + an−1,n−1 xn−1 = bn−1 In forma matriciale diventa Ax = b. La decomposizione LU ` una metodo che, a partire da una matrice A ∈ Rnxn e genera due matrici L, U ∈ Rnxn , con L triangolare inferiore e U triangolare superiore, tali che A = LU . In questo modo ` possibile risolvere il sistema lineare e Ax = b risolvendo due sistemi lineari triangolari: Ly = b e successivamente U x = y. L’algoritmo della decomposizione LU si basa sull’eliminazione di Gauss: il principio ` quello di ”eliminare” progressivamente delle variabili dal sistema, e finch` non rimane una sola variabile. Presa ad esempio la i-esima equazione nel e sistema, possiamo eliminare un’incognita da un’altra equazione j, moltiplicando l’equazione i per il termine costante −aji /aii e aggiungendo l’equazione risul- tante all’equazione j. L’obiettivo ` quello di eliminare tutti i termini che stanno e nel 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. La versione sequenziale dell’algoritmo si pu` scrivere in questo modo: o // Esegue la decomposizione LU della matrice "a" scrivendo in // "a" il triangolo superiore e in "l" il triangolo inferiore
  14. 14. 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; } } 3 Il costo computazionale dell’algoritmo ` proporzionale a O( n ). L’accesso e 6 ai dati nel ciclo pi` interno avviene per righe, di conseguenza si ha un buono u sfruttamento della cache. Vediamo ora come ` possibile parallelizzare questo algoritmo. Supponiamo e di avere un processore per ogni riga della matrice, ovvero p = n. Ogni processore avra’ memorizzato gli elementi della riga a lui associata, ovvero il processore P0 ha 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 elementi della propria riga. Tutti gli altri processori Pi : i > 0 potranno cosi’azzerare il primo elemento della propria riga. Nella seconda iterazione, il processore P1 invia a tutti gli altri, tramite una comunicazione broadcast, gli elementi della propria riga, eccetto il primo elemento perche’ e’ gia’ stato azzerato. Tutti gli altri 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 le sue rimanenti n − j variabili ai processori Pi : i > j, i quali azzereranno la loro j-esima variabile. Si vede chiaramente come molti processori sono idle, ovvero non compiono nessuna operazione. Ad esempio, il processore P0, dopo aver comunicato la propria riga agli altri processori, non ha piu’ compiti da eseguire per tutto il resto dell’algoritmo. Il carico di lavoro e’ quindi mal bilanciato e lo speedup di questo algoritmo non potra’ essere buono. Ci si poteva aspettare un risultato simile, in quanto siamo partiti dall’assunto di avere tanti processori quante sono le righe della matrice. Il numero di processori sarebbe troppo elevato per ottenere buoni speedup, come descritto nel paragrafo 1.5. Analizziamo ora un caso piu’ realistico, ovvero il caso in cui di hanno molti meno processori rispetto alle righe della matrice. Questo e’ un caso piu’ reale,
  15. 15. 2.2. DECOMPOSIZIONE LU 15 Figure 2.2: in quanto generalmente le matrici da fattorizzare hanno dimensioni nell’ordine delle migliaia, mentre il numero di processori disponibili e’ nell’ordine della decina. Se distribuissimo la matrice A a blocchi per righe non risolveremmo il problema del cattivo bilanciamento del carico. Il processore P0 avrebbe pochissime opeazioni da svolgere, dato che dovrebbe azzerare gli elementi del triangolo inferiore. L’ultimo processore dovrebbe al contrario operare su quasi tutti i suoi elementi. Per bilanciare il carico di lavoro, e’ allora utile utilizzare una distribuzione ciclica 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 pu` o schematizzare nel seguente modo: 1 Il processore P0 comunica agli altri processori le righe della matrice da fattorizzare, secondo la distribuzione ciclica. La riga i sar` inviata al a processore i mod p. 2 Per ogni riga i: il processore i mod p, ovvero il processore che detiene la riga 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 dal processore q-esimo. Per ciascuna riga in Rq , annulla l’i-esimo elemento, sfruttando la riga ricevuta al passo 2.
  16. 16. 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 ora conterr` gli elementi annullati) e della matrice L. a E’ possibile osservare che in questo algoritmo ` necessario operare un grande e numero di comunicazioni: occorrono n comunicazioni per il passo 1, altre n per il passo 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 poich´ solitamente il numero di comuni- e cazione proporzionale al numero di processori, ma non aumenta all’aumentare della dimensione del problema (aumenta la quantit di dati trasmessi, ma non il numero di comunicazioni). Inoltre, la distribuzione ciclica produce un bi- lanciamento perfetto del carico di lavoro solo asintoticamente, quindi pi` sono u piccole le dimensioni della matrice A, pi` si sar` lontani da un bilanciamento u a ottimale. Considerati questi punti, non sar` possibile osservare speedup ottimali a per 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 di problemi reali composti da blocchi interconnessi tra loro, come la progettazione di circuiti integrati VLSI o l’ottimizzazione strutturale. Vedremo come ` pos- e sibile sfruttare le particolarit` di questo problema per sviluppare un metodo di a risoluzione parallelo. In generale, un sistema a freccia ha la seguente forma:       A1 B1 x1 b1  A2 B2   x2   b2  . . .        .. .   .  =    .  det(Ai ) = 0   . .     .   .    Ap Bp   xp   bp  C1 C2 ··· Cp As xs bs Introduciamo ora un po’ di notazione che ci permette di scrivere il sistema in una forma pi` compatta. Si definiscono: u AI = diag(A1 , · · · , Ap )   B1 B= .   .  . Bp
  17. 17. 2.3. RISOLUZIONE DI UN SISTEMA LINEARE A FRECCIA 17 C = (C1 , . . . , Cp )   x1 xI =  .   .  . xp   b1 bI =  .   .  . bp Il sistema pu` essere riscritto come: o AI xI + Bxs = bI (2.1) CI xI + As xs = bs (2.2) Si noti come la matrice AI ` fortemente sparsa, in quanto ha tutti gli ele- e menti nulli eccetto i blocchi sulla diagonale. Quando si sviluppa un algoritmo parallelo, il primo passo ` quello di identificare se ci sono operazioni indipen- e denti che possono essere distribuite tra i vari processori. Apparentemente, in questo sistema non ci sono operazioni di questo tipo e quindi, a prima vista, si potrebbe essere portati a risolvere il sistema utilizzando la decomposizione LU parallela oppure un qualunque metodo iterativo. Tuttavia ` possibile riscrivere e il problema tramite una trasformazione in modo da evidenziare sottoproblemi indipendenti. Moltiplicando 2.1 per CA−1 e sottraendo il risultato da 2.2 si ha: I (As − CA−1 B)xs = bs − CA−1 bI I I ovvero ˆ ˆ Axs = b dove    A−1 1 B1 p ˆ ..  .  A = As − (C1 , . . . , Cp )    .  = As − Ci A−1 Bi  . . i A−1 Bp i=1 p e    A−1 1 b1 p ˆ ..  .  b = bs − (C1 , . . . , Cp )    .  = bs − Ci A−1 bi  . . i A−1 bp i=1 p In questo modo ` stato possibile determinare le componenti incognite xs . e Ora, osservando che l’equazione 2.1 ` un sistema diagonale a blocchi, ` possibile e e sfruttare xs per calcolare la soluzione risolvendo p sistemi indipendenti. E’ possibile schematizzare l’algoritmo di risoluzione coi seguenti passi:
  18. 18. 18 CHAPTER 2. ALGORITMI PARALLELI 1 Invertire le matrici A1 , . . . , Ap ; ˆ 2 Calcolare A = As − p ˆ Ci A−1 Bi e b = bs − p Ci A−1 bi ; i=1 i i=1 i 3 Risolvere il sistema Axs = ˆ per determinare xs ; ˆ b 4 Calcolare le restanti componenti del vettore soluzione x risolvendo p sis- temi lineari Ai xi = bi − Bi xs , i = 1, . . . , p. Osserviamo che il punto 1 ` intrinsecamente parallelo, in quanto devono es- e sere risolti i p problemi indipendenti di inversione delle matrici Ai . Il punto 2 pu`o essere eseguito in parallelo solo parzialmente; infatti ogni processore pu` eseguire o i prodotti Ci A−1 Bi e Ci A−1 bi , ma dovr` essere eseguita una comunicazione per i i a calcolare la somma, dato che occorre raccogliere in un unico processore tutti i prodotti parziali per sommarli. Il punto 3 non ` intrinsecamente parallelo. Pu` e o essere risolto in sequenziale dal processore master o in alternativa pu` essereo risolto con un algoritmo LU parallelo visto nel paragrafo precedente. Una volta determinato xs , ` possibile eseguire il punto 4 in parallelo; infatti i p sistemi e da risolvere sono indipendenti e la loro risoluzione pu` essere intrinsecamente o distribuita 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 Bi e i A−1 bi ; notiamo che calcolare Ai bi corrisponde a determinare la soulzione del i −1 sistema lineare Ai x = bi . Analogamente, se si rappresenta la matrice Bi come l’insieme delle sue colonne [Bi1 Bi2 · · · Bin ], dove Bin e’ l’n-esima colonna della matrice Bi , e’ possibile calcolare le colonne del prodotto A−1 Bi risolvendo gli n i sistemi lineari Ai xj = Bij per j = 1, · · · , n. Si noti che e’ sufficiente calcolare la decomposizione LU di Ai inizialmente e riutilizzarla per risolvere ogni sistema lineare. Si pu` schematizzare l’algoritmo risolutivo parallelo nel seguente modo: o Distribuzione dei dati: Per semplicit` si suppone che il numero di pro- a cessori coincida con p. Ogni processore i memorizza i seguenti elementi del problema: Ai , Bi , Ci , bi . Memorizza inoltre le matrici Li e Ui ottenute dalla decomposizione 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 = Ci A−1 bi sfruttando la i decomposizione LU calcolata al punto 1; 3 Tramite una comunicazione di tipo Reduce con somma sui termini di , il p ˆ processore master ottiene la somma i=1 Ci Ai bi e con questa calcola b; −1 4 Ogni processore i-esimo calcola il termine Di = Ci A−1 Bi mediante la i risoluzione degli n sistemi lineari Ai xj = Bij ;
  19. 19. 2.4. AUTOVALORI DI UNA MATRICE TRIDIAGONALE 19 5 Tramite una comunicazione di tipo Reduce con somma sui termini Di , il p processore master ottiene la somma i=1 Ci A−1 Bi e con questa calcola i ˆ 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 una Broadcast; 8 Ogni processore i-esimo risolve il sistema lineare Ai xi = bi − Bi xs ; 9 Le componenti xi vengono riunite in un unico vettore tramite una co- municazione di tipo Gather. Unite assieme a xs , formano la soluzione al problema iniziale. L’algoritmo presentato in questo paragrafo e’ un ottimo esempio di come sia possibile esprimere il parallelismo intrinseco ad un problema, anche quando questo non e’ apparenemente evidente. 2.4 Autovalori di una matrice tridiagonale In questo paragrafo ` presentato un algoritmo parallelo per il calcolo degli auto- e valore di una matrice tridiagonale. L’algoritmo sar` sviluppato secondo il prin- a cipio del divide and conquer. Al termine del capitolo si vedr` come lo sviluppo a di un nuovo algoritmo parallelo pu` portare a nuove idee che permettono di o migliorare anche la versione sequenziale del metodo. Verranno ora dati alcuni cennti preliminari al calcolo degli autovalori. Sia A una matrice quadrata in Rnxn e x ∈ Rn . Tutti i valori λ che soddisfano l’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’equazione precedente si chiama autovettore della matrice A. Riscrivendo l’equazione 2.3 come (A − λI)x = 0 ` possibile vedere che gli autovalori di A possono essere calcolati richiedendo e che det(A − λI) = 0. Questo d` origine ad un polinomio di grado n nella variabile λ le cui radici sono a ` gli autovalori cercati. E chiaro che un simile metodo di calcolo ` di scarsa utilit` e a pratica, dal momento che non esistono formule esplicite per il calcolo delle radici di un polinomio di grado superiore al 4 e che il problema ` estremamente mal e condizionato. La tecnica che viene pi` frequentemente adottata ` il metodo del u e
  20. 20. 20 CHAPTER 2. ALGORITMI PARALLELI QR iterativo. Esso ` un metodo estremamente generico che permette il calcolo e degli autovalori di generiche matrici senza una struttura particolare. In numerose applicazioni pratiche, tuttavia, ` spesso necesario il calcolo degli e autovalori di una matrice trigiagonale simmetrica, ovvero una matrice con la seguente forma:   d1 β1  β1 d2 β2     β2 d3 β3  T =   ..    .    β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 riguarda i motori di ricerca, come Google, in cui per determinare il PageRank (ovvero una stima di quanto una pagina ` linkata nel web) vengono ricercati gli autovalori e pi` grandi della matrice rappresentante il grafo delle connessioni nel web. u L’obbiettivo di questo paragrafo ` di analizzare un metodo parallelo per il e calcolo degli autovalori di T , utilizzando il principio del divide and conquer: si divider` la matrice in due sottomatrici di met` dimensione e si calcoleranno gli a a autovalori di queste due matrici (eventualmente riapplicando ricorsivamente il metodo) in modo completamente indipendente l’uno dall’altro. In questo modo viene espresso un parallelismo. Infine verranno ”unite” le informazioni parziali ottenute da ciascun sottoproblema, per determinare gli autovettori della matrice originale T , mediante una tecnica che illustreremo in seguito. Sia k = n , β = βk , e1 , ek vettori della base canonica di Rk , ovvero: 2     1 0  0   0  e1 =  . , ek =  .      . .   . .   0 1 Se si definiscono     d1 β1 dk − β βk  ..   ..   β d2 . βk dk+1 . T1 =  1    , T2 =    .. ..   .. ..   . . βk−2   . . βn−1  βk−2 dk−1 − β βn−1 dn La matrice T pu` essere riscritta come: o
  21. 21. 2.4. AUTOVALORI DI UNA MATRICE TRIDIAGONALE 21   0 0 0 0 0 0   0 0 0 0 0 0   T1 0  0 0 β β 0 0  T = + = 0 T2   0 0 β β 0 0    0 0 0 0 0 0  0 0 0 0 0 0 T1 0 ek eT k e k eT 1 = +β = 0 T2 e1 eT k e1 e T 1 T1 0 ek = +β eT eT k 1 0 T2 e1 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 ` spezzato il problema e in due sottoproblemi indipendenti; tali problemi possono essere risolti con un metodo diretto, ciascuno su un processore diverso, oppure si pu` avviare una o procedura ricorsiva di suddivisioni in sottoproblemi sempre pi` piccoli, come nel u caso dell’algoritmo di ordinamento visto precedentemente. Una volta terminato questo passo, ` necessario sviluppare un metodo che permetta di calcolare gli e autovalori della matrice originale a partire dall’informazione parziale che si ha dai sottoproblemi. Avendo calcolato gli autovalori e gli autovettori, ` possibile e rappresentare T1 e T2 come T 1 = Q1 D1 QT 1 T 2 = Q2 D2 QT 2 dove D1 e D2 sono matrici diagonali i cui elementi sono gli autovalori di T1 e T2 mentre Q1 e Q2 sono due matrici unitarie le cui righe sono i rispettivi autovettori di T1 e T2 . Allora la matrice T pu` essere scritta come: o Q1 D1 QT 0 ek T = 1 +β eT eT 0 Q2 D2 QT 2 e1 k 1 Ricordando le propriet` delle matrici unitarie, osserviamo che Q1 QT = I. a 1 Inoltre anche la matrice Q1 0 0 Q2 ` unitaria, essendo Q1 e Q2 unitarie. La precedente equazione diventa allora: e Q1 0 D1 0 QT 0 ek Q1 0 QT 0 T = +β 1 eT eT 1 0 Q2 0 D2 0 QT2 e1 k 1 0 Q2 0 QT2 Q1 0 D1 0 q1 QT 0 = +β qT qT 1 0 Q2 0 D2 q2 1 2 0 QT2
  22. 22. 22 CHAPTER 2. ALGORITMI PARALLELI dove q 1 = QT ek e q 2 = QT e1 . 1 2 Di conseguenza, gli autovalori di T saranno uguali gli autovalori di D1 0 q1 +β q T qT 1 2 0 D2 q2 Quindi il problema del calcolo degli autovalori di T si riduce al calcolo degli autovalori di D + βzz T con D matrice diagonale i cui elementi sono δ1 , · · · , δn , β scalare e z vettore di norma unitaria e con elementi non nulli. Applicando la definizione di autovalore, ` possibile scrivere l’equazione nell’incognita e λ (gli autovalori) (D + βzz T )x = λx (2.4) (D − λI)x = −βz(z T x) (2.5) x = −β(z T x)(D − λI)−1 z (2.6) T z x = −β(z T x)z T (D − λI)−1 z (2.7) 1 = −βz T (D − λI)−1 z (2.8) Quindi ogni autovalore di T ` autovalore di D + βzz T e si pu` determinare e o risolvendo l’equazione 1 + βz T (D − λI)−1 z = 0 Questa equazione pu` essere riscritta in forma scalare: o n 2 zi f (λ) = 1 + β i=1 δi − λ ¯ ¯ Gli zeri dell’equazione 2.4, che chiameremo δ1 , . . . , δn , godono di un’ottima propriet` di localizzazione, ovvero ` noto a priori che essi sono reali, distinti e a e ¯ contenuti ciascuno nell’intervallo da (δi , δi+1 ). Pi` precisamente, δi ∈ (δi , δi+1 ) u per i < n, e δ ¯n > δn . Inoltre, all’interno di tali intervalli, la funzione f (λ) ` monotona crescente. Grazie a queste propriet`, risulta possibile utilizzare e a molto efficacemente un metodo iterativo per il calcolo degli zeri dell’equazione non lineare. La parallelizzazione di questo algoritmo ` intrinseca nel metodo: lo schema e ` di tipo divide and conquer, quindi la parallelizzazione avviene mediante suddi- e 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, ` anch’essa intrinsecamente e ¯ ¯ parallela, 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:
  23. 23. 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 della propria matrice Ti , generando Di e Qi ; 3 Ogni processore calcola il vettore q i ; 4 Di e q i vengono raccolti e comunicati a tutti i processori mediante una comunicazione di tipo All Gather; ¯ ¯ 5 Ogni processore determina un sottoinsieme delle soluzioni δ1 , . . . , δn .
  24. 24. 24 CHAPTER 2. ALGORITMI PARALLELI
  25. 25. Chapter 3 Librerie di calcolo Le librerie di calcolo scientifico sono una vasta collezione di funzioni altamente ottimizzate che svolgono i principali compiti computazionali basilari del calcolo scientifico. Tra questi compiti si possono citare la risoluzione di sistemi lineari con 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 stessa sintassi della versione standard; - per ogni routine, esistono versioni specifiche in base al tipo di dato da trattare: singola/doppia precisione, numeri reali/complessi; - per ogni routine, esistono versioni specifiche in base alla struttura della matrice: piena, a banda, sparsa. Il sito www.netlib.org ` il repository ufficiale per le principali librerie di e calcolo. Da questo sito ` possibile scaricare le librerie, la documentazione e e dei programmi di esempio. 3.1 Descrizione delle librerie sequenziali • BLAS (Basic Linear Algebra Subprograms) E’ la libreria di livello pi` basso e contiene le routines basilari per il trat- u tamento di vettori e matrici. E’ suddivisa in 3 livelli: - Level 1 BLAS: contiene le operazioni tra due vettori (somma, prodotto scalare, ...); 25
  26. 26. 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 riga per colonna, somma di matrici) Tutte le librerie di calcolo si basano sulla BLAS per il trattamento delle matrici e dei vettori, quindi ` importante che questa libreria sia partico- e larmente ottimizzata. Per questa ragione, i produttori di microprocessori forniscono spesso una versione della BLAS ottimizzata per le loro CPU. Un’altra possibilit` ` fornita dalla ATLAS (Automatically Tuning Linear ae Algebra Software); ` un’implementazione della BLAS che in fase di in- e stallazione esegue dei test sul processore per determinare i suoi parametri ottimali di funzionamento per quello specifico sistema. • LINPACK (LINear PACKage) E’ la libreria che contiene le funzioni di algebra lineare relative alla risoluzione di sistemi lineari e alle fattorizzazioni di matrici. Attualmente ` stata sor- e passata dalla LAPACK, che unisce le funzioni della LINPACK e della EISPACK. • EISPACK E’ la libreria che contiene le funzioni per il calcolo di autovalori e autovet- tori di matrici. Attualmente ` inclusa nella LAPACK e • LAPACK (Linear Algebra PACKage) E’ la principale libreria di calcolo scientifico per architetture sequenziali. E’ nata come unione delle librerie LINPACK e EISPACK e attualmente comprende 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 ` in e grado di trattare matrici dense e a banda, ma non matrici sparse. Il sito http://www.cs.colorado.edu/ jessup/lapack/ contiene un motore di ricerca per selezionare la pi` appropriata routine LAPACK in base al compito da u svolgere, 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 ` scritta in Fortran 77. Per facilitare il suo e utilizzo all’interno di codici C o C++ sono state scritte queste librerie che consistono in funzioni di interfaccia che permettono di chiamare con la convenzione C e C++ le funzioni Fortran della LAPACK. • ESSL E’ la versione di LAPACK fornita da IBM, ottimizzata per i propri calco- latori.
  27. 27. 3.2. DESCRIZIONE DELLE LIBRERIE PARALLELE 27 • MKL (Math Kernel Library) E’ la versione di LAPACK fornita dalla Intel, ottimizzata per i propri processori. 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 sono indipendenti dalla piattaforma e dal protocollo di comunicazione (MPI, PVM, MPL, NX, ...). Su questa libreria si appoggiano tutte le librerie di calcolo parallelo, in modo da essere indipendenti dal sistema. • PBLAS (Parallel Basic Linear Algebra Subprograms) E limplementazione parallela della libreria BLAS; anchessa ` divisa nei e 3 livelli in base al tipo di operazione da svolgere. PBLAS basa le sue comunicazioni sulla BLACS. • ScaLAPACK E 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 sulla PBLAS per le operazioni interne parallele. • ParPACK Siccome la ScaLAPACK, come la LAPACK, non ` in grado di trattare e matrici sparse, ` stata creata la libreria ParPACK per il calcolo di auto- e valori e autovettori di matrici sparse. Questa libreria contiene sia funzioni sequenziali che parallele. • CAPSS e MFACT Sono due librerie, sia sequenziali che parallele, per la risoluzione di sistemi lineari sparsi con metodi diretti.
  28. 28. 28 CHAPTER 3. LIBRERIE DI CALCOLO
  29. 29. Chapter 4 Programmazione dei sistemi a memoria condivisa In questo capitolo saranno analizzati i sistemi a memoria condivisa: dopo una breve descrizione introduttiva di tali sistemi, verranno analizzate le analogie e le differenze nello sviluppo di codice parallelo rispetto ai sistemi a memoria distribuita. Infine verr` introdotta la programmazione multithread quale tecnica a per programmare tali sistemi; in particolare saranno introdotti i thread POSIX, detti pthread. I sistemi a memoria condivisa sono macchine multiprocessore dotate di un’unica memoria RAM accessibile a tutti i processori. Fino all’anno 2005, questi sistemi erano la minoranza rispetto a tutte le macchine multiprocessore. Di solito si trattava di calcolatori con due processori accoppiati sullo stesso nodo, fino ad arrivare a nodi con 8 o 16 processori su alcune architetture IBM. Recentemente, per`, i sistemi a memoria condivisa stanno diventando la maggior parte delle o macchine multiprocessore. Infatti in tutti i processori di moderna generazione, anche quelli utilizzati nei comuni PC domestici, sono presenti almeno 2 o 4 cores, ovvero processori fisicamente integrati nello stesso chip, ma in grado di operare indipendentemente l’uno dall’altro. La tendenza sar` quella negli anni a futuri di incrementare la potenza dei microprocessori aumentando il numero di cores anzich` rendendo pi` veloci i singoli cores. Lo stesso vale anche per le e u nuove 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 i suoi server ma usati anche nella Playstation 3, gli FPGA... Attualmente ` raro e che su un calcolatore sia necessario eseguire un unico compito specializzato con tutta la potenza di elaborazione disponibile. Pi` frequentemente le applicazioni u svolgono tante operazioni contemporaneamente. Per esempio, quando si naviga su una pagina web, il browser sta contemporaneamente eseguendo i comandi dell’utente, caricando una pagina, visualizzando il testo, muovendo tutti gli el- ementi grafici animati e riproducendo i contenuti multimediali. Essendo tanti compiti eseguiti contemporaneamente, ha senso avere a disposizione tanti cores, 29
  30. 30. 30CHAPTER 4. PROGRAMMAZIONE DEI SISTEMI A MEMORIA CONDIVISA ognuno dei quali potr` svolgere uno o pi di quei compiti. Vedremo tra poco che a questi ”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 calcolo disponibile, ma non aumenta la quantit` di memoria disponibile. Quindi a le risorse di calcolo sono molto abbondanti, mentre le risorse di memoria tendono ad essere il collo di bottiglia. 2 Le comunicazioni tra i processori sono pi` efficienti. Infatti non ` neces- u e sario che vi sia un invio di dati tra due processori (come avviene invece nel paradigma Message Passing) poich` ogni processore pu` accedere a tutta e o la memoria e leggere cos` direttamente i dati di cui ha bisogno. Le comu- ı nicazioni servono quindi unicamente per sincronizzare i processori, ovvero un processore segnala agli altri quando ha finito di elaborare i suoi dati, in modo che gli altri sappiano quando sono disponibili e possono essere letti dalla memoria comune. Queste due osservazioni cambiano il modo con cui si programmano i sistemi a memoria condivisa rispetto a quelli a memoria distribuita, ma non cambiano i principi di parallelismo, ovvero le idee e le tecniche che si seguono per paral- lelizzare un algoritmo. 4.1 Programmi e processi Un programma ` costituito dal codice oggetto generato dalla compilazione dei e sorgenti. Esso ` un’entit` statica, che rimane immutata durante l’esecuzione. e e Un processo, invece, ` un’entit` dinamica, che dipende dai dati che vengono e e elaborati, e dalle operazioni eseguite su di essi. Il processo ` quindi caratter- e izzato, oltre che dal codice eseguibile, dall’insieme di tutte le informazioni che ne definiscono lo stato, come il contenuto della memoria indirizzata, i thread, i descrittori dei file e delle periferiche in uso. Sostanzialmente, quindi, il processo ` la rappresentazione che il sistema operativo ha di un programma in esecuzione. e 4.2 Processi e thread Il concetto di processo ` associato a quello di thread (abbreviazione di thread e of execution, filo dell’esecuzione), con cui si intende l’unit` granulare in cui un a processo pu` essere suddiviso, e che pu` essere eseguito in parallelo ad altri o o thread. In altre parole, un thread ` una parte del processo che viene eseguita e in maniera concorrente ed indipendente internamente al processo stesso. Il termine inglese rende bene l’idea, in quanto si rif` visivamente al concetto di a fune composta da vari fili attorcigliati: se la fune ` il processo in esecuzione, e allora i singoli fili che la compongono sono i thread. Un processo ha sempre almeno un thread (s` stesso), ma in alcuni casi un e processo pu` avere pi` thread che vengono eseguiti in parallelo. o u
  31. 31. 4.3. SUPPORTO DEL SISTEMA OPERATIVO 31 Una prima differenza fra thread e processi consiste nel modo con cui essi condividono le risorse. Mentre i processi sono di solito fra loro indipendenti, utilizzando diverse aree di memoria ed interagendo soltanto mediante appositi meccanismi di comunicazione messi a disposizione dal sistema, al contrario i thread tipicamente condividono le medesime informazioni di stato, la memoria ed altre risorse di sistema. L’altra differenza sostanziale ` insita nel meccanismo di attivazione: la e creazione di un nuovo processo ` sempre onerosa per il sistema, in quanto devono e essere allocate le risorse necessarie alla sua esecuzione (allocazione di memoria, riferimenti alle periferiche, e cos` via, operazioni tipicamente onerose); il thread ı invece ` parte del processo, e quindi una sua nuova attivazione viene effettuata e in tempi ridottissimi a costi minimi. Le definizioni sono le seguenti: Il processo ` l’oggetto del sistema operativo a cui sono assegnate tutte le e risorse di sistema per l’esecuzione di un programma, tranne la CPU. Il thread ` l’oggetto del sistema operativo o dell’applicazione a cui ` assegnata la CPU e e per l’esecuzione. In un sistema che non supporta i thread, se si vuole eseguire contemporaneamente pi` volte lo stesso programma, ` necessario creare pi` u e u processi basati sullo stesso programma. Tale tecnica funziona, ma ` dispendiosa e di risorse, sia perch ogni processo deve allocare le proprie risorse, sia perch per comunicare tra i vari processi ` necessario eseguire delle relativamente lente e chiamate di sistema, sia perch la commutazione di contesto tra thread dello stesso processo ` pi` veloce che tra thread di processi distinti. e u Avendo pi` thread nello stesso processo, si pu` ottenere lo stesso risultato u o allocando una sola volta le risorse necessarie, e scambiando i dati tra i thread tramite la memoria del processo, che ` accessibile a tutti i suoi thread. e Un esempio di applicazione che pu` far uso di pi` thread ` un browser Web, o u e che usa un thread distinto per scaricare ogni immagine in una pagina Web che contiene pi` immagini. u Un altro esempio ` costituito dai processi server, spesso chiamati servizi o e daemon, che possono rispondere contemporaneamente alle richieste provenienti da pi` utenti. u In un sistema multiprocessore, si possono avere miglioramenti prestazion- ali, grazie al parallelismo fisico dei thread. Tuttavia, l’applicazione deve essere progettata in modo da suddividere tra i thread il carico di elaborazione. Tale progettazione ` difficile e frequentemente soggetta a errori, e va progettata con e molta cura. 4.3 Supporto del sistema operativo I sistemi operativi si classificano nel seguente modo in base al supporto che offrono a processi e thread: • Monotasking: non sono supportati n processi n thread; si pu` lanciare o un solo programma per volta.
  32. 32. 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 o finch il sistema operativo sospende il processo per passare la CPU a un altro processo. • Multithreaded: sono supportati sia i processi, che i thread. 4.4 Stati di un thread In un sistema operativo multitasking, ci sono pi` thread contemporaneamente u in esecuzione. Di questi, al massimo un numero pari al numero di processori pu` avere effettivamente il controllo di un processore. Quindi i diversi processi o possono utilizzare il processore per un numero limitato di tempo, per questo motivo i processi vengono interrotti, messi in pausa e richiamati secondo degli algoritmi di schedulazione. Gli stati in cui un thread si pu` trovare sono: o • esecuzione (running): il thread ha il controllo di un processore; • pronto (ready): il thread ` pronto ad essere eseguito, ed ` in attesa che e e lo scheduler lo metta in esecuzione ; • bloccato (suspended): il thread ha eseguito una chiamata di sistema, ed ` fermo in attesa del risultato ; e Con commutazione di contesto (Context switch) si indica il meccanismo per cui un thread in esecuzione viene fermato (perch ha eseguito una chiamata di sistema o perch lo scheduler ha deciso di eseguire un altro thread), e un altro pronto viene messo in esecuzione. 4.5 Sincronizzazione di thread Ci sono due ragioni principali per sincronizzare i thread di un processo. La prima ` insita nella struttura degli algoritmi utilizzati e nella logica di pro- e grammazione, ed ` la stessa ragione per cui si sincronizzano i processori in un e ambiente a memoria distribuita: sono quelle comunicazioni che permettono ad un processore di informare gli altri che il proprio compito ` terminato e che i dati e prodotti sono disponibili. Questo tipo di comunicazione avviene per mezzo di eventi, ovvero un meccanismo di comunicazione con cui un processore segnala agli altri che una certa condizione si ` verificata. Ad esempio, nell’algoritmo di e decomposizione LU parallela, occorre che i processori inizino contemporanea- mente ad annullare una nuova riga quando la riga precedente ` stata annullata e da tutti i processori. In ambiente a memoria distribuita, questa sincronizzazione
  33. 33. 4.5. SINCRONIZZAZIONE DI THREAD 33 avveniva intrinsecamente utilizzando le comunicazioni di MPI nel momento della Broadcast. Questa comunicazione non aveva solo il compito di trasferire dati tra i processori, ma anche di sincronizzarli; la Broadcast ` infatti una comuni- e cazione bloccante e quindi termina quando tutti i processori hanno ricevuto i dati, ovvero quando tutti i processori sono arrivati a quel punto di esecuzione. Gli eventi funzionano come una qualunque comunicazione sincrona senza che per` vi sia un trasferimento di dati. o La seconda ragione ` pi` sottile e non si presenta nel caso di sistemi a memo- e u ria distribuita; la sua funzione ` quella di stabilire un ordine ben determinato e a come due o pi` processori possono utilizzare la stessa area di memoria o, pi` u u in generale, la stessa risorsa. Per queste situazioni vengono utilizzati i mutex e i semafori. Nei sistemi a memoria condivisa si hanno pi` processori, ma una u sola memoria, un solo hard disk, un solo monitor, tastiera, mouse, scheda audio (queste sono le risorse); pi` processori potrebbero avere la necessit` di accedere u a allo stesso file o a una stessa risorsa. I mutex e i semafori serializzano l’accesso alle risorse, ovvero premettono ad un solo processore alla volta di accedere alla risorse richesta. Per illustrare la necessit` dei mutex consideriamo il seguente esempio: sup- a poniamo di avere una variabile v che ogni thread incrementa di un’unit` ogni a volta che ha eseguito un certo calcolo. Potr` capitare la situazione in cui due a thread devono incrementare contemporaneamente quella variabile. L’incremento di una variabile non ` un’ operazione atomica, ovvero richiede pi` istruzioni per e u essere eseguita. clk v thread 1 thread 2 1 100 Leggi v ... 2 100 Somma 1 Leggi v 3 100 Scrivi v Somma 1 4 101 ... Scrivi v 5 101 ... ... Dalla tabella, si pu` vedere come il thread 2 ha iniziato ad eseguire l’incremento o di 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 valore incrementato e cos` il thread 2 si trover` ad incrementare un valore vecchio di ı a v. Il risultato ` che nonostante i due incrementi, la variabile v ` passata da dal e e valore 100 al valore 101 anzich` 102. Occorre quindi un oggetto che permetta e di 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 possono catturarlo finch` il primo thread non l’ha rilasciato. La scrittura corretta del e codice per l’incremento parallelo della variabile v ` scritto in questo modo: e void *MyThread(void *arg) { ... mutex_lock(IncMutex);
  34. 34. 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 gli altri thread rimangono bloccati alla loro istruzione mutex lock finch` il primo e thread non l’ha rilasciato con l’istruzione mutex unlock. Ovviamente queste comunicazioni, come i messaggi di MPI, riducono le prestazioni del programma poich` pongono i processori in uno stato di attesa senza che vengano eseguite e istruzioni utili. Con l’uso dei mutex, la seguenza di istruzioni diventerebbe clk v thread 1 thread 2 1 100 mutex lock ... 2 100 Leggi v mutex lock 3 100 Somma 1 in attesa 4 101 Scrivi v in attesa 5 101 mutex unlock in attesa 6 101 ... Leggi v 7 101 ... Somma 1 8 101 ... Scrivi v 9 102 ... mutex unlock Da questa tabella si vede come l’accesso alla risorsa sia stata serializzata anche se questo ha introdotto degli stati di attesa nel secondo thread. I Semafori sono un caso pi` generico dei mutex. Il mutex consente ad u un solo thread di accedere ad una risorsa, mentre il semaforo consente ad un massimo numero predeterminato di thread di accedere alla risorsa protetta. Sono tipicamente utilizzati in ambienti multiutente o client/server quando c’` e un server che deve rispondere contemporaneamente ad un grande numero di richieste (ad esempio un server web) avendo un numero limitato di risorse. Ad esempio se un server web riesce a gestire un massimo di 100 connessioni con- temporaneamente, ma ci sono 200 utenti collegati, verr` utilizzato un semaforo a per serializzare le richieste di 200 utenti (un thread per utente) sulle 100 risorse disponibili. Generalmente siamo portati ad abbinare il concetto di thread al concetto di processore o core. Ovvero, se abbiamo a disposizione n processori o n cores pu` o sembrare logico creare n threads, in modo che ciascuno venga eseguito su un processore. Tuttavia, ` utile osservare che tale assunzione non ` sempre vera: e e infatti spesso ` conveniente attivare pi` thread dei processori disponibili. Se si e u hanno pi` thread per processore e uno o pi` di questi thread ` bloccato perch` u u e e fermo in una sincronizzazione o perch` il carico di lavoro non ` ben bilanciato, il e e processore pu` dedicare le sue risorse agli altri threads e cos` non rimane in stato o ı di attesa. Questa ` un’utile tecnica per bilanciare automaticamente il carico di e lavoro.

×