SlideShare a Scribd company logo
1 of 34
Download to read offline
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               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.
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              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++)
    {
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             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:
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                 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)
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                  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.
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                                   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;
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                                    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,
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                                          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
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                                          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 ;
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                                               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
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                                         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:
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   CHAPTER 2. ALGORITMI PARALLELI
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                                       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.
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   CHAPTER 3. LIBRERIE DI CALCOLO
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
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
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.
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
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);
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.

More Related Content

What's hot

Time, Schedules, and Resources in Artificial Intelligence.pptx
Time, Schedules, and Resources in Artificial Intelligence.pptxTime, Schedules, and Resources in Artificial Intelligence.pptx
Time, Schedules, and Resources in Artificial Intelligence.pptx
kitsenthilkumarcse
 
Parallel architecture
Parallel architectureParallel architecture
Parallel architecture
Mr SMAK
 
MODES OF TRANSFER.pptx
MODES OF TRANSFER.pptxMODES OF TRANSFER.pptx
MODES OF TRANSFER.pptx
22X047SHRISANJAYM
 

What's hot (20)

Chapter02 basic computer organization
Chapter02   basic computer organizationChapter02   basic computer organization
Chapter02 basic computer organization
 
Time, Schedules, and Resources in Artificial Intelligence.pptx
Time, Schedules, and Resources in Artificial Intelligence.pptxTime, Schedules, and Resources in Artificial Intelligence.pptx
Time, Schedules, and Resources in Artificial Intelligence.pptx
 
Chap 2 computer forensics investigation
Chap 2  computer forensics investigationChap 2  computer forensics investigation
Chap 2 computer forensics investigation
 
Parallel architecture
Parallel architectureParallel architecture
Parallel architecture
 
Multiprocessor architecture
Multiprocessor architectureMultiprocessor architecture
Multiprocessor architecture
 
Operating system 40 lru algorithm
Operating system 40 lru algorithmOperating system 40 lru algorithm
Operating system 40 lru algorithm
 
AI_ 3 & 4 Knowledge Representation issues
AI_ 3 & 4 Knowledge Representation issuesAI_ 3 & 4 Knowledge Representation issues
AI_ 3 & 4 Knowledge Representation issues
 
Os
OsOs
Os
 
Présentation rattrapage module Forensic
Présentation rattrapage module ForensicPrésentation rattrapage module Forensic
Présentation rattrapage module Forensic
 
Freenet
FreenetFreenet
Freenet
 
Operating system 03 handling of interrupts
Operating system 03 handling of interruptsOperating system 03 handling of interrupts
Operating system 03 handling of interrupts
 
Unix Shell and System Boot Process
Unix Shell and System Boot ProcessUnix Shell and System Boot Process
Unix Shell and System Boot Process
 
Scheduling (sjf, fcfs and round robin
Scheduling (sjf, fcfs and round robinScheduling (sjf, fcfs and round robin
Scheduling (sjf, fcfs and round robin
 
Buses in a computer
Buses in a computerBuses in a computer
Buses in a computer
 
MODES OF TRANSFER.pptx
MODES OF TRANSFER.pptxMODES OF TRANSFER.pptx
MODES OF TRANSFER.pptx
 
First Order Logic resolution
First Order Logic resolutionFirst Order Logic resolution
First Order Logic resolution
 
Computer forensics
Computer  forensicsComputer  forensics
Computer forensics
 
Air Cargo transport
 Air Cargo transport Air Cargo transport
Air Cargo transport
 
8 memory management strategies
8 memory management strategies8 memory management strategies
8 memory management strategies
 
Mikrokontroler pertemuan 7
Mikrokontroler pertemuan 7Mikrokontroler pertemuan 7
Mikrokontroler pertemuan 7
 

Similar to Performance dei sistemi di calcolo

Architettura elaboratore
Architettura elaboratoreArchitettura elaboratore
Architettura elaboratore
serex86
 
Architettura dei Calcolatori 11 Cache
Architettura dei Calcolatori 11 CacheArchitettura dei Calcolatori 11 Cache
Architettura dei Calcolatori 11 Cache
Majong DevJfu
 
A query-to-hardware compiler for FPGA architectures
A query-to-hardware compiler for FPGA architecturesA query-to-hardware compiler for FPGA architectures
A query-to-hardware compiler for FPGA architectures
Enrico Cambiaso
 
Modulo1 lezione1
Modulo1 lezione1Modulo1 lezione1
Modulo1 lezione1
scipag
 
Attacchi alle applicazioni basati su buffer overflow
Attacchi alle applicazioni basati su buffer overflowAttacchi alle applicazioni basati su buffer overflow
Attacchi alle applicazioni basati su buffer overflow
Giacomo Antonino Fazio
 
Struttura dell'elaboratore (sample)
Struttura dell'elaboratore (sample)Struttura dell'elaboratore (sample)
Struttura dell'elaboratore (sample)
Parco nord.
 

Similar to Performance dei sistemi di calcolo (20)

Assemblare un pc
Assemblare un pcAssemblare un pc
Assemblare un pc
 
Cpu
CpuCpu
Cpu
 
Cpu Abacus
Cpu AbacusCpu Abacus
Cpu Abacus
 
Cpu abacus
Cpu abacusCpu abacus
Cpu abacus
 
Architettura elaboratore
Architettura elaboratoreArchitettura elaboratore
Architettura elaboratore
 
Elaborazione automatica dei dati: calcolatore e matlab
Elaborazione automatica dei dati: calcolatore e matlabElaborazione automatica dei dati: calcolatore e matlab
Elaborazione automatica dei dati: calcolatore e matlab
 
Scheda Madre
Scheda MadreScheda Madre
Scheda Madre
 
Architettura dei Calcolatori 11 Cache
Architettura dei Calcolatori 11 CacheArchitettura dei Calcolatori 11 Cache
Architettura dei Calcolatori 11 Cache
 
A query-to-hardware compiler for FPGA architectures
A query-to-hardware compiler for FPGA architecturesA query-to-hardware compiler for FPGA architectures
A query-to-hardware compiler for FPGA architectures
 
Jvm performance Tuning
Jvm performance TuningJvm performance Tuning
Jvm performance Tuning
 
Personal computer
Personal computerPersonal computer
Personal computer
 
Modulo1 lezione1
Modulo1 lezione1Modulo1 lezione1
Modulo1 lezione1
 
Modulo 1 - Lezione 2
Modulo 1 - Lezione 2Modulo 1 - Lezione 2
Modulo 1 - Lezione 2
 
Hardware e software
Hardware e softwareHardware e software
Hardware e software
 
Architettura dei calcolatori
Architettura dei calcolatoriArchitettura dei calcolatori
Architettura dei calcolatori
 
Back to Basics, webinar 6: Messa in esercizio
Back to Basics, webinar 6: Messa in esercizioBack to Basics, webinar 6: Messa in esercizio
Back to Basics, webinar 6: Messa in esercizio
 
Calcolo Parallelo
Calcolo ParalleloCalcolo Parallelo
Calcolo Parallelo
 
Attacchi alle applicazioni basati su buffer overflow
Attacchi alle applicazioni basati su buffer overflowAttacchi alle applicazioni basati su buffer overflow
Attacchi alle applicazioni basati su buffer overflow
 
Struttura dell'elaboratore (sample)
Struttura dell'elaboratore (sample)Struttura dell'elaboratore (sample)
Struttura dell'elaboratore (sample)
 
Aumentare la memoria dinamica (heap) assegnata alla jvm [sc]
Aumentare la memoria dinamica (heap) assegnata alla jvm [sc]Aumentare la memoria dinamica (heap) assegnata alla jvm [sc]
Aumentare la memoria dinamica (heap) assegnata alla jvm [sc]
 

More from Majong DevJfu

9 - Architetture Software - SOA Cloud
9 - Architetture Software - SOA Cloud9 - Architetture Software - SOA Cloud
9 - Architetture Software - SOA Cloud
Majong DevJfu
 
8 - Architetture Software - Architecture centric processes
8 - Architetture Software - Architecture centric processes8 - Architetture Software - Architecture centric processes
8 - Architetture Software - Architecture centric processes
Majong DevJfu
 
7 - Architetture Software - Software product line
7 - Architetture Software - Software product line7 - Architetture Software - Software product line
7 - Architetture Software - Software product line
Majong DevJfu
 
6 - Architetture Software - Model transformation
6 - Architetture Software - Model transformation6 - Architetture Software - Model transformation
6 - Architetture Software - Model transformation
Majong DevJfu
 
5 - Architetture Software - Metamodelling and the Model Driven Architecture
5 - Architetture Software - Metamodelling and the Model Driven Architecture5 - Architetture Software - Metamodelling and the Model Driven Architecture
5 - Architetture Software - Metamodelling and the Model Driven Architecture
Majong DevJfu
 
4 - Architetture Software - Architecture Portfolio
4 - Architetture Software - Architecture Portfolio4 - Architetture Software - Architecture Portfolio
4 - Architetture Software - Architecture Portfolio
Majong DevJfu
 
3 - Architetture Software - Architectural styles
3 - Architetture Software - Architectural styles3 - Architetture Software - Architectural styles
3 - Architetture Software - Architectural styles
Majong DevJfu
 
2 - Architetture Software - Software architecture
2 - Architetture Software - Software architecture2 - Architetture Software - Software architecture
2 - Architetture Software - Software architecture
Majong DevJfu
 
1 - Architetture Software - Software as a product
1 - Architetture Software - Software as a product1 - Architetture Software - Software as a product
1 - Architetture Software - Software as a product
Majong DevJfu
 
10 - Architetture Software - More architectural styles
10 - Architetture Software - More architectural styles10 - Architetture Software - More architectural styles
10 - Architetture Software - More architectural styles
Majong DevJfu
 

More from Majong DevJfu (20)

9 - Architetture Software - SOA Cloud
9 - Architetture Software - SOA Cloud9 - Architetture Software - SOA Cloud
9 - Architetture Software - SOA Cloud
 
8 - Architetture Software - Architecture centric processes
8 - Architetture Software - Architecture centric processes8 - Architetture Software - Architecture centric processes
8 - Architetture Software - Architecture centric processes
 
7 - Architetture Software - Software product line
7 - Architetture Software - Software product line7 - Architetture Software - Software product line
7 - Architetture Software - Software product line
 
6 - Architetture Software - Model transformation
6 - Architetture Software - Model transformation6 - Architetture Software - Model transformation
6 - Architetture Software - Model transformation
 
5 - Architetture Software - Metamodelling and the Model Driven Architecture
5 - Architetture Software - Metamodelling and the Model Driven Architecture5 - Architetture Software - Metamodelling and the Model Driven Architecture
5 - Architetture Software - Metamodelling and the Model Driven Architecture
 
4 - Architetture Software - Architecture Portfolio
4 - Architetture Software - Architecture Portfolio4 - Architetture Software - Architecture Portfolio
4 - Architetture Software - Architecture Portfolio
 
3 - Architetture Software - Architectural styles
3 - Architetture Software - Architectural styles3 - Architetture Software - Architectural styles
3 - Architetture Software - Architectural styles
 
2 - Architetture Software - Software architecture
2 - Architetture Software - Software architecture2 - Architetture Software - Software architecture
2 - Architetture Software - Software architecture
 
1 - Architetture Software - Software as a product
1 - Architetture Software - Software as a product1 - Architetture Software - Software as a product
1 - Architetture Software - Software as a product
 
10 - Architetture Software - More architectural styles
10 - Architetture Software - More architectural styles10 - Architetture Software - More architectural styles
10 - Architetture Software - More architectural styles
 
Uml3
Uml3Uml3
Uml3
 
Uml2
Uml2Uml2
Uml2
 
6
66
6
 
5
55
5
 
4 (uml basic)
4 (uml basic)4 (uml basic)
4 (uml basic)
 
3
33
3
 
2
22
2
 
1
11
1
 
Tmd template-sand
Tmd template-sandTmd template-sand
Tmd template-sand
 
26 standards
26 standards26 standards
26 standards
 

Performance dei sistemi di calcolo

  • 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 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. 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 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. 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 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. 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 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. 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 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. 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 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. 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 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. 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 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. 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 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. 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 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. 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 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. 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 CHAPTER 2. ALGORITMI PARALLELI
  • 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 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. 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 CHAPTER 3. LIBRERIE DI CALCOLO
  • 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. 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. 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. 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. 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. 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.