Documente Academic
Documente Profesional
Documente Cultură
Savantul american de origine ungară John von Neumann a propus în 1945 cel mai
cunoscut model al unui calculator. Această descriere conceptuală, realizată când tehnologia
era la nivel de pionierat, este şi astăzi considerată una dintre cele mai importante etape în
dezvoltarea ştiinţei calculatoarelor.
Modelul descris în [13] (cunoscut mai târziu sub
numele de modelul von Neumann sau arhitectura
Princeton) stabileşte următoarele caracteristici ale unui
sistem de calcul:
• Calculatorul este compus din: memorie, unitatea
aritmetico-logică, unitatea de control şi
periferice;
• În timpul execuţiei, programul este stocat în
memorie;
• La un anumit moment, memoria poate fi
accesată fie de unitatea de control, fie de
unitatea aritmetico-logică;
• Instrucţiunile programului se execută secvenţial.
7
singură magistrală de date între memorie şi procesor. O alternativă la modelul von Neumann
este modelul Harvard, caracterizat prin tipuri diferite de memorii: unul pentru date (de tip
citire/scriere) şi altul pentru program (de tip read-only). Astfel, se pot executa simultan
schimbări în starea programului şi citiri/scrieri de date. Modelul Harvard modificat permite
ca memoria dedicată programului să fie tot de tip read/write. Acest model este utilizat de
diverşi producători de procesoare (familia ARM9 este un exemplu) sau este folosit pentru
dispozitive specializate (microcontrolere).
Dezvoltarea tehnologică ulterioară şi necesităţile tot mai mari de putere de calcul au
evidenţiat limitele modelului von Neumann, astfel că au apărut diverse metode de creştere a
vitezei de execuţie a unui program. De exemplu, memoriile intermediare de tip cache oferă
acces mai rapid la datele necesare execuţiei unei instrucţiuni. Posibilitatea accesului rapid la
date şi dezvoltările hardware au dus la realizarea în 2005 a primelor procesoare dual-cores,
care sunt de fapt două procesoare cu aspect fizic unitar, care posedă un sistem de memorii
cache. Au urmat apoi procesoare din ce în ce mai complexe, cu arhitecturi care utilizează
ierarhii din ce în ce mai dezvoltate de memorii cache, cum este cel din figura 1.
8
simultan din punctul de vedere al utilizatorului, fără ca acesta să observe sincope. Pe de altă
parte, arhitectura mono-procesor impune execuţia unei singure aplicaţii la un anumit moment
de timp. Metoda prin care se realizează aceste procese se numeşte multitasking şi înseamnă
partajarea timpului de lucru al procesorului în intervale disjuncte şi alocarea acestor intervale
diverselor procese aflate în execuţie. Cum aceste intervale sunt foarte scurte, utilizatorul nu
percepe schimbările realizate de sistemul de operare, ci are impresia că toate procesele se
desfăşoară simultan.
Calculul concurent este o caracteristică software, opusă calculului serial, care permite
execuţia simultană a mai multor aplicaţii. De exemplu, pe un procesor mono-nucleu, calculul
concurent se poate realiza prin multitasking (programele în execuţie primesc intervale
disjuncte de timp pentru accesarea procesorului; la fiecare moment de timp un singur
program se execută; la expirarea timpului alocat, starea programului în execuţie se salvează şi
se dă controlul procesorului altui program; aceste schimbări se realizează des, astfel încât
utilizatorul are senzaţia că toate programele se execută în paralel). Pe un calculator multi-
procesor, care deci este alcătuit din mai multe procesoare care partajează memorie comună,
calculul concurent se realizează efectiv sub forma calculului paralel (care este deci o
proprietate hardware). Pe de altă parte, în cazul reţelelor de calculatoare, alcătuite din sisteme
independente, cu memorii independente, calculul concurent capătă forma calculului distribuit
(care este, de asemenea, caracteristică hardware).
Fib(0) = Fib(1) = 1
Fib(n) = Fib(n − 1) + Fib(n − 2), ∀n ≥ 2
Pentru a calcula în mod iterativ valoarea lui Fib(n) se poate folosi următoarea metodă:
Function f(n)
if (n < 2) return 1
x=1
y=1
z=2
k=2
while (k < n)
x=y
9
y=z
z=x+y
k=k+1
return z
Function g(n)
if (n < 2) return 1
return g(n - 1)+g(n - 2)
Această metodă necesită cunoştinţe mai avansate de programare şi este mai înceată
(sau produce depăşiri de memorie) la execuţia în limbajul C, chiar pentru valori comune ale
lui n. Avantajul ei este însă că se auto-apelează (pentru n > 2) cu factor de multiplicare 2.
Aceasta înseamnă, de exemplu, că pentru aflarea lui g(8) se lansează o copie a sa care
calculează g(7) şi încă una pentru găsirea lui g(6). Fiecare dintre acestea lansează alte două
execuţii, etc.
În tabelul 1 este prezentat modul în care lucrează funcţia g dacă este apelată pentru
valoarea n = 4. Coloanele tabelului arată modul de încărcare pe stivă a copiilor lui g, până
când se calculează valoarea g(4). Rândurile tabelului arată succesiunea temporală a execuţiei
procedurii g pentru valoarea 4. Apelul 1 este apelul procedurii, care lansează imediat pe stivă
apelul 2, adică o copie a codului pentru valoarea n = 3 şi apoi intră în aşteptare (idle) până
când primeşte valoarea g(3), adică 3. Apelul 2 lansează o nouă copie a aplicaţiei, pentru a
calcula g(2), etc. Starea de execuţie este indicată prin culoarea de fundal gri; starea idle a unui
anumit proces este indicată prin diagonalele trasate. De exemplu, apelul 3 ocupă timp de 4
unităţi de timp o unitate de procesare (începând cu momentul 3), din care jumătate stă în
aşteptare (este idle). Se observă că pe stivă, pentru n = 4, vor exista cel mult 4 copii în
execuţie şi că unele copii stau în aşteptare destul de mult timp, astfel explicându-se faptul că
execuţia lui g este mai înceată decât cea a lui f, pentru aceeaşi valoare a lui n.
10
Apel 1 Apel 2 Apel 3 Apel 4
(main)
realizat de: - Apel 1 Apel 2 Apel 3
12 g(1)
13 return 1
14 g(1) = 1
15 return 3
16 g(3) = 3
17 g(2)
18 g(1)
19 return 1
20 g(1) = 1
21 g(0)
22 return 1
23 g(0) = 1
24 return 2
25 g(2) = 2
26 return 5
Tabelul 1. Evoluţia temporală a apelului recursiv g(4)
Trecerea spre execuţia pe mai multe fire urmează modelul din tabelul 1 şi presupune
că sunt disponibile unităţi de procesare astfel încât să poată prelua execuţia la nevoie şi astfel
firele de execuţie să nu aştepte mai mult decât este necesar. Pentru lansarea succesivă a
apelurilor se introduc două funcţii specifice calculului concurent, care permit multiplicarea
execuţiei şi respectiv închiderea tuturor firelor lansate şi continuarea execuţiei în clasicul mod
serial. Aceste două noi comenzi sunt:
• spawn – procedură prin care un proces (părinte) lansează un proces (fiu)
• sync – procedură prin care se aşteptă încheierea tuturor proceselor lansate în apelul
curent prin spawn şi continuarea numai cu procesul părinte.
Folosind aceste două noi comenzi, procedura recursivă concurentă pentru calculul
valorii generice din şirul lui Fibonacci devine:
Function h(n)
if (n < 2) return 1
x = spawn h(n – 1)
//de aici aplicaţia are 1 fir-părinte + 1 fir-fiu
y = spawn h(n – 2)
//de aici aplicaţia are 1 fir-părinte + 2 fire-fiu
sync
//aici se ajunge după ce fii se încheie
return x + y
11
5 lucrează şi 4 sunt în starea idle. Se observă de asemenea că apelul h(4) necesită 12 unităţi
de timp pentru execuţie, pe când apelul g(4) are nevoie de 26 unităţi de timp şi de cel mult 4
execuţii simultane.
La fel ca în tabelul 1, zonele gri reprezintă intervalele de timp în care procesoarele
sunt alocate, iar zonele care au diagonale sunt intervalele de timp în care procesoarele stau în
aşteptare (idle). Observăm însă că apelul 1 avansează şi la momentul 2, când realizează
lansarea apelului 3, spre deosebire de situaţia prezentată în tabelul 1, când este deja în
aşteptare. Şi în acest caz se observă un dezechilibru al încărcării procesoarelor, de exemplu
ultimele 5 procese lucrează 2 intervale de timp, pe când primul procesor este ocupat toate
cele 12 intervale de timp.
12
de supercalculatoare, care oferă în mod curent acces simultan la mii de procesoare. Calculul
concurent nu este decât modelul actual a ceea ce natura face de milioane de ani: se dezvoltă
prin evoluţii simultane ale ecosistemelor, populaţiilor, evenimentelor geologice, etc.
Tehnologia ne permite ca mişcarea planetelor, evenimentele climatice, migraţiile păsărilor,
servirea clienţilor la un magazin, conducerea corporaţiilor trans-naţionale, tranzacţiile
bancare sau vizionarea filmelor să fie acum studiate sau realizate prin sisteme de calcul
paralel sau distribuit. Globalizarea lumii în care trăim este posibilă şi datorită dezvoltării
calculului concurent.
Odată cu dezvoltarea tehnologică şi apariţia sistemelor de calcul care pot oferi acces
simultan la mai multe unităţi de procesare, cercetătorii au realizat diverse clasificări ale
acestor sisteme. Clasificarea următoare se referă la funcţionalitatea sistemelor de calcul, deci
la modul în care aplicaţia realizează prelucrarea datelor.
13
Aplicaţiile concurente se pot evalua folosind clasicele măsuri software, destinate
tuturor categoriilor de aplicaţii. Aceste măsuri se pot clasifica astfel:
• măsuri cantitative: număr de linii de cod, mărimea (în biţi) a fişierului executabil,
viteza de execuţie, numărul de ore de programare necesare proiectării, dezvoltării
şi testării, etc.;
• măsuri calitative: corectitudine (rezolvă problema propusă), completitudine
(rezolvă toate cazurile posibile), scalabilitate (capabilă de a procesa chiar şi
volume de date mai mari decât cele aşteptate, sau se poate uşor dezvolta astfel
încât să le accepte), mentenabilitate (uşurinţa în exploatare şi actualizare), etc.
Măsurile software specifice programelor care se avansează simultan pe mai multe fire
de execuţie se referă atât la caracteristicile de consum total ale resurselor implicate (timp de
procesare, număr de procesoare), cât şi la economiile realizate prin utilizarea acestora (cât
timp se câştigă dacă utilizez în paralel mai multe procesoare, decât dacă folosesc un singur
procesor). În continuare sunt prezentate câteva măsuri specifice calculului concurent.
Eficienţa (parallel efficiency) este o măsură derivată din acceleraţie, fiind raportul
dintre aceasta şi numărul de procesoare utilizate:
S (n)
E ( n) =
n
În baza observaţiei anterioare, deducem că eficienţa unei aplicaţii concurente are
întotdeauna o valoare subunitară, iar eficienţa unei aplicaţii concurente ideale este 1.
14
Din punct de vedere constructiv, calculatoarelor paralele li se pot aplica diverse
măsuri: numărul de nuclei, memoria RAM, consumul de energie, viteza de procesare. O
măsură a vitezei de procesare ieşită din uz este numărul de instrucţiuni pe secundă. Cum
aceste instrucţiuni pot necesită intervale de tip de lungimi diferite pentru execuţie, acum se
utilizează numărul de operaţii în virgulă mobilă pe secundă (floating point operations per
second – FLOP/s).
15
Legea lui Gustafson (John Gustafson, 1988)
Pe o configuraţie hardware dată (numărul de procesoare n fixat) se pot scrie aplicaţii
paralele care au acceleraţie liniară.
Cu notaţiile anterioare avem că:
a
+1
lim S (n) = lim b =n
b →∞ b →∞ a 1
+
b n
Deci dacă partea paralelizabilă este suficient de mare, atunci acceleraţia este aproximativ
egală cu numărul de procesoare. Legea lui Gustafson specifică o regulă simplă: dacă vrem să
ne apropiem de eficienţa ideală a unei aplicaţii paralele, partea paralelizată trebuie să fie cât
mai consistentă.
Gordon Earl Moore (1929 - ) Gene Myron Amdahl (1922 - ) John Gustafson (1955 - )
16
Capitolul 2
Paralelism implicit
Timpul
0 1 2 3 4 5 6 7 8 9
IF D DF E WB
IF D DF E WB
IF D DF E WB
IF D DF E WB
IF D DF E WB
IF D DF E WB
Figura 2. Dezvoltarea temporală pentru o procesare ideală de tip pipeline
19
toate fazele, câte una din primele 5 instrucţiuni). Această situaţie nu apare mereu în realitate,
deoarece pot apărea întârzieri. De exemplu:
• dacă instrucţiunea curentă are nevoie de încărcarea unor date (faza DF) care la
momentul respectiv sunt actualizate printr-o fază de WB a unei instrucţiuni
anterioare. În acest caz aşteptarea este obligatorie.
• dacă instrucţiunea din care se doreşte execuţia fazei IF face parte dintr-o
instrucţiune alternativă (if-then-else), dar încă nu s-a produs faza E din aceasta,
deci la momentul respectiv nu se cunoaşte pe care ramură avansează aplicaţia. În
acest caz se pot utiliza resurse suplimentare pentru încărcarea simultană a ambelor
ramuri ale instrucţiunii if, iar când se cunoaşte care ramură este cea aleasă, se
renunţă la cealaltă. Această rezolvare se utilizează în mod curent, deoarece situaţia
apare des în programare – aproximativ 15% din instrucţiunile unei aplicaţii sunt
instrucţiuni alternative. Sub numele de descompunere speculativă, aceeaşi idee
este tratată în subcapitolul 4.2.
Procesarea de tip pipeline este numită şi procesare scalară, deoarece permite
creşterea vitezei de procesare liniar cu creşterea numărului de faze din instrucţiuni diferite
care se pot executa simultan.
Timpul
0 1 2 3 4 5 6 7 8 9
IF D DF E WB
IF D DF E WB
IF D DF E WB
IF D DF E WB
IF D DF E WB
IF D DF E WB
IF D DF E WB
IF D DF E WB
IF D DF E WB
IF D DF E WB
IF D DF E WB
IF D DF E WB
Figura 3. Dezvoltarea temporală pentru o procesare ideală cu două fluxuri pipeline
Acest tip de procesare asigură, în cazul ideal, o viteză dublă faţă de procesarea pe un
singur flux pipeline (în exemplul din figura 3 se execută integral 12 instrucţiuni în 10 unităţi
de ceas). Exemplul următor arată că modul de programare este esenţial pentru viteza efectivă
de execuţie [7].
Să presupunem că dorim să adunăm 4 valori întregi, stocate fiecare pe 4 octeţi, în
variabilele a, b, c şi d, aflate în memorie începând cu adresa hexazecimală 1000. Prima
20
versiune a codului în limbaj de asamblare realizează ((a + b) + (c + d)). Figura 4 arată timpul
necesar acestei versiuni.
Versiunea 1
1 load R1, @1000
2 load R2, @1008
3 add R1, @1004
4 add R2, @100C
5 add R1, R2
6 store R1, @2000
Timpul
0 1 2 3 4 5 6 7 8 9 10 11
IF D DF
IF D DF
IF D DF E
IF D DF E
IF D DF E
IF D DF E WB
Figura 4. Dezvoltarea temporală pentru versiunea 1 cu 2 fluxuri pipeline
Versiunea 2
1 load R1, @1000
2 add R1, @1004
3 load R2, @1008
4 add R2, @100C
5 add R1, R2
6 store R1, @2000
Această schimbare care pare minoră conduce la prelungirea timpului de procesare, aşa
cum este arătat în figura 5.
Timpul
0 1 2 3 4 5 6 7 8 9 10 11
IF D DF
IF D DF E
IF D DF
IF D DF E
IF D DF E
IF D DF E WB
Figura 5. Dezvoltarea temporală pentru versiunea 2 cu 2 fluxuri pipeline
21
De această dată, perechea de instrucţiuni 1 şi 2 nu se mai poate lansa la acelaşi
moment, deoarece sunt dependente. La fel se întâmplă cu perechea următoare; instrucţiunea 5
aşteaptă încheierea instrucţiunii 4 pentru a intra în faza data fetch. Instrucţiunea 6 se lansează
imediat după lansarea instrucţiunii 5 şi astfel versiunea 2 propusă mai sus necesită 11 unităţi
de timp.
O altă posibilitate pentru adunarea celor patru valori este prezentată în continuare: se
utilizează un singur registru de memorie, folosind adunări succesive: (((a + b) + c) + d). În
acest ultim caz, figura 6 prezintă modul de execuţie.
Versiunea 3
1 load R1, @1000
2 add R1, @1004
3 add R1, @1008
4 add R1, @100C
5 store R1, @2000
Timpul
0 1 2 3 4 5 6 7 8 9 10 11
IF D DF
IF D DF E
IF D DF E
IF D DF E
IF D DF E WB
Figura 6. Dezvoltarea temporală pentru versiunea 3 cu 2 fluxuri pipeline
22
2.4. Folosirea ierarhiilor de memorii cache
se execută mai devreme, deoarece registrul R2 este eliberat la momentul 102 ns, valoarea din
R2 aşteaptă depunerea valorii variabilei a[2] în R1 şi deci instrucţiunea
add R1, R2
se execută în intervalul 304-305 ns. Transferul valorii a[2] + b[2] se face în intervalul 305-
406 ns. În concluzie, execuţia repetată a blocului de 4 instrucţiuni se face în blocuri de 203
ns, deci timpul total necesar este de n ⋅ 203 ns în cazul în care vectorii au câte n componente.
Dintre cele 4 instrucţiuni necesare aflării unui element din vectorul-sumă, trei (prima,
a doua şi a patra) necesită 101 ns pentru execuţie şi cea de-a treia se execută într-o
nanosecundă; de asemenea, primele două se pot executa concomitent, însă ultimele două se
execută secvenţial după ce primele două s-au încheiat.
Dacă însă se folosesc memorii cache, care pot stoca integral cei trei vectori şi dacă
presupunem că lărgimea de bandă permite transferul datelor într-o singură fază, atunci prima
23
dată se aduc toate datele de intrare în cache (100 ns), apoi se calculează toate valorile
vectorului c (aproximativ n ns deoarece se pot executa 4 instrucţiuni în paralel, iar aflarea
unei valori din vectorul c necesită tot 4 instrucţiuni) şi în final toate valorile se trec în RAM
în 100 ns. În acest caz timpul total este de n + 200 ns, de aproximativ 200 de ori mai mic
decât în cazul în care nu se utilizează memorii cache.
Creşterea vitezei de procesare a datelor prin metode implicite se mai poate realiza prin
eliminarea accesării datelor stocate la distanţă (stride elimination). Aducerea datelor din
memoria secundară (de exemplu, hard-disc) în memoria principală (RAM) nu se face
individual, ci prin intermediul paginilor de memorie – un grup de adrese adiacente în
memorie, care include şi valoarea cerută de procesor Un grup de date alăturate în memorie se
numesc date contigue (contiguous data). De obicei o pagină de memorie are 4 kB şi această
metodă a fost implementată deoarece s-a considerat că, de obicei, aplicaţiile folosesc date
stocate în zone alăturate. Acest principiu poate fi utilizat în reducerea timpului de procesare a
matricelor mari. Programatorii nu dau mare importanţă ordinii de parcurgere a unei matrice,
dar în cele ce urmează vom vedea că în cazul unor matrice mari, aceasta are impact major
asupra timpului de execuţie.
Versiunea 2
for (i = 0; i < 1000; i++)
b[i] = 0;
for (i = 0; i < 1000; i++)
for (j = 0; j < 1000; j++)
b[i] += a[i][j];
În acest caz, pentru i fixat, execuţia completă a structurii for interne necesită doar câteva
transferuri de date de pe hard-disc (în funcţie de tipul de dată utilizat şi de poziţia în pagina
de memorie a valorii a[i][j]).
24
Capitolul 3
Paralelism explicit
Acest capitol este dedicat arhitecturilor cu mai multe unităţi de procesare, care permit
specificarea explicită a proceselor care se pot desfăşura concurent. Spre deosebire de
capitolul anterior (Paralelism implicit), care tratează cazul unei arhitecturi mono-procesor,
acest capitol descrie facilităţile oferite de colecţiile de procesoare.
Trecerea de la programarea secvenţială la programarea paralelă nu este uşoară.
Metoda paralelizării unor aplicaţii secvenţiale deja scrise nu este cea mai eficientă. Pot exista
caracteristici ale problemei care nu sunt speculate de aplicaţiile secvenţiale şi care scapă
atenţiei programatorilor şi în cazurile paralele. Pe de altă parte, scrierea de la început a unor
aplicaţii paralele este o activitate dificilă, care necesită solide şi moderne cunoştinţe de
programare.
Aşa cum în cazul aplicaţiilor secvenţiale controlul (adică paşii aplicaţiei) este
specificat prin instrucţiuni, în cazul aplicaţiilor paralele este necesară specificarea controlului
(ce face fiecare proces) dar şi a modelului de comunicare (cum/când/cine/ce comunică).
O aplicaţie paralelă lipsită de comunicaţii poate fi gândită ca o execuţie în izolare a
proceselor, deci ar putea fi înlocuită cu înlănţuirea secvenţială a proceselor pe un singur
procesor. În acest caz, timpul de execuţie ar fi egal cu suma timpilor de execuţie a fiecărui
proces, câştigul adus de procesarea paralelă fiind reducerea vitezei de procesare (folosind
formulele din Capitolul 1, în cazul a n procese perfect echilibrate ca timp de execuţie,
acceleraţia este n şi eficienţa este 1). Deşi acesta este o situaţie extrem de eficientă,
rezolvarea unor probleme complexe a dovedit că o descompunere în procese care comunică
reuşeşte să găsească soluţii mai bune decât dacă nu există comunicare între procese. O
strategie de comunicare prin care procesele să schimbe inteligent date este cheia pentru a
obţine soluţii de calitate la problemele dificile ale lumii actuale.
27
paralelă. Această soluţie este generală (se poate utiliza pentru o clasă largă de aplicaţii
paralele), flexibilă (se poate echilibra în funcţie de consumul de resurse), dar necesită spaţii
suplimentare de memorie, deoarece fiecare sistem de calcul este controlat independent (figura
9). Sistemele bazate pe multiple procesoare (CPUs) sunt construite pentru a scădea latenţa:
• memoria cache este destinată măririi şanselor ca datele necesare să fie deja
disponibile, datorită precedentelor transferuri din memoriile externe
procesorului;
• unitatea de control (UC) permite predicţia ramificării codului (branch
prediction - codul se execută în avans, pe ramura cea mai probabilă a
structurilor de decizie, dar există şi instrumente de restaurare şi avans pe
celelalte ramuri, în cazul în care predicţia s-a dovedit a fi incorectă).
Structurile de decizie pot fi: instrucţiunea if-then-else, instrucţiunea
case/switch sau cele care realizează controlul instrucţiunilor repetitive (dacă se
reiau sau se trece la următoarea instrucţiune).
• unitatea de control (UC) permite utilizarea rapidă a valorilor calculate recent
(data forwarding) prin mecanisme de urmărire a locului din aplicaţie în care
este necesară o valoare tocmai calculată şi utilizarea acestei valori deşi aceasta
nu a fost încă depusă în zona de memoria secundară (pe hard-disc).
Orientarea către eliminarea latenţei face ca sistemele de calcul bazate pe CPU să fie
extrem de eficiente în cazul aplicaţiilor secvenţiale. Industria hardware a urmărit, de la
începuturi, eficientizarea acestor aplicaţii. Doar după ce s-a pus problema calculului paralel,
au început să apară preocupări în eficientizarea hardware pe această direcţie.
UC UP UC UP UC UP ... UC UP
UC
UP UP UP ... UP
28
Controlul este deci exterior unităţilor de procesare, care nu mai oferă facilităţile
branch prediction sau data forwarding. Unităţile aritmetico-logice (UALs) sunt numeroase,
eficiente energetic şi permit execuţie de tip pipeline. Deşi memoria disponibilă pentru GPU
induce o latenţă mult mai mare decât în cazul unui CPU (lipsesc facilităţile descrise în lista
precedentă), numărul mare de operaţii care se pot suprapune (arhitectura este puternic
orientată către suprapunerea de tip pipeline) face ca aproape la fiecare ciclu de ceas sa fie
încheiată execuţia unei instrucţiuni. Aceste caracteristici fac ca dispozitivele hardware de tip
GPU să poată executa mult mai eficient unele aplicaţii paralele decât dispozitivele CPU.
iar dacă dorim folosirea facilităţilor SIMD, atunci codul paralel (care alocă o poziţie din
vectorul c unui proces şi fiecare proces are acces la componentele corespunzătoare din cei doi
vectori de intrare) va conţine instrucţiunea:
c[current] = a[current] + b[current];
În primul caz procesorul execută 2000 de atribuiri (sunt necesare 1000 de atribuiri
pentru contorul i şi 1000 de atribuiri pentru elementele vectorului c). În al doilea caz se
execută simultan 1000 de atribuiri pentru variabila locală current şi apoi simultan 1000 de
atribuiri pentru calculul valorii corespunzătoare din vectorul c.
Situaţia se complică dacă există instrucţiuni decizionale în codul secvenţial. De
exemplu, dacă există instrucţiunea:
if (x == 0) z = y; else z = y/x;
execuţia sa pe seturi diferite de date necesită doi paşi: întâi se atribuie valoarea din y
variabilei z pentru toate procesele care au valoarea 0 stocată în variabila x şi apoi se atribuie
valoarea y/x variabilei z pentru restul proceselor. În toate procesele paralele, se lucrează pe
valorile curente (locale procesului) ale variabilelor. Instrucţiunea pentru versiunea paralelă
este:
where (x == 0) z = y; elsewhere z = y/x;
D1 D2 D3 D4
înainte de x 0 x 2 x 3 x 0
where y 7 y 8 y 3 y 2
z z z z
atunci etapa 1 în execuţia instrucţiunii where constă în alocarea valorilor variabilei z din
seturile D1 şi D4:
29
D1 D2 D3 D4
etapa 1 din x 0 x 2 x 3 x 0
where y 7 y 8 y 3 y 2
z 7 z z z 2
D1 D2 D3 D4
etapa 2 din x 0 x 2 x 3 x 0
where y 7 y 8 y 3 y 2
z 7 z 4 z 1 z 2
Principiul comunicaţiilor prin spaţiu partajat de adrese este des întâlnit în natură, fiind
o alternativă la mai des întâlnitul principiu al schimbului de mesaje. În locul sincronizării
expeditorului cu destinatarul de fiecare dată când este necesară o transmisie de date (schimb
de mesaje), comunicaţia prin adrese partajate nu necesită sincronizare: expeditorul depune în
spaţiul comun valoarea pe care doreşte să o transmită şi destinatarul o accesează atunci când
are nevoie de valoarea respectivă.
Aceeaşi metodă este întâlnită în natură la coloniile de furnici, care depun pe sol o
substanţă specifică (feromon) pe care o recunosc şi o interpretează cele care trec ulterior prin
acelaşi loc. Deşi sunt insecte aproape oarbe, furnicile reuşesc să găsească în scurt timp
drumul cel mai scurt de la sursa de hrană la cuib. Un alt model este partajarea vehiculelor,
pentru economisirea carburantului, micşorarea emisiilor poluante şi descongestionarea
şoselelor. În figura 11 este prezentat indicatorul pentru benzile HOV (high-occupancy vehicle
lanes) dedicate, iar în figura 12 se află o imagine a unei astfel de benzi. Controlul utilizării
30
benzilor HOV se face prin sisteme automate de captare şi interpretare a imaginilor,
asigurându-se acces liber doar autovehiculelor în care se află cel puţin două persoane
(posesorii celorlalte autovehicule sunt amendaţi).
Sistemele de calcul care suportă programarea de tip SPMD se numesc sisteme multi-
procesor. Dacă timpul pentru accesarea memoriei este identic pentru fiecare spaţiu de
memorie, atunci platforma este de tip Uniform Memory Access (UMA) (figura 13). În caz
contrar, platforma este de tip Non-uniform Memory Access (NUMA) (figura 14).
Comenzile de scriere în memoria comună sunt complexe la nivelul implementării,
deoarece necesită mecanisme de blocare, specificate în continuare, în acest subcapitol.
UP UP UP ... UP
M M M ... M
31
UP M UP M ... UP M
Cea mai eficientă arhitectură cu adrese partajate este oferită de memoria PRAM
(Parallel Random Access Machine): mai multe procesoare accesează un spaţiu comun de
memorie. Un model asemănător este metoda didactică a rezolvării la tablă a unei probleme:
profesorul scrie informaţiile la tablă, iar elevii le preiau, asigurându-se că la momentul când
profesorul şterge tabla şi depune noi informaţii pe tablă, cele precedente au fost deja utilizate
(învăţate sau scrise în caiete) şi deci nu mai este necesară prezenţa lor în spaţiul comun (adică
tabla).
Acest model este cunoscut în Inteligenţa Artificială sub numele de modelul
Blackboard [31], fiind folosit pentru a rezolva probleme complexe care se descompun în sub-
probleme dependente, având următoarele componente:
• baze de cunoştinţe (knowledge sources) stocate distribuit; fiecare astfel de bază poate
rezolva independent o sub-problemă;
• o zonă de memorie la care au acces bazele de cunoştinţe, care conţine problema
iniţială, sub-problemele în care aceasta s-a descompus, soluţiile acestor sub-probleme,
modul în care se utilizează aceste soluţii în rezolvarea altor sub-probleme sau a
problemei iniţiale, etc.;
• un modul de control, care asigură desfăşurarea eficientă a procesului de rezolvare a
problemei iniţiale, care defineşte şi urmăreşte strategia de rezolvare a acesteia. Scopul
modulului este de a împiedica deducţiile nefolositoare rezolvării problemei.
Proiectarea şi programarea acestui modul este esenţială şi arată importanţa
conceptului de oportunism în informatică. Specularea oricăror informaţii şi metode
care ajută la rezolvarea rapidă a problemelor ajunge să fie o artă, care se bazează pe
cunoştinţe avansate, pe experienţa şi pe cooperarea echipei de specialişti care lucrează
la rezolvarea problemei respective.
Câteva sisteme open-source destinate implementării modelului tablei sunt [21, 31].
32
serializarea proceselor care în celelalte cazuri (descrise în continuare) s-ar putea
desfăşura în paralel.
• Concurrent Read, Exclusive Write (CREW) – modelul cel mai întâlnit, care
serializează scrierile, dar permite citirile simultane din aceeaşi zonă de date.
• Exclusive Read, Concurrent Write (ERCW) – modelul care serializează citirile
aceleaşi zone de memorie, dar permite scrierea simultană în acelaşi loc.
• Concurrent Read, Concurrent Write (CRCW) – cel mai puternic model PRAM,
care eliberează programatorul de grija operaţiilor de intrare/ieşire. Toate operaţiile
de citire/scriere care doresc să acceseze simultan o zonă de memorie se execută
(unele având efectul scontat de programatorul procesului, altele nu) şi aplicaţia nu
întârzie.
UP M UP M ... UP M
33
Limbajele de programare care permit schimbul de mesaje implementează şablonul
general MPI (Message Passing Interface), aflat acum la versiunea 3 [32]. Printre cele mai
utilizate implementări MPI menţionăm [23, 25, 26].
Sistemele distribuite (cu spaţii exclusive de memorie) folosesc comunicarea prin
schimb de mesaje, pe când metoda alternativă (comunicarea prin acces la memoria comună)
este potrivită sistemelor de calcul paralel.
Schimbul de mesaje se realizează întotdeauna într-un context (un grup), care defineşte
o submulţime a calculatoarelor legate în reţea care pot comunica. La un moment dat, într-o
aplicaţie distribuită, pot exista mai multe grupuri, dar o comunicaţie are loc într-un anumit
grup. O comunicaţie presupune existenţa a cel puţin unui expeditor (sursă) şi a cel puţin unui
destinatar.
În funcţie de participanţii la comunicaţie, acestea pot fi de următoarele tipuri:
• 1 – 1 (one-to-one), când schimbul de mesaje se realizează prin perechea de
comenzi Send (la sursă)/Receive (la destinaţie);
• 1 – toţi (one-to-all), când sursa trimite câte un mesaj fiecărui proces din grup,
folosind una dintre comenzile Broadcast sau Scatter;
• toţi – 1 (all-to-one), când un singur destinatar colectează mesaje de la toţi
participanţii din grup, cu ajutorul comenzilor Gather sau Reduce.
mesaj
Memorie sursă → Buffer sursă Buffer destinaţie → Memorie destinaţie
(MS) (BS) (BD) (MD)
Sursă Destinaţie
Pentru realizarea transmisiei, sursa trebuie să execute o comandă Send, iar destinaţia
are de executat o comanda Receive. În modelul general, parametrii acestor comenzi sunt:
Send (mesaj, destinatar, eticheta, grup)
Receive (mesaj, sursa, eticheta, grup, stare)
unde: mesaj descrie identificatorul structurii locale de date care conţine mesajul,
destinatar/sursa sunt identificatorii celuilalt proces, eticheta permite împerecherea corectă a
celor două comenzi, deoarece identifică mesajul în mod unic, grup reprezintă contextul în
care se realizează comunicarea, iar stare este o structură de date care reflectă modul în care s-
a realizat efectiv comunicaţia.
Existenţa buffer-ului care intermediază schimbul permite următoarele variante de
comunicare:
• blocking, în care sursa rămâne în execuţia comenzii Send până când buffer-ul său
(BS din figura 16) poate fi rescris. De asemenea, destinaţia stă în execuţia
comenzii Receive până când buffer-ul său (BD din aceeaşi figură) conţine întreg
mesajul transmis. După ce se îndeplineşte condiţia corespunzătoare lui, fiecare
proces poate începe execuţia următoarei instrucţiuni. Această variantă presupune
deci că procesele nu pot avansa decât după ce comunicaţia s-a realizat efectiv
(deci se poate spune „comunicaţia a sincronizat procesele”).
34
• non-blocking, în care fiecare dintre comenzile Send/Receive este împărţită în două
faze, între care fiecare proces poate executa alte comenzi:
o iniţierea comunicării
o testarea realizării comunicării.
Această variantă permite ca aplicaţiile să avanseze ne-sincron, nefiind necesară
execuţia în acelaşi timp a comenzii Send la sursă şi a comenzii Receive la
destinaţie.
În cazul comunicării de tip 1 – toţi, se presupune existenţa unui grup, în cadrul căruia
există un proces Sursă, care transmite câte un mesaj fiecărui proces din grup, inclusiv lui
însuşi. Comanda Broadcast transmite acelaşi mesaj tuturor proceselor din grup:
date date
a a
1 7 Broadcast (a, 1, grup) 1 7
procese
procese
2 2 7
3 3 7
4 4 7
Figura 17. Efectul unei comenzi Broadcast într-un grup de 4 procese
Comanda Scatter transmite mesaje diferite tuturor proceselor care compun un grup:
35
Această comandă este generalizarea comenzii Broadcast, în sensul că o colecţie de
date stocată la sursă este fragmentată şi fiecare element este transmis unui proces, în ordinea
identificatorilor proceselor din grup. Dacă presupunem că grupul are n calculatoare, atunci
comenzile Scatter executate de fiecare sunt echivalente cu n comenzi Send executate de Sursă
şi câte o comandă Receive executată de fiecare proces. În figura 18 este prezentat rezultatul
unei comenzi Scatter executată pe fiecare dintre cele 4 procese ale unui grup, în care procesul
1 este sursa şi trimite fiecărui proces din grup (în ordinea identificatorilor) câte o componentă
a vectorului A.
date date
A b A b
1 7 3 5 2 Scatter (A, b, 1, grup) 1 7 3 5 2 7
2
procese
2 3
procese
3 3 5
4 4 2
date date
A b A b
1 2 1 2 9 4 6 2
Gather (b, A, 1, grup)
procese
2 9 2 9
procese
3 4 3 4
4 6 4 6
Comanda Reduce presupune utilizarea unui operator asociativ (fie predefinit, fie
definit în aplicaţie) – exemplul din Figura 20 se referă la operatorul de adunare (SUM). Prin
comanda Reduce, la destinaţie se colectează valorile obţinute prin aplicarea repetată a
operatorului specificat, pentru fiecare dintre structurile de date primite de la fiecare proces
din grup. Dacă presupunem că grupul are n calculatoare, atunci comenzile Reduce executate
de fiecare sunt echivalente cu n comenzi Receive executate de Destinaţie, câte o comandă
Send executată de fiecare proces şi o prelucrare a rezultatelor finale. În figura 20, fiecare
dintre cele 4 componente ale vectorului B se obţine prin adunarea componentelor
corespunzătoare din fiecare vector A, stocat pe fiecare calculator din grup. De exemplu, B[1]
este suma valorilor stocate în A[1] de către fiecare proces.
36
date date
A B
1 2 2 1 0 Reduce (A, B, SUM, 1, grup) 1 7 6 3 6
procese
2 3 1 0 4 2
procese
3 0 3 1 1 3
4 2 0 1 1 4
Reţelele alcătuite prin conectarea mai multor unităţi de procesare destinate calculului
paralel sunt de obicei reţele regulate, cu o topologie strict orientată către viteze mari de
transmisie a mesajelor şi distanţe scurte între procesoare. De aceea, reţelele mici sunt
complete de cele mai multe ori, iar cele mari respectă următoarele topologii [7]:
• Stea (star): un procesor este legat la toate celelalte procesoare. De obicei este o
reţea eterogenă, procesorul central asumându-şi rolul de server, iar celelalte – pe
cel de client.
• Vector (cu sau fără circularitate; în primul caz obţinem reţeaua de tip inel): toate
procesoarele sunt conectate la o magistrală (bus).
• Arbore (tree): topologia este de arbore (graf neorientat conex şi fără circuite). În
figura 21 a) este prezentat un arbore binar cu 8 noduri terminale. Cea mai dificilă
problemă a acestei topologii este aglomerarea mesajelor în zona rădăcinii,
deoarece muchiile conexe rădăcinii fac trecerea între sub-arborele stâng şi cel
drept al acesteia. Din acest motiv, arborii sunt eficientizaţi prin creşterea lărgimii
de bandă pentru conexiunile din apropierea rădăcinii, ca în figura 21 b).
• Plasă (mesh): procesoarele sunt legate cu vecinii aflaţi în nodurile unei reţele cu
două sau mai multe dimensiuni. Reţeaua poate fi cu circularitate (toroidală) sau
deschisă. În figura 22 sunt prezentate cele două cazuri pentru o plasă 2D cu 9
noduri. În figura 23 se află o reţea mesh 3D cu 27 de noduri. Pentru cazul general,
se defineşte reţeaua de tip k-d mesh: o plasă din k noduri în d dimensiuni. O reţea
2-d mesh este denumită hipercub. În figura 24 este prezentată seria 2-0 (a), 2-1
(b), 2-2 (c), 2-3 (d) şi 2-4 (e) de hipercuburi.
a) b)
Figura 21. Reţea arbore (tree) şi arbore masiv (fat tree)
37
a) b)
Figura 22. Reţea plasă 2D a) deschisă b) circulară
a) b) c) d) e)
Figura 24. Hipercuburi în k = 0 - 4 dimensiuni [7]
38
Metode eficiente de comunicare multiplă
P1
P2
P3
P4
timp
7 6 5 4
0 1 2 3
1
2
3 3
39
Primul pas constă în comunicaţia între 0 şi 4. La pasul 2 se efectuează două
comunicaţii: 0-2 şi 4-6. La pasul 3 se realizează 4 schimburi de mesaje: 0-1, 2-3, 4-5 şi 6-7.
Dacă fiecare hop (calculator intermediar) dintr-o comunicaţie este traversat într-o unitate de
timp, atunci pentru încheierea comenzii Broadcast pe cele 8 calculatoare sunt necesare: 4 + 2
+ 1 = 7 unităţi de timp, adică timpul optim, deoarece doar comunicaţia 0 – 7 (cel mai lung
traseu) necesită 7 unităţi de timp. Metoda descrisă (care se numeşte metoda dublării
recursive, deoarece la fiecare pas se dublează numărul de transmisii de date) are o proprietate
foarte importantă: fiecare canal de comunicaţie este parcurs mereu de cel mult un mesaj, deci
niciun canal nu este aglomerat.
3 7 11 15
4 4 4 4
2 6 10 14
1 5 9 13
3 3 3 3
4 4 4 4
0 4 8 12
1
2 2
40
Implementarea unei comenzi Broadcast pe o reţea mesh 3D cu 8 calculatoare [7]
Pentru reţeaua mesh 3D din figura 28, execuţia unei comenzi Broadcast cu sursa 0
prin metoda dublării recursive conduce la 3 faze în transmisie, evidenţiate prin săgeţile
întrerupte care sunt numerotate corespunzător:
• o primă comunicaţie între 0 şi 4, care durează o unitate de timp;
• două transmisii efectuate în paralel, 0 – 2 şi 4 – 6, care durează tot o unitate de
timp;
• patru schimburi simultane de mesaje, 0 – 1, 2 – 3, 4 – 5 şi 6 – 7, care de asemenea
durează o unitate de timp.
Metoda dublării recursive este optimă, deoarece realizează transmisia în cel mai scurt
timp (calculatoarele 0 şi 7 au nevoie de 3 unităţi de timp pentru a transfera un mesaj) şi nu
există aglomerare pe canalele de comunicaţie.
41
Capitolul 4
Proiectarea unui bun algoritm concurent este dificilă, deoarece trebuie să respecte
principiile generale de eficienţă, care să îi asigure succesul indiferent de condiţiile de
execuţie, dar să şi ţină cont de particularităţile problemei rezolvate. Principiile generale de
eficienţă sunt prezentate în continuare, exemplificate pentru câteva probleme simple;
programatorul care este pus în situaţii efective de rezolvare a unei probleme poate astfel
aplica aceste principii şi le poate adapta situaţiei sale concrete. În capitolul următor sunt
descrise câteva probleme reale, care apar des în proiectele software şi pentru care principiile
generale de eficienţă sunt discutate.
Figura 29. Graf orientat ciclic Figura 30. Graf orientat aciclic
43
de procesare, sau se pot executa în orice ordine atunci când nu sunt suficiente unităţi de
procesare. Graful de precedenţă are deci n noduri şi niciun arc (figura 31).
1 2 3 ... n-1 n
• P2 calculează c n , c n , K , c n ;
+1 +2 2
k k k
• ...
• Pk calculează c n ,c n ,K, c n .
( k −1) +1 ( k −1) + 2
k k
În figura 32 este prezentat graful de precedenţă pentru k = 4.
1 2 3 4
Rezolvare. Această comandă SELECT foloseşte un singur tabel (CARS), din care
extrage articolele care îndeplinesc o condiţie complexă. Vom nota sub-mulţimile de articole
din CARS care sunt implicate în această execuţie astfel:
• A1 este formată din maşinile Logan;
• A2 conţine maşinile produse în anul 2012;
• A3 are date despre maşinile albe;
• A4 se referă la maşinile gri.
Comanda SELECT are deci scop listarea elementelor mulţimii
A = A1 I A2 I ( A3 U A4 ) .
Elementele mulţimii A se pot afla în paralel în mai multe moduri. În cele două cazuri
pe care le vom analiza, paralelismul execuţiei va fi evaluat, folosind graful de precedenţă.
Rezolvarea 1. O posibilă rezolvare a problemei construieşte mulţimea A astfel:
• A5 = A1 I A2
• A6 = A3 U A4
• A = A5 I A6 .
44
Dacă există procesoare disponibile, atunci graful de precedenţă pentru acest mod de
rezolvare a problemei este prezentat în figura 33 (mulţimile A5 şi A6 se pot găsi în paralel).
A1
A5
A2
A
A3
A6
A4
Gradul de concurenţă este dat de numărul de procese care se pot desfăşura simultan. În acest
4 + 2 +1
caz, gradul mediu de concurenţă este = 2,33 . Dacă presupunem că durata de
3
execuţie a proceselor este aceeaşi (în lipsa unor informaţii privind durata lor reală), atunci
drumul critic în graful de precedenţă din figura 33 are lungimea 2. Reamintim că drumul
critic într-un graf aciclic este lungimea celui mai mare drum care leagă două noduri.
Rezolvarea 2. O rezolvare cu un grad mediu de concurenţă mai scăzut poate construi
mulţimea A astfel:
• A5 = A3 U A4
• A6 = A2 I A5
• A = A1 I A6 .
A1
A
A2
A6
A3
A5
A4
4 +1+1+1
În acest caz, gradul mediu de concurenţă este = 1,75 şi drumul critic are
4
lungimea 3.
45
În capitolul următor vom prezenta o problemă pentru care vom analiza din nou efectul
ordinii efectuării unor operaţii asociative. Aici am considerat că durata proceselor este
identică; în subcapitolul 5.2 vom vedea care este impactul unor durate diverse asupra
timpului total de execuţie al unei aplicaţii paralele.
Evaluarea paralelismului
În cazul unei aplicaţii paralele, programatorii sunt interesaţi de optimizarea
următoarelor caracteristici:
• gradul maxim de concurenţă, care arată numărul maxim de unităţi de procesare
disponibile, fiind deci o valoare limitată de configuraţia hardware pe care se
execută aplicaţia;
• gradul mediu de concurenţă, care arată cât de „paralelizată” este o aplicaţie, fiind
o imagine a eficienţei algoritmului;
• drumul critic al grafului de precedenţă, care este lungimea maximă a drumurilor în
graf, obţinută prin ponderarea nodurilor cu timpii corespunzători de execuţie.
Această valoare arată timpul total de execuţie.
46
Exemple de paralelizare prin descompunere recursivă sunt: calculul termenului
generic din şirul lui Fibonacci (prezentat în Capitolul 1), sau sortarea prin metoda rapidă
(quick-sort).
Descompunerea speculativă este descompunerea funcţională în care se lansează (pe
procesoare care altfel nu ar avea încărcare) în avans procese-alternative: procese care
modelează ramurile unei instrucţiuni if-then-else sau case/switch. Când procesul-părinte
ajunge la respectiva instrucţiune şi decide care este ramura pe care se va avansa, atunci se
închid toate procesele-fiu şi se preia starea procesului-fiu corespunzător ramurii alese.
47
9 8 6 4
8 9 6 8 4 6 5
7 6 9 4 8 5 6
6 7 4 9 5 8 7
5 4 7 5 9 7 8
4 5 7 9
1 2 3 4 5 6 timp
O reţea de sortare simplă care permite la intrare o secvenţă de n valori necesită cel
n( n − 1)
puţin C n2 = comparatoare (deoarece un comparator poate produce cel mult o
2
inversiune – atunci când secvenţa este privită ca o permutare; datele descrescătoare, ca în
exemplul din figura 40, necesită numărul maxim de inversiuni). Din acest punct de vedere,
reţeaua din fig. 40 este optimă, având exact C 62 = 15 comparatoare.
Eficienţa reţelei simple de sortare cu un număr minim de comparatoare este mare,
deoarece:
• transferul de date se face în proximitate, consumul de resurse fiind minimal;
• dispozitivele hardware (comparatoarele) sunt în număr minimal;
• comparările se pot realiza în paralel – acesta fiind singura caracteristică unde
intervin cunoştinţele programatorului şi unde se pot obţine execuţii mai scurte.
Exemplul următor ilustrează cum organizări diferite conduc la timpi diferiţi de
execuţie.
65
Timpul necesar pentru implementarea sa secvenţială este dat de următoarea relaţie de
recurenţă:
T ( n) = (n − 1) + T (n − 1)
n( n − 1)
şi cum T(2) = 1, avem că T ( n) = ( n − 1) + ( n − 2) + K + 1 =
2
Bloc pentru n – 1
elemente
n elemente
Dacă, însă, metoda de sortare permite utilizarea simultană a mai multor comparatoare,
atunci timpul de sortare poate scădea. De exemplu, prima comparare aferentă Blocului pentru
sortarea a n - 1 elemente se poate realiza la momentul 3, nu se aşteaptă până la momentul n.
Sunt necesare 9 unităţi de timp, nu 15 cum ar rezulta din formula de mai sus pentru n = 6
(figura 42).
6 elemente
1 2 3 4 5 6 7 8 9 timp
66
Reţeaua din figura 42 este simplă, optimă din punct de vedere al numărului de
comparatoare (15), dar are nevoie de mai mult timp decât sortarea din fig. 40, care este mai
compactă. Gradul de paralelizare al sortării din ultima figură este mai scăzut.
O altă idee de paralelizare este găsirea în paralel a optimului (valorii maxime) din
secvenţa de n elemente, folosind metoda turneului, prezentată deja în acest subcapitol.
Relaţia de recurenţă pentru timpul de sortare în paralel devine astfel:
T ( n) = log(n) + T ( n − 1)
Iată cum, deşi procesele sincronizează la fiecare trecere la o secvenţă mai scurtă
(producând întârzieri inerente), paralelizarea produce o scădere semnificativă a timpului de
sortare.
La nevoie, dacă problema are restricţii semnificative de timp, atunci aceste două idei
se pot combina. Algoritmul rezultat este mult mai complex, dar asigură un timp de execuţie
extrem de scăzut.
67