Sunteți pe pagina 1din 546

Lucian N.

VINȚAN

FUNDAMENTE ALE ARHITECTURII


MICROPROCESOARELOR

ISBN: 978-606-25-0276-8
http://www.matrixrom.ro/romanian/editura/domenii/cuprins.php?cuprins=FA50

Matrix Rom, București


2016
PREFAȚĂ

Scopul principal al acestei cărți este acela de a familiariza cititorul cu


anumite aspecte, considerate mai importante și, de asemenea, mai puțin
perisabile, referitoare la arhitectura microprocesoarelor de azi și, eventual, chiar
de mâine. Așadar, cartea de față are un preponderent caracter formativ,
focalizându-se pe prezentarea unor cunoștințe generice, fundamentale, oarecum
independente de implementări particulare, comerciale. Totuși, bazat pe propria
experiență de predare [Vin15b], dar și de cercetare științifică a autorului în
domeniul arhitecturilor de calcul, credem că prezentarea este una originală,
reflectând o meditație și un efort de înțelegere relativ îndelungate asupra
subiectelor tratate, situate la nivelul interfeței hardware-software. Pentru fiecare
metodă sau tehnică prezentată s-a încercat revelarea metaforei cognitive care stă
la baza acesteia, în speranța că, astfel, cunoștințele se vor consolida. Uneori,
anumite contribuții originale ale autorului în domeniul sistemelor de calcul, sau
ale unor cercetători consacrați, publicate în reviste și în conferințe internaționale
de un bun nivel științific, sunt integrate în prezentare, “topite” într-o formă mai
accesibilă (spre exemplu, prezentarea fundamentelor predicției dinamice
neuronale a instrucțiunilor de salt condiționat, idee introdusă în premieră de
autor, la nivel mondial, prin publicarea unui articol la conferința International
Joint Conference on Neural Networks, Washington DC, USA, 10-16 July, 1999).
Considerăm că integrarea unor contribuții științifice ale autorului, sau familiare
acestuia, constituie un demers binevenit într-un tratat universitar, contribuind la
o viziune specifică utilă, la puncte de vedere care pot stârni interesul,
curiozitatea și chiar dezacordul (!) cititorului. Credem că este nevoie de
asemenea cărți, care nu intră – pentru că nici nu ar putea-o face – în concurență
cu marile text-book-uri ale domeniului (unele citate în bibliografie), ci,
dimpotrivă, prezintă domeniul într-un mod mult mai succint și insistând pe
tematicile mai familiare autorului, printr-o experiență de cercetare științifică
proprie.
Acest tratat universitar reutilizează și integrează într-un tot unitar, sub o
formă îmbunătățită și extinsă într-un mod semnificativ, părți ale unor lucrări
anterioare, scrise și publicate de autor sub forma unor monografii sau articole
tehnico-științifice. Autorul a rescris și a actualizat anumite părți ale acestor
lucrări, care abordau chestiuni esențiale referitoare la arhitectura sistemelor de

5
calcul, revizuindu-le, sintetizându-le și îmbogățindu-le în cartea de față, în
speranța că sub această formă nouă, poate mai didactică decât versiunile
anterioare, informația prezentată va fi mai ușor asimilabilă de către studenții și
specialiștii interesați. S-au utilizat în acest scop, în principal, lucrările VINŢAN
N. LUCIAN – Arhitecturi de procesoare cu paralelism la nivelul instrucțiunilor,
Editura Academiei Române, Bucureşti, 2000; VINŢAN N. LUCIAN –
Prediction Techniques in Advanced Computing Architectures (în limba engleză),
Matrix Rom Publishing House, Bucharest, 2007; VINŢAN N. LUCIAN,
FLOREA ADRIAN – Microarhitecturi de procesare a informaţiei, Editura
Tehnică, Bucureşti, 2000; FLOREA ADRIAN, VINŢAN N. LUCIAN –
Simularea şi optimizarea arhitecturilor de calcul în aplicaţii practice, Editura
Matrix Rom, Bucureşti, 2003, dar și altele, importante, ale autorului sau ale
altora, precum anumite text-book-uri de notorietate și apreciere mondiale în
domeniul sistemelor de calcul (v. bibliografia cărții). Așadar, textele preluate din
propriile lucrări ale autorului au fost rescrise, cu modificări și adăugiri
semnificative și structurate aici sub forma unei cărți de sine stătătoare, care
“curge” natural, de la simplu la complex și care, speră autorul, dă seama asupra
unor aspecte tehnico-științifice fundamentale ale microprocesoarelor de uz
general, ale modului în care acestea procesează programele. În ciuda
caracterului preponderent formativ, la nivel universitar, considerăm că o parte
importantă a acestei lucrări conține informații și cunoștințe izvorâte dintr-o
experiență vie a autorului în acest domeniu, atât pe plan didactic, cât și științific,
care ar putea trezi curiozitatea specialiștilor. Spre exemplu, în acest sens,
arhitectura sistemelor de calcul este prezentată, în premieră într-un curs
universitar credem noi, într-o strânsă legătură cu anumite metode de învățare
automată (machine learning, de tip rețele neuronale, algoritmi genetici etc.) dar
și cu anumite metode matematice, deosebit de utile în analiza unor modele de
procesare (algebre logice de tip fuzzy, metode euristice de optimizare multi-
obiectiv, teoria informației etc.) Aceasta exprimă, de fapt, viziunea
interdisciplinară a autorului, dezvoltată și prin activitatea sa științifică de mai
bine de 25 de ani, prin care încearcă să contribuie, după modestele sale puteri, la
maturizarea empiricei discipline inginerești numite “arhitectura sistemelor de
calcul”, prin utilizarea și adaptarea unor metode teoretice mai riguroase. Printre
lucrările de pionierat în acest sens au fost și cele semnate de autor, fertile prin
multiple citări independente, anume STEVEN G., VINȚAN L. - Modelling
Superscalar Pipelines with Finite State Machines, "Proceedings of the 22nd
Euromicro’96 Conference. Beyond 2000: Hardware/Software Design

6
Strategies", September 1996, Prague, Czech Republic, pp. 20-25, IEEE
Computer Society Press, Los Alamitos, California, USA, ISBN 0-8186-7703-1,
Library of Congress Number 96-79894 respectiv VINȚAN L. - Towards a High
Performance Neural Branch Predictor, Proceedings of The International Joint
Conference on Neural Networks - IJCNN ’99 (CD-ROM, ISBN 0-7803-5532-6),
pp. 868 – 873, vol. 2, Washington DC, USA, 10-16 July, 1999 (această a 2-a
lucrare a introdus, în premieră, conceptul de predictor dinamic neuronal în
arhitectura calculatoarelor, având ~60 de citări independente până în anul 2016.)
Într-adevăr, arhitectura sistemelor de calcul este încă o știință inginerească
preponderent empirică, insuficient de matură din punct de vedere teoretic, bazată
în principal pe metode de benchmarking. Dezvoltarea sa a fost una predominant
conjuncturală, generată deseori de limitări tehnologice particulare, lipsindu-i un
cadru de dezvoltare riguros, matematizat, încă de la începuturi. Astfel, ideile
novatoare ale domeniului au apărut, deseori, fără a se conștientiza suportul lor
teoretic mai adânc, deseori comun. Spre exemplu, ideea de memorie virtuală
apare implementată prin anul 1962, înaintea celei de memorie cache (1965).
Înțelegerea faptului că, în esență, sunt idei cu o bază comună din punct de
vedere teoretic (al teoriei statistice a probabilităților), a apărut ulterior. Un alt
exemplu: predictoarele dinamice aferente instrucțiunilor de salt condiționat s-au
dezvoltat fără ca inventatorii lor să înțelegă că acestea sunt, de fapt, predictoare
stohastice de tip Markov. Abia în anul 1996, dr. Trevor Mudge înțelege acest
fapt și publică un articol lămuritor, dar, din păcate, cu efecte limitate asupra
domeniului. În fine, un alt exemplu: procesarea vectorială nu este prezentată în
literatura de specialitate în contextul conceptului natural, mai general, de spațiu
vectorial. Conștientizarea faptului că procesarea vectorială are ca bază teoretică
noțiunea fertilă de spațiu vectorial euclidian normat, ar putea avea nu doar un
beneficiu cognitiv, ci și unul utilitar, concret. În baza acestei conștientizări s-ar
putea dezvolta, spre exemplu, procesoare cu facilități hardware de evaluare a
similarității a doi vectori, pe baza unor norme matematice, care ar putea accelera
semnificativ aplicații de tip clasificare/clustering. Și astăzi remarcăm o
dezvoltare oarecum dezordonată, conjuncturală, a domeniului (a se vedea, spre
exemplu, dezvoltarea ad-hoc a sistemelor multicore din ultimii ani, precum și
cea a limbajelor de programare concurente, prin intermediul cărora acestea să fie
exploatate corespunzător.) O mare problemă teoretică a domeniului
arhitecturilor de calcul constă în faptul că cercetările, în marea lor majoritate
bazate pe benchmarking, după cum am mai menționat, nu sunt reproductibile
sau sunt extrem de dificil (laborios) reproductibile. Complexitatea domeniului,

7
legată în principal de procesarea unor programe obiect de mari dimensiuni și
care și-au pierdut semantica în urma compilării, este departe de a fi stăpânită în
mod corespunzător. Metodologiile de cercetare și de evaluare a performanțelor
sunt încă relativ empirice. De aceea, credem că orice efort de a „matematiza”
această știință predominant empirică, merită subliniat.
În ciuda caracterului preponderent formativ al acestei cărți, ea
încearcă să sugereze cititorului și anumite idei științifice, unele, poate, chiar
novatoare. De altfel, la nivelul unui tratat sau curs universitar, amprenta
științifică sau de interpretare originală a autorului sunt nu doar binevenite ci,
credem noi, chiar necesare. (În caz contrar, am preda cu toții, 100% după text-
book-urile clasice, într-o uniformizare păguboasă, care nu este specifică
universităților autentice.) Astfel, un mesaj esențial al acestei cărți este acela
că, domeniul numit Computer Architecture constituie, de fapt, un set uriaș
de studii de caz pentru cercetări științifice autentice, mature, pe o bază
matematică rafinată. Mai mult, domeniul empirico-ingineresc al
arhitecturii sistemelor de calcul poate induce și motiva cercetări științifice
mai generale, cu adevărat profunde. Pentru asta însă, este nevoie de o
generalizare a problemei particulare, tratate în acest context pur ingineresc. Iată
doar câteva asemenea probleme deschise, potențial fertile, cel puțin în opinia
autorului, prezentate în carte în mod natural și izvorâte din chiar propria sa
experiență de cercetare:

• Care este legătura dintre complexitatea programelor dinamice


deterministe și comportamentul lor, uneori impredictibil, entropic,
cvasi-aleator?
• Utilizarea cunoștințelor de domeniu în optimizarea multi-obiectiv a
sistemelor de calcul, ridică probleme științifice serioase, legate de
reprezentarea adecvată a cunoașterii de domeniu. Cum se poate
reprezenta în mod eficient cunoașterea din domeniul sistemelor de
calcul, în vederea optimizării eficiente a acestora? Cum poate fi
integrată această cunoaștere specifică, în algoritmii generali de
optimizare (în general, euristici)?
• Reprezentarea cunoașterii de domeniu, prin reguli logice de tip
fuzzy, prezentată în lucrare (la finele Capitolului 4), conduce la
problema deschisă a determinării gradului de contradicție intrinsec,
aferent unei astfel de micro-ontologii de domeniu, implicând
probleme conexe importante (spre exemplu, dacă acest grad de

8
contradicție este prea mare, ce se poate face? Eliminarea unor
reguli? Care?) Generalizări ale problemei, în contextul unor
ontologii mai generale (ca semantică, dar și ca mod de
reprezentare), ar putea fi de mare interes, atât pe plan cognitiv, cât
și utilitar (spre exemplu, stabilirea gradului de contradicție existent
într-un text scris în limbaj natural).
• Ce ar putea deveni paradigma de optimizare multi-obiectiv de tip
Pareto, într-o abordare a mulțimilor (fronturilor) Pareto în
paradigma teoriei fuzzy a mulțimilor? (Adică, să se determine
gradul de apartenență la frontul Pareto al fiecărui individ care
aparține acestuia, pe baza gradului mutual de dominanță. În toate
abordările actuale, un individ poate doar să aparțină, sau nu,
frontului Pareto.)
• Abordările meta-algoritmice, de genul meta-predicțiilor, meta-
clasificărilor, meta-optimizărilor etc., prezentate în această carte
strict în contextul arhitecturii calculatoarelor, induc în mod natural
noțiunea de sinergie. (Altfel, meta-algoritmica sau abordările
hibride n-ar mai prea avea sens.) Cum s-ar putea ajunge la o teorie
matematică riguroasă a noțiunii de sinergie, în context meta-
algoritmic? Care este legătura între sinergia aceasta și sistemele
neliniare din ingineria sistemelor? Etc.

Considerăm asemenea probleme deschise ca fiind potențial fertile.


Rezolvarea lor (aspectul cognitiv deci) ar putea conduce la soluționarea multor
probleme utilitare derivate. Pe de altă parte, asemenea abordări interdisciplinare
dezvăluie o perspectivă mai profundă a acestei discipline, considerată
preponderent empirică, cu utilitate multă (dată, în fond, de toate dispozitivele
electronice digitale de calcul pe care le folosim), dar implicând cunoaștere
științifică relativ puțină. Credem că situația este similară pentru multe alte
discipline din domeniul mai larg al ingineriei sistemelor de calcul.
Pe scurt, lucrarea este structurată astfel. Debutul se face cu o prezentare a
istoriei sistemelor electronice de calcul numeric, insistându-se nu doar pe geneza
și evoluția ideilor tehnico-științifice, dar și pe oamenii care le-au dezvoltat.
Apoi, se face o prezentare sintetică a structurii și funcționării unui microsistem
generic de calcul, insistându-se asupra aspectelor fundamentale (microprocesor,
memorii, interfețe de I/O, procesarea instrucțiunilor, moduri de lucru între
microprocesor și dispozitivele periferice etc.) Capitolul 2 prezintă arhitectura

9
sub-sistemului de memorie al unui sistem de calcul. Se arată că între viteza
microprocesorului (de ordinul sutelor de picosecunde în cazul celor mai
avansate) și timpul de răspuns al memoriei principale (de ordinul zecilor de
nanosecunde), respectiv al celei secundare (de ordinul câtorva milisecunde),
există o “prăpastie semantică”. În consecință, se prezintă caracteristicile
principale ale ierarhiei de memorii cache, dar și mecanismul de memorie
virtuală. Înainte de aceste detalii însă, autorul prezintă necesitatea acestor soluții,
problema propriu-zisă, care, deseori este mai importantă chiar decât unele soluții
particulare. De altfel, acest mod de prezentare, care startează cu prezentarea cât
mai clară a problemei puse în discuție, a importanței acesteia, reprezintă un
invariant al lucrării (credem că multe cursuri universitare păcătuiesc prin
prezentarea unor soluții excesiv de complicate, fără o precizare clară a
problemelor aferente acestora și a importanței acestor probleme în contextul dat;
plastic spus, “dețin soluție complicată, caut problema corespunzătoare!“). În
capitolul următor se prezintă fundamentele microprocesoarelor RISC scalare, cu
procesare pipeline a instrucțiunilor. Se insistă aici, în mod clasic, pe problemele
hazardurilor în structurile pipeline de procesare a instrucțiunilor și, în
consecință, pe schițarea principalelor soluții propuse în literatura de specialitate.
De asemenea, se prezintă aspecte importante legate de problematica excepțiilor
în procesoarele pipeline, analiza alias-urilor de memorie (memory
disambiguation), execuția predicativ-speculativă (sic!) a instrucțiunilor etc.
Capitolul 4 generalizează abordarea celui precedent, referindu-se, în principal, la
microprocesoarele cu paralelism la nivelul instrucțiunilor, dar și la alte tipuri de
sisteme de calcul mai avansate (spre exemplu, sisteme predictiv-speculative,
multithreading, multicore etc.) Se analizează atât abordările hardware
(algoritmul lui Tomasulo, bufferul de reordonare etc.) cât și cele software
(scheduling static al programului obiect) care urmăresc acest scop (Instruction
Level Parallelism), inclusiv pe baza unor studii de caz. Tot aici, se prezintă
ideile principale care stau la baza microprocesoarelor cu procesări multifir,
arhitecturile de calcul vectorial (SIMD), care exploatează paralelismul la nivelul
datelor, dar și câteva elemente fundamentale referitoare la sistemele paralele de
tip multiprocesor (multicore, MIMD). De asemenea, se prezintă în premieră în
literatura tehnică românească, cel puțin după știința autorului, un paragraf
focalizat pe câteva metode euristice de optimizare automată, de tip multi-
obiectiv, aplicate sistemelor de calcul complexe. Acestea sunt augmentate prin
utilizarea unor cunoștințe din domeniul arhitecturii procesoarelor, exprimate
prin logici de tip fuzzy, care le fac mai eficiente, dar și mai performante. Pe baza

10
experienței de cercetare științifică a autorului, se face aici inclusiv o introducere
în problematica meta-optimizării sistemelor de calcul, constând în utilizarea
concurentă a mai multor algoritmi de optimizare, cu beneficii sinergice. Se
continuă cu probleme propuse spre rezolvare, care provoacă cititorul să aplice în
mod practic-aplicativ, cunoștințele expuse în carte. Rezolvarea de aplicații
practice este esențială în procesul de învățare (și) al acestui domeniu. Lucrarea
se încheie cu o bibliografie selectivă și cu un glosar, în care se încearcă
explicarea sintetică a principalilor termeni tehnici utilizați în carte (deseori acești
termeni, preluați din limba engleză, nu mai necesită traduceri în limba română,
intrând în vocabularul tehnic al specialiștilor sub forma originară.) În mod
deliberat, autorul a explicitat în mod redundant anumite concepte, dând deseori
formulări echivalente alternative, utilizate în literatura de specialitate, în virtutea
anticului principiu pedagogic care afirmă că repetitio (est) mater studiorum.
Pentru cititorul care urmărește strict însușirea unor aspecte pur formative ale
arhitecturii microprocesoarelor de uz general, fără a fi deci preocupat momentan
de probleme mai avansate, recomandăm următorul traseu de parcurgere a cărții:

• Capitolul 1- integral (O introducere în filosofia microsistemelor de calcul)


• Capitolul 2 – integral (Memorii cache și memoria virtuală)
• Capitolul 3 (Microprocesoare pipeline scalare de tip RISC), fără sub-
paragrafele intitulate “Problema salturilor condiţionate impredictibile” și
„Fundamentele predicției neuronale a branch-urilor”
• Capitolul 4: Procesoare cu execuții multiple ale instrucțiunilor
(Instruction Level Parallelism) – Paragrafele 4.1, 4.2, 4.3, 4.4. În plus, din
acest capitol:
o Microprocesoare multi-thread (Par. 4.5b)
o Microprocesoare cu scheduling static: Par. 4.6 (optimizare locală –
metoda List Scheduling), Par. 4.7 (optimizare globală – Trace
Scheduling), Par. 4.8 (optimizarea buclelor de program – Loop
Unrolling și Software Pipelinining), Par. 4.9 (Microarhitecturi TTA)
și Par. 4.11 (Arhitecturi cu paralelism la nivelul datelor – SIMD sau
vectoriale)
o Sisteme multiprocesor – Par. 4.12, fără sub-paragraful intitulat
„Direcții de dezvoltare actuale în sistemele multicore /manycore”

Ne exprimăm așadar speranța că această lucrare, sub forma unui tratat


universitar unitar, se va dovedi utilă studenților din domeniul științei / ingineriei

11
calculatoarelor și tehnologiei informației sau din domenii conexe (electronică și
telecomunicații, ingineria sistemelor, inginerie electrică etc.), dar și specialiștilor
care doresc să-și consolideze cunoștințele referitoare la bazele arhitecturale ale
microprocesoarelor și sistemelor de calcul moderne (și nu numai).
În finalul acestei prefețe, autorul își exprimă gratitudinea față de soția sa,
Maria Vințan, și față de fiul său, Radu Vințan, pentru sprijinul generos acordat
pe parcursul elaborării acestei cărți și nu numai.

22 iulie 2016, Sibiu


Lucian N. Vințan

12
CUPRINS

0. O SCURTĂ ISTORIE A SISTEMELOR DE CALCUL ....................................... 16

1. O INTRODUCERE ÎN FILOSOFIA MICROSISTEMELOR DE CALCUL .... 25


1.1 SCHEMA BLOC A UNUI MICROSISTEM. ROLUL
BLOCURILOR COMPONENTE, FUNCŢIONARE DE ANSAMBLU ........... 25
1.2 MODURI DE LUCRU ÎNTRE MICROPROCESOR ŞI INTERFEŢELE I/O . . 40
1.2.1 MODUL DE LUCRU PRIN INTEROGARE (“POLLING”) .................... 40
1.2.2 MODUL DE LUCRU PRIN ÎNTRERUPERI HARDWARE .................... 42
1.2.3 MODUL DE LUCRU PRIN TRANSFER DMA (DIRECT MEMORY
ACCESS) .................................................................................................... 46

2. ARHITECTURA SISTEMULUI IERARHIZAT DE MEMORIE ...................... 53


2.1 MEMORII CACHE.............................................................................................. 53
2.2 MEMORIA VIRTUALĂ .................................................................................... 89

3. PROCESOARE PIPELINE SCALARE CU SET OPTIMIZAT DE


INSTRUCŢIUNI ..................................................................................................... 99
3.1. MODELUL RISC. GENEZĂ ŞI CARACTERISTICI GENERALE ................. 99
3.2. SET DE INSTRUCŢIUNI. REGIŞTRI INTERNI LA MODELUL
ARHITECTURAL RISC .................................................................................. 101
3.2.1. DUALITATEA ARHITECTURĂ – APLICAŢIE: IMPLEMENTAREA
GESTIUNII STIVELOR DE DATE ASOCIATE FUNCŢIILOR C........ 109
3.2.2. IMPLEMENTAREA RECURSIVITĂŢII ................................................ 114
3.3. ARHITECTURA SISTEMULUI DE MEMORIE LA PROCESOARELE
RISC .................................................................................................................. 116
3.4. PROCESAREA PIPELINE ÎN CADRUL PROCESOARELOR SCALARE ..118
3.4.1. DEFINIREA CONCEPTULUI DE ARHITECTURĂ PIPELINE
SCALARĂ ................................................................................................ 119
3.4.2. PRINCIPIUL DE PROCESARE ÎNTR-UN PROCESOR PIPELINE ..... 122

13
3.4.3. STRUCTURA PRINCIPIALĂ A UNUI PROCESOR RISC ................... 125
3.4.4. PROBLEMA HAZARDURILOR ÎN PROCESOARELE RISC.............. 129
3.4.4.1. HAZARDURI STRUCTURALE (HS): PROBLEME
IMPLICATE ŞI SOLUŢII ................................................................ 130
3.4.4.2. HAZARDURI DE DATE: DEFINIRE, CLASIFICARE,
SOLUŢII DE EVITARE A EFECTELOR DEFAVORABILE ........ 135
3.4.4.3. HAZARDURI DE RAMIFICAŢIE (HR): PROBLEME
IMPLICATE ŞI SOLUŢII ................................................................ 143
3.4.5. PROBLEMA EXCEPŢIILOR ÎN PROCESOARELE RISC ................... 197
3.4.6. AMBIGUITATEA REFERINŢELOR LA MEMORIE ........................... 200
3.4.7. EXECUŢIA CONDIŢIONATĂ ŞI SPECULATIVĂ............................... 203

4. PROCESOARE CU EXECUŢII MULTIPLE ALE


INSTRUCŢIUNILOR. MULTIPROCESOARE ..................................................... 207
4.1. CONSIDERAŢII GENERALE. PROCESOARE SUPERSCALARE
ŞI VLIW (EPIC)................................................................................................ 207
4.2. MODELE DE PROCESARE ÎN ARHITECTURILE SUPERSCALARE ...... 228
4.3. ARHITECTURA LUI R. TOMASULO ........................................................... 230
4.4. O ARHITECTURĂ REPREZENTATIVĂ DE PROCESOR
SUPERSCALAR ........................................................................................................ ...239
4.5. PROBLEME SPECIFICE INSTRUCŢIUNILOR DE
RAMIFICAŢIE ÎN ARHITECTURILE CU EXECUȚII MULTIPLE............. 258
4.5.b MICROPROCESOARE MULTI-MICROTHREAD ..................................... 260
4.6. OPTIMIZAREA BASIC-BLOCK-URILOR ÎN ARHITECTURILE MEM.... 275
4.6.1. PARTIŢIONAREA UNUI PROGRAM ÎN "BASIC-BLOCK"-URI ....... 279
4.6.2. CONSTRUCŢIA GRAFULUI DEPENDENŢELOR DE DATE
ASOCIAT.................................................................................................. 280
4.6.3. CONCEPTUL CĂII CRITICE.................................................................. 283
4.6.4. ALGORITMUL "LIST SCHEDULING" (LS) ......................................... 284
4.7. PROBLEMA OPTIMIZĂRII GLOBALE ÎN CADRUL
PROCESOARELOR MEM .............................................................................. 288
4.7.1. TEHNICA "TRACE SCHEDULING" (TS) ............................................. 289
4.8. OPTIMIZAREA BUCLELOR DE PROGRAM............................................... 300
4.8.1. TEHNICA "LOOP UNROLLING" .......................................................... 301

14
4.8.2. TEHNICA "SOFTWARE PIPELINING"................................................. 304
4.9. ARHITECTURI CU TRANSPORT DECLANŞAT......................................... 307
4.10. EXTENSII ALE ARHITECTURILOR MEM PE BAZĂ DE
REUTILIZARE ȘI PREDICȚIE A INSTRUCȚIUNILOR ............................ 311
4.11. PROCESAREA VECTORIALĂ (SIMD) ....................................................... 349
4.12. SISTEME MULTIPROCESOR (MIMD) ....................................................... 363
4.13. OPTIMIZAREA MULTI-OBIECTIV A SISTEMELOR DE CALCUL ....... 472

5. PROBLEME PROPUSE SPRE REZOLVARE ................................................... 498

BIBLIOGRAFIE SELECTIVĂ .................................................................. 530


GLOSAR DE TERMENI TEHNICI UTILZAȚI .................................................... 536
COPERTA A IV-A........................................................................................................548

15
0. O SCURTĂ ISTORIE A SISTEMELOR DE CALCUL

Acest capitol are la bază părți ale unei lucrări anterioare, scrise și publicate
de autor [Vin07b], cu revizuiri și adăugiri în versiunea prezentă, în speranța că
sub această formă nouă, va fi mai ușor asimilabil de către studenții și cititorii
interesați. Așadar, prezentăm în continuare, pe baza lucrării noastre [Vin07b], o
succintă istorie a sistemelor de calcul, insistând pe geneza principalelor idei
arhitecturale. La 12 mai 1941, inginerul german Konrad Zuse face, la Berlin, o
demonstraţie de lucru a calculatorului Z3 proiectat de el. Acesta este considerat
a fi primul calculator automat, programabil în mod flexibil şi deci universal
(citea programele de pe o bandă de celuloid perforată). Totuşi, nu avea
instrucţiuni de salt condiţionat şi deci nici posibilitatea buclelor de program
controlabile. Era un calculator binar şi lucra cu numere în virgulă mobilă pe 22
de biţi. Conţinea cca. 2000 de relee electromagnetice şi avea o frecvenţă de tact
de 5-10 Hz. Fără să ştie de celebra teză de masterat a lui Claude E. Shannon de
la MIT, intitulată A Symbolic Analysis of Relay and Switching Circuits (1937),
K. Zuse mapează şi el algebra logică a lui George Boole şi aritmetica binară în
circuite logice cu relee. Calculatorul Z3 a fost distrus în bombardamentele din
Berlin, în anul 1944. O copie a calculatorului Z3, construită ulterior chiar de
către Zuse, se află la Muzeul Tehnicii din Munchen. Încă din anul 1936 Zuse a
trimis spre aprobare un patent, în care se explica ideea programului memorat.
Brevetul i-a fost respins! Realizările lui Zuse au fost făcute publice relativ
târziu, probabil datorită apartenenței sale la nazism.
În perioada 1939-1942 Dr. John Vincent Atanasoff (de origine bulgară) de la
Iowa State College, SUA, construieşte împreună cu fostul său student, pe nume
Clifford Berry, probabil primul calculator (parţial) electronic, binar, numit ABC
(Atanasoff Berry Computer). Proiectarea lui a început încă din anul 1937. Se
pare că nu a fost niciodată complet funcţional. Totuşi, a fost primul calculator
parţial electronic (comandă electromecanică, calcul electronic) care a
implementat reprezentarea binară a datelor (pe 50 de biţi, în virgulă fixă) şi care
a separat memoria de partea computaţională. Avea o memorie regenerabilă pe
bază de condensatori (o precursoare a DRAM-urilor de azi, am putea considera),
funcţiona cu un tact de frecvenţa reţelei de alimentare (60 Hz) şi efectua cca. 30
de adunări / scăderi pe secundă (în comparație cu sutele de milioane de operații
pe secundă de astăzi, pare chiar insignifiant). Exploata paralelismul datelor în
rezolvarea unor ecuaţii liniare, fapt remarcabil și vizionar. Totuşi, nu avea

16
implementată ideea programului memorat, fiind practic un calculator dedicat. În
iunie 1941 Mauchly examinează calculatorul, ulterior prilej de controverse
asupra adevăratului părinte al ideii de calculator electronic digital (numeric).
Decembrie 1943 (prototipul) / februarie 1944 (funcţionare), inginerul britanic
Tommy Flowers şi grupul său de cercetare proiectează şi construiesc
calculatorul electronic binar (dedicat) numit Colossus, utilizat la decriptarea de
către britanici a mesajelor germanilor, pe timpul celui de-al doilea război
mondial. Era implementat cu tuburi electronice. Era parţial reprogramabil, prin
modificări ale comutatoarelor hardware. A fost primul calculator care a
implementat în hardware registre de deplasare şi reţele sistolice utilizate pentru
decodări simultane, ceea ce arată din nou un vizionarism fertil.
1944, Harvard Mark I (iniţial s-a numit Automatic Sequence Controlled
Calculator – abreviat ASCC) a fost primul calculator numeric automat din SUA,
realizat cu relee şi comutatoare (contactoare). Citea programele de pe bandă
perforată, iar datele le stoca în registre (de aici termenul de memorie Harvard,
adică având spaţii de stocare distincte pentru instrucţiuni şi date). A fost
proiectat de către fizicianul Dr. Howard Hathaway Aiken de la Universitatea
Harvard, cu finanţare IBM. (A fost construit efectiv la IBM şi trimis la
Universitatea Harvard, în februarie 1944.) Nu avea salturi condiţionate, ceea ce
implica programe lungi, fără bucle de program. Lucra cu numere zecimale pe 23
de digiţi şi realiza doar 3 adunări/scăderi pe secundă.
1943 - 1946, Fizicianul Dr. John Adam Presper Eckert și inginerul John
Mauchly (Universitatea din Pennsylvania, Moore School) au iniţiat proiectarea
și construcţia primului calculator electronic pe scară largă, de uz general,
complet operaţional, numit ENIAC (Electronic Numerical Integrator and
Calculator), finanţat de armata SUA, utilizat la calculul tabelelor balistice de
artilerie pe timpul celui de-al 2-lea război mondial, proiectarea bombei cu
hidrogen etc. (scopuri războinice deci...) La proiect au mai participat şi alţi
ingineri străluciţi. Avea cca. 18000 de tuburi electronice și 20 de registre
interne, pe 10 digiţi. O adunare dura 200 µs ( 5000 de adunări/s, faţă de câteva
sute de milioane azi, pe calculatoare personale!), avea instrucţiuni de salt
condiţionat, consuma 150 KW! Când funcționa, deseori cădea rețeaua electrică a
unui orășel din apropiere. Avea unităţi speciale pentru adunare, înmulţire,
împărţire, extragere de radical etc. Era parţial programabil, prin switch-uri
manuale, datele se citeau de pe cartele perforate IBM, iar rezultatele se tipăreau.
Performanţe: 5000 de adunări pe secundă, 385 înmulţiri pe secundă etc.
Construcţia sa a fost iniţiată prin proiectul secret Project PX între armata SUA şi

17
Universitatea Pennsylvania (5 iunie 1943). Primele procesări s-au realizat încă
din 1944.
30 iunie, 1945, se publică celebrul raport științific al lui John von Neumann
intitulat First Draft of a Report on the EDVAC, (EDVAC - Electronic Discrete
Variable Automatic Computer), Moore School of Electrical Engineering,
Contract W-670-ORD-4926 between the and the United States Army Ordnance
Department and the University of Pennsylvania; conţinea 43 de pagini. John von
Neumann - strălucit matematician - este atras încă din 1944 la proiectul ENIAC.
Este prima lucrare care defineşte procesarea programelor într-un calculator
electronic digital (binary digit = bit). Printre altele, lucrarea conţine: structura
unui calculator de uz general (all purpose) cu programe memorate (idee nouă),
pe baza a 5 module interconectate (Computer Arithmetic, Central Control,
Memory – stochează programe şi date, Input şi Output pentru operațiile de
intrare-ieșire), proiectate astfel încât să proceseze sincron, în special probleme
matematice de interes practic (sisteme neliniare de ecuaţii diferenţiale, sortări,
probleme statistice etc.), elaborarea aritmeticii binare şi a circuitelor aferente (cu
exploatarea paralelismelor aritmetice!), transferul şi procesarea informaţiilor,
modelate detaliat pe baza analogiei cu neuronii artificiali ai lui W. J.
MacCulloch şi W. Pitts (E-elements; intrări excitatoare - 1, inhibatoare - 0 şi
ieşire), definirea tipurilor de instrucţiuni maşină (orders) şi a acţiunilor acestora
asupra modulelor componente. Von Neumann, Mauchly, Eckert şi Herman
Goldstine au adus contribuţii esenţiale în conceperea acestui memoriu
(calculator electronic cu programe memorate), care a fertilizat domeniul
calculatoarelor pe scară largă. EDVAC devine operaţional abia în 1952.
1946, Profesorul Maurice V. Wilkes (Univ. Cambridge) participă la Moore
School în SUA la nişte cursuri despre calculatoare electronice numerice. Într-o
noapte citeşte raportul lui von Neumann asupra lui EDVAC (nu existau facilităţi
de fotocopiere şi a trebuit să-l returneze a 2-a zi.) Reîntors la Cambridge,
construieşte calculatorul numit EDSAC (Electronic Delay Storage Automatic
Computer), definitivat în mai 1949 – primul calculator electronic complet
echipat, operaţional, cu programe memorate (un mic prototip, Manchester Small
Scale Experimental Machine, s-a realizat totuşi, anterior, la Universitatea din
Manchester în 1948, sub conducerea prof. F. Williams). Alte inovaţii ale lui
Wilkes, un pionier al calculatoarelor: microprogramarea, ca tehnică de
proiectare a unităţii de comandă (primul articol publicat în anul 1951, a fost
aplicată de IBM abia la începutul anilor 60), bibliotecile de programe şi
macroinstrucţiunile, memoriile cache, numite inițial slave memories (primul

18
articol în 1965), sistemele de operare cu time-sharing şi acces controlat la
resurse etc. Wilkes a obţinut Premiul Turing acordat de organizațiile
profesionale internaționale IEEE & ACM, cel mai prestigios în ştiinţa
calculatoarelor. La maturitate, profesorul Wilkes afirma: "I can remember the
exact instant when I realized that a large part of my life from then on was going
to be spent in finding mistakes in my own programs."
1946, Arthur Burks, H. Goldstine, J. V. Neumann publică lucrarea intitulată
Preliminary discussions of the logical design of an electronic computer, Institute
for Advanced Study (IAS), Princeton University. Raportul acesta a fost unul
extraordinar; din acest document derivă marea majoritate a conceptelor
moderne de arhitectură a calculatoarelor. Raportul conduce la construirea
calculatorului IAS de către Julian Bigelow (coordonator von Neumann), la
Universitatea Princeton. Avea 1024x40 biţi de memorie şi era de 10 ori mai
rapid decât ENIAC! Calculatorul IAS a stat la baza primului calculator
comercial al IBM, celebrul IBM-701 (1952).
August 1949, Eckert-Mauchly Computer Corporation produce primul
calculator electronic comercial numit BINAC, pentru o companie (Remington
Rand, care îl cumpără în 1950). În iunie 1951, apare UNIVAC I primul
calculator electronic comercial de mare succes comercial, derivat din BINAC.
Costa 250,000$ şi s-au construit 48 de asemenea sisteme!
1957, ing. Victor Toma (n. 1922, viitor membru de onoare al Academiei
Române) creează primul calculator electronic digital din România (numit CIFA-
1, cca. 1500 de tuburi electronice şi cilindru magnetic de memorie, realizat la
Institutul de Fizică al Academiei, Măgurele, lângă București). România este a 8
- a ţara din lume care construieşte un asemenea calculator si a 2-a dintre fostele
ţări socialiste, după fosta URSS. Au urmat: CIFA-2 cu 800 de tuburi
electronice(1959), CIFA-3 pentru Centrul de calcul al Universităţii din
Bucureşti(1961), CIFA-4 (1962).
1959-1961, Matematicianul Iosif Kaufmann şi ing. Wilhelm Lowenfeld (+
ian. 2004), construiesc MECIPT-1 (Mașina electronică de calcul a Institutului
Politehnic din Timișoara), primul calculator numeric electronic conceput şi
realizat într-o universitate românească (Institutul Politehnic “Traian Vuia” din
Timişoara). Avea peste 2000 de tuburi electronice, 20000 de rezistori și
condensatori, peste 30 km de conductori, registre pe 31 biţi, memorie rezidentă
pe tambur magnetic – 1024 x 31 biţi (unica), prelucra 50 de operaţii pe secundă,
programare în cod maşină. Codul instrucţiunii era pe 5 biţi (32 instrucţiuni) iar
adresa de memorie era pe 10 biţi. Consum: 10 KW. Era microprogramat (sub

19
influenţa lucrărilor lui M. V. Wilkes). Scăderea, înmulţirea, împărţirea erau și
ele microprogramate. În această perioadă, Prof. A. Geier susținea deja cursuri de
programare (pentru cadre didactice), inclusiv pe MECIPT-1.
Aplicaţii practice realizate pe calculatorul MECIPT-1:

• Proiectare cupolă pavilion expoziţional Bucureşti, actual Romexpo (acad.


D. Mateescu, programator ing. V. Baltac)
• Proiectare baraj Vidraru (18 zile, în loc de 9 luni manual)
• Simulare hidrocentrală, dimensionare reţea apă Arad, calcule rezistenţă,
controlul statistic al calităţii producţiei etc.
• 1964, program de simulare a unor reţele neuronale artificiale (D. Farcaş)
• 1965-1967, simulator de automate auto-instribile (D. Farcaş, sub influenţa
prof. Kuseliov de la Moscova)
• Translator Algol 60 – limbaj maşină (1966)
• Practică studenţi (inclusiv unii din Bucureşti, Cluj, Iaşi)

1961, IBM 7030 primul calculator cu procesare pipeline a instrucţiunilor


(4 niveluri). Alte inovaţii arhitecturale: multiprogramare, protecţia memoriei,
întreruperi, aducere anticipată a instrucţiunilor (pre-fetch), memorii cu acces
întreţesut (pentru procesări vectoriale ale datelor). A fost cel mai rapid calculator
din lume între anii 1961-1964.
1962-1963, prof. univ. ing. Al. Rogojan (1914-1984) de la Institutul
Politehnic din Timişoara susţine un curs general despre calculatoare electronice
digitale. Din 1963, Kaufmann, Lowenfeld, V. Baltac, D. Farcaş susţin cursuri
despre MECIPT. În 1964 se înfiinţează la I. P. Timişoara o specializare de
calculatoare în cadrul secţiei de electromecanică, anul 4.
1963, apare calculatorul DACICC-1, dezvoltat la Institutul de calcul din
Cluj încă din anul 1959 (coordonator matematicianul Acad. Tiberiu Popoviciu,
specialist în analiza numerică; în 1959 s-a construit tot aici un calculator cu
relee, numit MARICA). Folosea atât tuburi cât şi tranzistoare. Memoriile erau
din ferite. Printre inginerii care l-au construit se numără: Bruno Azzola, Mircea
Bocu, Iolanda Juhasz, Gheorghe Farkas și Manfred Rosmann. Printre
programatorii în cod mașină ai acestuia se numără dr. Emil Munteanu, dr.
Teodor Rus (viitor specialist în compilatoare), Liviu Negrescu. În 1968 s-a
lansat DACICC 200, complet tranzistorizat.
1964, CET-500 (Calculatorul Electronic Tranzistorizat proiectat de către
ing. V. Toma, primul complet tranzistorizat din ţară). În 1960, în SUA,

20
companiile IBM şi CDC lansează producţia de serie a primelor calculatoare
tranzistorizate. A urmat CET-501 cu performanţe superioare în privinţa vitezei,
a capacităţii memoriei operative, a setului de instrucţiuni şi a echipamentelor
periferice folosite (1966). Semnificativă pentru folosirea acestor calculatoare
este şi lucrarea intitulată "Colecţie de programe pentru calculatorul CET-500”,
Editura Academiei Române (1967), prefaţată de către matematicianul Acad.
Miron Nicolescu, Preşedintele Academiei Române la acea vreme. Lucrarea, de
850 de pagini, a fost elaborată de către 41 de autori şi prezintă probleme
rezolvate efectiv, de mare utilitate practică, din 15 domenii tehnico-ştiinţifice.
1964, se lansează supercomputerul CDC 6600 (pipeline, unităţi multiple
de execuţie, vectorial). Printre proiectanţi Thornton şi Seymour Cray.
1966, apare calculatorul IBM 360/91, primul calculator cu procesări
multiple şi out of order ale instrucţiunilor (în ciuda inovațiilor arhitecturale
semnificative, nu a reprezentat un succes comercial!). Deşi nu avea
implementată predicţia dinamică a branch-urilor (instrucțiuni de salt
condiționat), şi deci posibilitatea de execuţii speculative ale instrucțiunilor,
arhitectura sa era asemănătoare cu cea a microprocesoarelor Pentium III, IV.
Printre proiectanţi, Michael Flynn (contribuții în arhitecturi paralele, aritmetică
binară) şi Robert Tomasulo, celebru pentru algoritmul de procesare a
instrucţiunilor care-i poartă numele și care va fi prezentat în această lucrare.
Ambii cercetători sunt laureaţi ai prestigiosului premiu Eckert Mauchly, acordat
pentru excelenţă în arhitectura calculatoarelor de organizațiile profesionale
internaționale IEEE & ACM (se are în vedere în special impactul operei).
1965, MECIPT-2 (tranzistorizat, memorie pe inele de ferită, cuvinte
instrucţiune pe 40 de biţi). MECIPT-3 n-a mai fost realizat.
1966, se înfiinţează, tot la I. P. Timişoara, prima specializare de
calculatoare electronice în cadrul Facultăţii de electrotehnică (prestigiul
MECIPT a fost esenţial în acest demers). Prof. Alexandru Rogojan obţine
dreptul de conducere de doctorate în calculatoare (deși, la acea vreme, el nu
deținea titlul de doctor!). Ing. V. Baltac obţine o specializare de 10 luni la
Universitatea din Cambridge (în cadrul celebrului Mathematical Laboratory din
Cambridge, coordonat de către Prof. Maurice V. Wilkes; aici se lucra pe
supercalculatorul ATLAS, considerat primul din lume din această clasă. Era
tranzistorizat şi a devenit complet operaţional în 1962. Implementa memorie
virtuală prin paginare şi procesare pipeline a instrucţiunilor. Avea un sistem de
operare cu time sharing, dezvoltat la Cambridge.)

21
1971, are loc realizarea primului microprocesor comercial (peste 2000 de
tranzistori per chip), care a facilitat intrarea într-o nouă eră a procesării
informaţiei (Intel Co, inventatorul fiind Dr. Marcian Edward „Ted” Hoff Jr., 4
biţi).
1972, Prof. univ. Alexandru Rogojan finalizează calculatorul
tranzistorizat CETA, utilizat şi în procesul didactic. Prof. Rogojan publică la IP
Timișoara un curs litografiat de “calculatoare numerice” (3 volume) în care
elaborează o metodă simplă şi sistematică de proiectare a unităţii de comandă
cablate, cu specificarea comenzilor pe faze (cicli) şi impulsuri de “orologiu”
(numărul tactului din respectiva fază), funcţie de starea anumitor semnale de
condiţie. (Acest curs a influențat pozitiv, în mod semnificativ, predarea
disciplinelor legate de proiectarea calculatoarelor numerice în universitățile
tehnice din România. Autorul acestei lucrări a beneficiat de acest curs, susținut
de profesorul univ. dr. ing. Vasile Pop de la Politehnica timișoreană, în anul
1985.)
1976 - se lansează supercomputerul Cray 1, de departe cel mai performant
din lume la acea vreme. Seymour Roger Cray (Septembrie 28, 1925 –
Octombrie 5, 1996) a fost un mare pionier al supercomputerelor, autor al unor
inovaţii arhitecturale de notorietate.
1980, primul calculator de tip RISC (Reduced Instruction Set Computer)
numit IBM 801, gândit de către Dr. John Cocke. Ideea este dezvoltată ulterior de
grupul profesorului David Patterson de la Universitatea Berkeley, care
construieşte, alături de studenții săi masteranzi, primul microprocesor RISC în
anul 1981 (lui Patterson i se datorează, ulterior, şi structurile de memorare de tip
RAID – Redundant Arrays of Inexpensive Disks). Urmează microprocesorul
RISC Berkeley II, avându-l ca șef de proiect pe dr. Manolis Katevenis (pe care
autorul acestei cărți a avut plăcerea să-l cunoască personal). La începutul anilor
1980, Dr. John Cocke de la IBM lansează ideea execuţiilor paralele ale
instrucţiunilor (Instruction Level Parallelism) şi termenul de procesor
superscalar. S-a implementat o maşină superscalară cu două instrucţiuni
procesate în paralel (Cheetah) şi respectiv cu 4 (America). Apoi IBM lansează
sistemul IBM RS/6000, un mare succes comercial şi tehnic. Cocke a obţinut
premiul Turing, cel mai prestigios posibil în ştiinţa şi ingineria calculatoarelor.
A adus contribuţii fertile şi în dezvoltarea tehnicilor de compilare optimizată a
programelor, în vederea minimizării timpului de execuţie. În legătură cu
procesorul superscalar America, într-o ediție a celebrei cărți de arhitectura

22
calculatoarelor a lui J. Hennessy şi D. Patterson, există următorul motto: “Who’s
first? America. Who’s second? Sir, there is no second!”
1975-1990: Apar primele calculatoare bazate pe microprocesoare în
România. La Cluj și la București se fabrică, la începutul anilor 80, sistemele
personale PRAE (în jurul unui microprocesor Z-80, proiectanți ing. Miklos
Patrubany și colaboratorii) și aMIC (Z80, prof. Adrian Petrescu și
colaboratorii). La Universitatea Politehnica din București se proiectează,
începînd din anul 1975, calculatoarele din familia Felix M18, M118
(microprocesor Intel 8080, pe 8 biți) și sistem de operare CP/M dar și
calculatoarele personale din familia HC-85 (microprocesor Z-80), compatibile
Spectrum Sinclair (proiectanți profesorii universitari Adrian Petrescu, Nicolae
Țăpuș, Trandafir Moisa și alții). Ulterior, aici se proiectează și calculatoare
compatbile IBM-PC (Felix PC). Microsisteme personale compatibile Spectrum
s-au mai realizat și la Universitatea “Politehnica” din Timișoara (Tim-S –
profesorul univ. dr. ing. Crișan Strugaru, Tim-N – ing. Ioan Corneliu Moș;
programele se încărcau de pe casetele magnetice ale unor casetofoane) sau la
ITC Brașov (microsistemul Cobra – acronim de la COmputer BRAșov, avea și
floppy-disk, coordonator profesor univ. dr. ing. Gheorghe Toacșe).
1975-1990: Se proiectează și se fabrică în România, la ITC București și la
Fabrica de calculatoare din București, minisisteme de calcul compatibile DEC
PDP11/44 pe 16 biți (familiile Independent – ITC și Coral – Fabrica de
calculatoare) respectiv VAX 11 (pe 32 de biți). Acestea au fost și exportate în
anumite țări socialiste din acea vreme (Cehoslovacia, RD Germană, China etc.)
1995, supercomputerul Cray 4: 64 de procesoare vectoriale de tip vector-
registru (Single Instruction Multiple Data) operând la un tact de frecvență 1
GHz. Avea câţiva GB de memorie internă. Realizat în tehnologie GaAs
(arsenură de galiu). Avea performanţe de 32 GFlops. Întrebat ce instrumente
CAD a folosit la proiectarea sistemului, Cray a răspuns că a folosit trei creioane
şi nişte topuri de hârtie. Când i s-a spus că cei de la Apple au achiziţionat un
sistem Cray pentru proiectarea viitorului calculator personal Apple Macintosh,
Seymour Cray a replicat că el şi-a cumpărat de curând un Macintosh ca să
proiecteze viitorul... super-computer Cray. Influenţa supercomputerelor Cray
apare chiar şi în cazul mai recentului IBM BlueGene (2005) care conţine 65536
noduri de procesare conectate într-o topologie de tip reţea 3 D toroidală. Fiecare
nod este interconectat la alte 6 noduri situate simetric pe direcția sistemului
ortogonal XYZ, centrat în respectivul nod. Un nod conţine 80 de procesoare de
tip multithread (cu fire multiple de execuție) cu memorii DRAM implementate

23
on chip (conceptul arhitectural PIM – Processing In Memory). Aceste 80 de
procesoare sunt interconectate printr-o reţea matricială de tip crossbar.
Supercomputerul conţinea, în plus, 1024 de noduri de intrare-ieşire şi atingea
performanţe de cca. 207 Tflops pe programe numerice! Ulterior, bariera de 1
Pentaflop a fost atinsă.La cele anterior enumerate succint, aş trage o singură
concluzie, situată, poate, în uşor dezacord cu opinia majoritară de azi: ideile
arhitecturale cele mai importante în calculatoare, sunt, totuşi, relativ vechi. În
esență, inovațiile arhitecturale în domeniul sistemelor de calcul au fost oarecum
incrementale, astăzi noi fiind încă tributari principiilor de procesare a
instrucțiunilor și datelor formulate de von Neumann, acum mai bine de 70 de
ani...

24
1. O INTRODUCERE ÎN FILOSOFIA MICROSISTEMELOR DE
CALCUL

1.1. SCHEMA BLOC A UNUI MICROSISTEM. ROLUL BLOCURILOR


COMPONENTE, FUNCŢIONARE DE ANSAMBLU.

Acest capitol are la bază părți ale unei lucrări anterioare, scrise și publicate
de autor [Vin03], inclusiv online, cu revizuiri și adăugiri semnificative în cadrul
acestei versiuni. Microprocesoarele şi, mai general, microarhitecturile de
prelucrare a informaţiei (procesoare, microcontrolere sau chiar calculatoare sau
multiprocesoare integrate pe un cip) au declanşat o adevărată revoluţie în
industria calculatoarelor, atât prin performanţele deosebite, cât şi prin costurile
tot mai diminuate, la aceeaşi putere de calcul. În multe aplicaţii, un
microprocesor de vârf al zilelor noastre depăşeşte performanţele unui
supercalculator de acum 20 ani, la un preţ de câteva zeci sau chiar sute de ori
mai mic. În perioada 1980-2004, performanţa relativă a microprocesoarelor a
crescut cu cca. 60% pe an. Cercetătorii susţin că cca. 65% din această creştere s-
a datorat îmbunătăţirilor arhitecturale şi doar cca. 35% celor de natură
tehnologică.
Azi, la peste 30 de ani de la inventarea calculatorului personal (Apple-
MacIntosh, utilizând un microprocesor pe 8 biți Motoroala 6800), din punct de
vedere al pieţei, apar trei tipuri distincte de calculatoare:

Calculatoarele personale (desktop-uri, laptop-uri) sunt destinate


utilizatorilor celor mai obişnuiţi, de toate categoriile profesionale. Costă de la
câteva sute de $ la cca. 10.000 $ în configuraţii hardware-software mai
sofisticate, de tip staţii de lucru (work-stations). Piaţa de PC-uri (Personal
Computers) impune găsirea unor compromisuri optimale performanţă – cost.
Se mai caracterizează printr-o performanţă deosebită a graficii, precum şi a
capabilităţilor de conectare la Internet. La nivelul anului 2000 s-au produs
cca. 150 milioane de desktop-uri. Compania Intel este un lider de piață în
domeniul microprocesoarelor care echipează aceste calculatoare.

25
Serverele sunt destinate să ofere servicii tot mai sofisticate de reţea, inclusiv
ca noduri de Internet, în locul mainframe-urilor de acum două decenii şi mai
bine. Caracteristicile lor cele mai importante se focalizează pe fiabilitate
(lucrează 24 de ore din 24), disponibilitate, scalabilitate şi viteză de procesare.
Aceste servere costă actualmente între 10.000$ şi 10.000.000$
(supercalculatoare) şi absorb cca. 4 milioane de microprocesoare pe an (la
nivelul anului 2000).
Sistemele dedicate (Embedded Systems) au dezvoltarea cea mai dinamică,
estimările arătînd că la nivelul anului 2000 s-au produs cca. 300 milioane de
astfel de sisteme pe an, iar tendința este una semnificativ ascendentă. Ele
acoperă aplicaţiile cele mai frecvente (aparate foto şi camere video, comandă
aparate electrocasnice, telefoane mobile inteligente, notebook-uri, iPAD-uri,
imprimante, comenzi auto, jocuri electronice, switch-uri pentru reţele etc.) şi
au costuri cuprinse între 10$ şi 100.00$. Multe dintre aceste sisteme au
softurile scrise de producător, având un grad de programabilitate relativ redus.
Performanţa se focalizează pe îndeplinirea cerinţelor de timp real ale
aplicaţiei. Sunt caracterizate, în principal, de consumuri reduse de putere
(deseori sunt alimentate prin baterii şi acumulatori) şi memorii de capacităţi
relativ reduse [Fis05]. Peste ani, aceste sisteme vor fi integrate practic în toate
dispozitivele folosite de om şi interconectate prin reţeaua globală de tip
Internet, conducând la conceptul de calculator omniprezent, mobil sau
incorporat și senzitiv la context, care înțelege acest context și acționează pe
această bază în mod proactiv (“ubiquitous computing”) [Vin07]. Astfel,
calculatorul miniaturizat va migra de la explicitul remarcabil de azi, la
implicitul tot mai banal de mâine, prin caracteristici precum conectivitate
(internet), mobilitate, funcții dedicate și senzitivitate la context. Compania
ARM este un lider de piață în domeniul microprocesoarelor low-power care
echipează aceste sisteme dedicate. Compania ARM doar le proiectează,
fabricarea efectivă se face în alte companii.

O caracterizare globală a actualelor şi viitoarelor microarhitecturi de


procesare a informaţiei necesită înţelegerea tendinţelor tehnologice şi respectiv a
celor arhitecturale. În ultima perioadă de timp, prin arhitectura unui
microprocesor se înţelege nu numai setul de instrucţiuni şi modurile de adresare
(ISA – Instruction Set Arhitecture), ci şi structura organizatorică a procesorului
şi respectiv implementarea hardware a acestuia, toate aflate intr-o ierarhizare
strictă [Hay98, Pat98, Pat04, Hen11].

26
Între cele mai importante tendinţe tehnologice amintim:
În cazul microprocesoarelor, gradul de integrare al tranzistorilor pe cip
creşte cu cca. 55% pe an, în acord calitativ cu legea lui Moore, care va mai
funcționa, pare-se, până prin anul 2025. Tehnologia de integrare a
microprocesoarelor a evoluat de la 10 microni (1971) la 0,18 microni (2001).
Frecvenţa ceasului a crescut si ea cu cca. 50% pe an, până în anul 2004 (după
aceea, datorită puterii dinamice prea mari, implicînd disipații termice tot mai
semnificative, s-a renunțat la creșterea frecvenței de tact în favoarea
implementării mai multor procesoare pe același cip – multicore).
În cazul memoriilor DRAM, densitatea de integrare creşte cu 40-60 % pe
an (tot în progresie geometrică), în timp ce timpul de acces aferent scade cu
33% pe decadă (descurajant).
Densitatea informaţiei scrise pe hard-disc-uri creşte cu cca. 100% pe an,
în timp ce timpul de acces aferent scade cu cca. 33% pe decadă (iarăşi
descurajant).
Tehnologia şi performanţele reţelelor se îmbunătăţesc semnificativ
(Ethernet la 1 Gb, switch-uri ultrarapide, cablare pe fibră optică etc.)
În paralel cu tendinţele mai sus menţionate, costurile scad simţitor în timp
(practic la aceeaşi putere de calcul ori capacitate de memorare).
Între cele mai importante tendinţe arhitecturale, unele analizate în
continuarea acestui curs universitar, amintim succint:
Exploatarea paralelismului la nivelul instrucţiunilor şi firelor de execuţie,
atât prin tehnici statice (software), cât şi dinamice (hardware, run-time) sau
chiar hibride (spre exemplu, cazul arhitecturii IA-64, procesorul Intel
Itanium)
Structuri tot mai performante de ierarhizare a sistemului de memorie, prin
utilizarea unor arhitecturi evoluate de memorie de tip cache, pe niveluri
multiple
Reducerea latenţei căii critice de program, inclusiv prin tehnici
speculative de reutilizare dinamică a instrucţiunilor şi predicţie a valorilor
instrucţiunilor [Vin02, Vin07].
Utilizarea multiprocesoarelor (shared memory / distributed memory), în
special în cadrul arhitecturilor serverelor şi staţiilor grafice, dar nu numai. De
asemenea, se constată o dezvoltare a sistemelor distribuite de procesare a
informaţiei (message passing), inclusiv prin implementarea unor rețele de
calculatoare pe același cip (Network on Chip).

27
Figura 1.1. Schema bloc a unui microsistem (Microprocesor,
amplificatoare de magistrale, magistrale de adrese, date comenzi
şi stări, module memorie ROM şi RAM, porturi I/O lente, porturi
I/O rapide – interfeţe DMA, program încărcător - POST,
programe BIOS)

Microprocesorul este elementul central al structurii, responsabil cu


aducerea din memorie, decodificarea şi execuţia instrucţiunilor maşină,
codificate binar și memorate sub această formă în memoria principală (ROM,
RAM). În conformitate cu specificațiile acestor instrucţiuni, microprocesorul
generează secvenţiat în timp (sincron cu semnalul de tact) toate semnalele
(adrese, date, comenzi) necesare memoriilor şi interfeţelor pe care le
gestionează. Conţine regiştri interni (de date, de adrese, de stări, de instrucțiuni
etc.), unităţi de execuţie, o unitate de comandă, cablată sau microprogramată,
bus-uri (magistrale) interne de interconectare etc. În general este integrat pe un
singur circuit sau chiar în mai multe circuite integrate, împachetate într-o
singură capsulă. În sfera comercială, primul microprocesor, pe doar 4 biţi, s-a
realizat în anul 1971 la compania Intel şi a fost proiectat de către inginerul Tedd
Hoff. S-a numit Intel 4004 și nu a avut un succes comercial deosebit. (În
domeniul militar existau, pare-se, mai demult, asemenea sisteme integrate
complexe. Spre exemplu, în comanda avioanelor militare americane F14A a
existat un microsistem pe 20 de biţi, cu procesare pipeline a instrucţiunilor – v.

28
http://www.microcomputerhistory.com/. Încă o dovadă că tehnologia militară
este situată mult înaintea celei comerciale).
Bus-ul (magistrala) de adrese este practic unidirecţional (se va vedea că și
DMA-ul îl poate controla sau alte procesoare în cazul sistemelor multiprocesor),
de tip tree state (TS, înaltă impedanță – pentru a putea fi controlat de mai multe
surse, gen DMA, procesoare). Prin intermediul acestui bus microprocesorul
pune adresa de acces la memorie sau la porturile (interfețele) de I/O
(Input/Output). Lumea externă a microprocesorului este constituită exclusiv din
memorie şi interfeţele de intrare – ieşire. Acestea sunt resursele care pot fi
accesate (scrise respectiv citite) de către microprocesor. Aşadar, acesta nu
“vede” în mod direct perifericele, ci doar indirect, prin intermediul interfeţelor
de I/O.
Bus-ul de date este de tip bidirecţional, TS. Prin intermediul acestui bus
microprocesorul aduce din memorie instrucţiunea, respectiv citeşte data
(operandul) din memorie sau dintr-un port de intrare (arhitectura Princeton de
memorie) – inclusiv vectorul de întrerupere.
La scriere, microprocesorul plasează pe bus-ul de date rezultatul pe care
doreşte să-l scrie în memorie sau într-un port de ieşire. La citire, rezultatul este
preluat prin intermediul acestui bus din memorie sau dintr-un port de intrare. În
ambele cazuri, microprocesorul activează sincron cu semnalul de tact adresa
respectivă pe bus-ul de adrese, împreună cu semnalele de comandă aferente
(Read / Write, Memorie / Interfaţă etc.), pe bus-ul de comenzi. Pe bus-ul de
stări, dispozitivele slave (memorii, interfeţe) comunică informaţii referitoare la
modul de desfăşurare al transferului (Ex. semnalul “aşteaptă” - Busy, emis spre
microprocesor de către dispozitivele de comandă ale memoriei sau interfețelor
de I / O, cu semnificaţia că transferul de date comandat nu este încă încheiat).
Memoria poate fi văzută din punct de vedere logic, într-o primă abordare,
ca o stivă de locaţii binare (cuvinte, în general octeți), fiecare cuvânt fiind
caracterizat de o adresă binară unică.

29
Figura 1.2. Schemă generică de memorie

În general M=8,16,32,64 semnificând lărgimea busului date al


microprocesorului (microsistem pe M biți). Memoria este caracterizată prin doi
parametri de bază:
- capacitatea (numărul de locaţii pe care le conţine)
- latenţa (timpul de acces), care este o caracteristică intrinsecă a circuitului
de memorie și reprezintă, în principiu, timpul scurs din momentul furnizării
adresei de către microprocesor până în momentul în care memoria a încheiat
operaţia comandată (citire sau scriere). Fireste, se doresc capacităţi cât mai mari
și latențe cât mai mici ale circuitelor de memorie, cerinţe în general
contradictorii.

30
Figura 1.3. Un ciclu (fază) extern generic de citire din memorie a
microprocesorului
Figura 1.3 prezintă un ciclu generic de citire din memorie a
microprocesorului. În tactul T1 (pe frontul căzător, în acest exemplu) acesta
pune adresa de memorie pe busul de adrese (conținutul PC – ului, dacă este un
ciclu de fetch instrucțiune, respectiv adresa de memorie – calculată conform
modului de adresare al instrucțiunii curente – operandului sursă / rezultatului
instrucțiunii). În tactul T2 microprocesorul activează semnalul de citire din
memorie READ. Urmează o serie de tacte de așteptare (Tw), în care
microprocesorul doar interoghează starea semnalului READY, generat de
dispozitivul de comandă a memoriei. Dacă acesta este inactiv, semnifică faptul
că memoria nu este pregătită pentru transferul de date. Activarea READY=1
semnifică faptul că memoria a generat pe busul de date cuvântul, reprezentat în
binar, solicitat de microprocesor. Astfel, dupa activarea READY, în tactul T3
microprocesorul citește (strobează) într-un registru intern conținutul busului de
date. În tactul T4 microprocesorul dezactivează conținutul busului de adrese,
precum și semnalul READ. Sesizând dezactivarea semnalului de comandă
READ, memoria dezactivează la rându-i semnalul de stare READY, precum și
conținutul busului de date, care reintră în starea de înaltă impedanță (TS=three
state). După tactul T4, un nou ciclu extern poate, eventual, începe. De remarcat
faptul că protocolul de transfer este unul de tip hand-shaking, fiind deci asincron
(durează un număr variabil de tacte procesor, funcție de timpul de acces al
memoriei, respectiv funcție de perioada de tact a microprocesorului.)

31
Între busul de adrese si memoria propriu-zisă există un decodificator N:2N
ca în figură (înglobat în circuitul de memorie):

Figura 1.4. Decodificator de adrese memorie N:2N

Sel 0 = NADR0 * NADR1 * ... * NADR(2 N − 1)



.

.

Sel (2 − 2) = ADR0 * ADR1 * ... * ADR(2 − 2) * NADR(2 − 1)
N N N

Sel (2 N − 1) = ADR0 * ADR1 * ... * ADR(2 N − 1)

NADRk=ADRk negat logic


Cu 10 biţi de adrese => 210 cuvinte = 1024 cuvinte = 1K cuvinte (Kilo)
Cu 20 biţi de adrese => 220 cuvinte = 210 K cuvinte = 1M cuvinte (Mega)
Cu 30 biţi de adrese => 230 cuvinte = 210 M cuvinte = 1G cuvinte (Giga)
Cu 40 biţi de adrese => 240 cuvinte = 210 G cuvinte = 1T cuvinte (Terra)
Dacă M = 8 => 1 cuvânt = 1 octet

Dintr-un punct de vedere tehnologic, memoriile se împart în două categorii:

• ROM (Read Only Memory) – EPROM, EEPROM, FLASH etc.


• RAM (Random Acces Memory)
- SRAM (static)
- DRAM (dinamic)

32
Memoriile EPROM sunt memorii rezidente, care păstrează deci conţinutul
şi după decuplarea tensiunii de alimentare. Ele sunt reprogramabile, în sensul în
care pot fi şterse prin expunere la raze ultraviolete şi reînscrise, pe baza unui
dispozitiv special, numit programator de EPROM –uri.
EPROM-urile păstrează aşa numitul program monitor, înscris de către
fabricant, care este primul program procesat de către sistem imediat după
alimentarea (resetarea) sa. Acest lucru este absolut necesar, întrucât conţinutul
memoriilor RAM este neprecizabil imediat după alimentare. Prin urmare,
imediat după activarea semnalului asincron de RESET, conţinutul PC-ului este
iniţializat şi va pointa spre prima instrucţiune din programul monitor, rezident în
EPROM. Rolul programului monitor este de a efectua o testare sumară a
microprocesorului şi a celorlalte componente ale microsistemului, după care va
iniţia încărcarea sistemului de operare (Linux, Windows etc.) de pe hard-disc, în
memoria RAM. După aceasta, programul monitor dă controlul sistemului de
operare rezident acum în RAM. De asemenea, în IBM-PC-uri ROM-ul conţine
și rutinele de intrare – ieşire BIOS.
SRAM: sunt memorii deosebit de rapide, timp de acces de t0 = 1 ns ÷ 7 ns,
având capacitate de integrare redusă (sute de Kocteți per circuit).
DRAM: constituie actualmente peste 95 % din memoria oricărui sistem de
calcul de uz general, datorită faptului că oferă densităţi mari de integrare (64
Mbiţi – 4 Gbiţi / chip) şi timpi de acces “relativ rezonabili”, t0=30 ns ÷ 60 ns.
Totuşi, decalajul între timpul de acces ridicat al acestor memorii şi viteza mare
de execuţie a microprocesorului (cu frecvențe de tact uzuale între 1 și 4 GHz),
constituie una dintre marile probleme tehnologice şi arhitecturale în ingineria
calculatoarelor. Fiind realizate în tehnologie CMOS, puterea absorbită este
redusă, ceea ce este remarcabil. Din păcate au două mari dezavantaje:
1. Accesare (citire / scriere) complicată. Circuitele DRAM sunt
organizate sub o formă matricială, pe linii şi coloane. Bitul ce se doreşte a fi
accesat, se află la intersecţia coloanei cu linia selectată.

Un circuit DRAM are următoarele terminale (pini):

33
Figura 1.5. Circuit DRAM
RAS (Row Address Strobe – strob adresă rând), CAS (Column Address
Strobe – strob adresă coloană)

Figura 1.6. Ciclul de citire din DRAM

Pentru o citire corectă este necesar ca frontul căzător al semnalului RAS


negat să strobeze perfect adresa de rând (o memorează într-un registru intern al
circuitului), iar frontul căzător al semnalului CAS negat să strobeze perfect
adresa de coloană (când aceasta este stabilă deci). În momentul memorării
adresei de coloană (căzătorul lui CAS negat) memoria are toate elementele
necesare furnizării bitului de ieşire Dout, după un timp precizat în catalog (de
cca. 20 ns la mai vechile memorii dinamice, având codul 4164). Există și un
mod de citire în rafală (burst), mai rapid, în care se citesc biții situați succesiv,
pe un singur rând. În acest caz există o singură activare a semnalului RAS pe
durata căreia apar mai multe activări ale semnalului CAS, corespunzătoare

34
biților succesivi situați pe același rând. Evident că în aces caz particular timpul
de acces este mai redus. Acest mod de acces a memoriei DRAM se folosește
pentru accesul memoriilor video, care trebuie să furnizeze datele (octeții RGB)
la monitorul video, în mod sincron. Oricum, rezultă deci că interfaţa între
microprocesor şi DRAM este complicată, întrucât din semnalele pe care le
furnizează microprocesorul (adresă de memorie şi comandă de citire/scriere),
interfaţa trebuie “să fabrice” protocolul mai sus expus (secvenţa ADRESE
microprocesor, RAS, CAS…).

2. Necesitatea regenerării memoriei DRAM


Bitul de informaţie din DRAM este implementat printr-un tranzistor de tip
MOS, care se comportă sub forma unui condensator. Din păcate, acest
“condensator” se descarcă în timp şi, prin urmare, cu timpul poate să piardă
informaţia pe care o memorează. Rezultă deci că periodic el trebuie reîncărcat
(refresh, regenerare). Regenerarea se face pe întreg rândul din matricea de
memorare. Conform catalogului, la mai vechile memorii având codul 4116 (16
kbiți) sau 4164 (64 kbiți), un anumit rând “ţine minte” informaţia circa 2 ms.
După acest interval, întreg rândul trebuie regenerat. Algoritmul de regenerare va
trebui să fie unul de tip secvenţial, care să regenereze rând după rând, în mod
ordonat. Rezultă că rata de regenerare a două rânduri succesive i respectiv (i+1)
trebuie să se facă la un interval de maxim 2 ms/N, unde N=nr. de rânduri al
circuitului de memorie DRAM. Spre exemplu, considerând N=128 rânduri
(DRAM 4116, 4164), rezultă că rata de regenerare trebuie făcută periodic la cel
puțin 2 ms/128 ~ 15,6 µs.
Prin urmare vom avea accese la DRAM din două părţi:
din partea µprocesorului, care citeşte / scrie conform programului
pe care îl execută.
din partea unui automat de regenerare care regenerează periodic,
rând după rând, memoria DRAM.
Posibilele conflicte la memoria DRAM între microprocesor şi automatul de
regenerare vor trebui gestionate corespunzător, eventual acceptând blocaje
reciproce, care conduc la scăderea performanţei. Astfel, dacă automatul de
regenerare este activ și microprocesorul dorește să acceseze memoria, acesta va
trebui să aștepte (automatul de regenerare îl va ține în așteptare). Reciproc, dacă
microprocesorul se află pe durata unui transfer cu memoria DRAM, iar
automatul ar dori să o regenereze, acesta din urmă va trebui să aștepte încheierea
transferului inițiat de microprocesor și abia apoi să declanșeze regenerarea,

35
ținând pe această durată microprocesorul în așteptare. Ar fi utilă implementarea
unei "regenerari transparente" (care sa nu blocheze deloc microprocesorul).
Aceasta ar trebui să se “strecoare” între finalul unui ciclu de acces la memorie al
microprocesorului și începutul unuia nou. Acest deziderat necesită
compromisuri între viteza de procesare (frecvența de tact a CPU) şi gradul de
transparenţă al regenerării. Oricum, prin stările de așteptare introduse în mod
suplimentar, regenerarea determină practic un timp de acces și mai mare al
memoriei DRAM.
Uneori, în comunicația CPU-memorie există implementat și un control de
paritate. Dacă magistrala de legătură deține N biți de date (D1, D2, ...,DN), se mai
adaugă la aceștia un bit de paritate P= D1 XOR D2 XOR ...XOR DN, unde
conectorul logic XOR=SAU EXCLUSIV. Bitul P=1, dacă există un număr
impar de biți de date pe 1 logic și zero în caz contrar. Spre exemplu, la o scriere
a CPU în memorie, acesta calculeaza paritatea datei de scris și o pune pe
magistrala de legătură ca al (N+1)-lea bit (bit de paritate emisă, PE). La recepție,
controlerul de memorie calculează paritatea datei recepționate prin bitul numit
PC. Dacă PC=PE se consideră că scrierea a decurs corect; altfel nu. Evident, se
poate detecta corect doar un număr impar de biți recepționați în mod eronat. În
cazul unui număr par de biți eronați, PC=PE și astfel erorile respective nu se pot
detecta. Așadar probabilitatea de a nu detecta erorile independente la recepție, în
acest caz este:

N
Pnd = ∑C
k = par
K
N p K (1 − p) N − K (distribuție binomială),

unde p=probabilitatea ca un bit să fie recepționat la memorie în mod eronat.


Variabila k ia valori începând de la 2 (nu de la 0... justificați!) și parcurgând
toate numerele pare mai mici sau egale cu N.
În sistemele de calcul înaltă fiabilitate se poate face nu doar detecția, ci și
corecția unor erori, prin așa numitele coduri ciclice redundante (CRC – Cyclic
Redundancy Codes). Pentru cei interesați de corecția prin CRC, recomandăm
cartea lui Henry S. Warren intitulată Hacker’s Delight, 2nd Edition, Addison
Wesley, 2012.
Standardizarea bus-urilor şi a protocoalelor aferente a condus la conceptul
fertil de microsistem de dezvoltare. Într-un asemenea microsistem, prin simpla
conectare la bus-ul comun sincron standard (date, adrese, comenzi, stări) a unor
noi module compatibile (memorii, interfeţe), se permite o dezvoltare în timp a

36
microsistemului iniţial. Spre exemplu, cunoscutele microsisteme standardizate
de tip IBM-PC, constituie astfel de sisteme de dezvoltare, construite în jurul
unor magistrale standardizate. Mai nou, lumea sistemelor dedicate (embedded) a
accentuat această tendință.

Cam cât timp durează procesarea unei instrucţiuni ?

Exemplu: instrucţiunea ADD reg1,(reg2)200, codificată binar pe un singur


cuvânt, egal cu lărgimea de bandă a interfeţei procesor – memorie. Semantica
instrucțiunii este următoarea:
reg1 (reg1) + Cuv. Mem| ADR: (reg2)+200
 1
 fCLK = 100 MHz ⇔ TCLK = = 10ns
 fCLK
Timpul de acces DRAM : taDRAM = 60 ns

Această instrucţiune se procesează, conform modelului secvenţial clasic
(atribuit marelui matematician american John von Neumann – autorul primului
document referitor la un calculator electronic numeric pe deplin funcţional), în
faze succesive, după cum urmează:
Faza IF (instruction fetch): ciclu de citire din memorie a instrucțiunii
codificate binar, de la adresa dată de registrul intern PC (Program Counter),
durează aproximativ, spre exemplu, 10 TCLK (conform protocolului unui ciclu
mediu de acces la memorie) = 100 ns. Instrucțiunea se aduce într-un așa numit
registru al instrucțiunii sau într-un buffer de prefetch de tip FIFO. S-a considerat
că microprocesorul a introdus 6 tacte de așteptare în cadrul ciclului de acces la
DRAM (60ns/10ns=6 tacte; v. Fig. 1.3). În realitate, un astfel de ciclu durează
mai puțin, dacă este cu hit în memoria cache (spre ex., doar un tact de așteptare).
Faza ID (instruction decode): în această fază, microprocesorul decodifică
instrucţiunea pe baza informațiilor codificate binar în corpul acesteia (“înţelege”
ce trebuie să facă în continuare) şi, ca urmare, aduce într-un registru temporar
intern, situat la intrarea ALU, operandul din reg1. La cealaltă intrare a ALU va
aduce din registrul instrucțiunii indexul respectiv (200, în acest caz).
Decodificarea instrucţiunii consumă uzual 1TCLK = 10ns.
Apoi, urmează ciclul de calcul adresă de memorie a celui de-al doilea
operand (reg2) + 200 ADR(BUS) “atacă” memoria DRAM: ~ 1 TCLK=10 ns.
Apoi, urmează declanşarea unui ciclu de aducere a operandului din
memoria DRAM, de la adresa anterior calculată. Durează, ca şi faza IF, ~ 10
TCLK = 100 ns.

37
Faza EX: execuţia propriu-zisă a instrucţiunii, adunarea celor doi operanzi,
durează uzual ~ 1 TCLK = 10 ns => timpul total de procesare aferent acestei
instrucţiuni T~250 ns => nr. de instrucţiuni pe secundă: ~ 4.000.000
instrucţiuni / secundă (4 MIPS) (Pentium I ~ 15 MIPS). De remarcat faptul că
aceste instrucțiuni sunt dinamice, nicidecum statice. O instrucțiune dinamică
reprezintă o instanță (instrucțiune efectiv executată de procesor) a unei
instrucțiuni statice (scrisă de programator sau generată de compilator.)
De observat faptul că o instrucţiune se procesează într-un model secvențial
de către procesor, sub forma unei înlănţuiri de cicli maşină (faze). Un ciclu
maşină reprezintă o înlănţuire de acţiuni, sincronizate cu un impuls de tact, într-
un scop clar definit. Ciclul maşină reprezintă unitatea atomică de procesare, cea
care nu poate fi întreruptă de nici o cerere hardware externă.
Observație: Utilitatea modurilor de adresare indirecte prin registru şi
indexate (adresa operand din memorie = R+index) este dată de facilitatea scrierii
compacte a programelor (bucle), adresării elegante a unor structuri de date de tip
tablou, situate în memorie etc. Astfel de structuri de date se întâlnesc
implementate atât în cadrul limbajelor de nivel mediu-înalt (spre ex. în limbajul
C, stiva de date asociată unei funcţii şi care trebuie accesată în vederea
transmiterii de parametri, respectiv revenire dintr-un apel) cât şi al aplicaţiilor
scrise în aceste limbaje.

Harta de memorie a unui microsistem

Exercițiu. Să se proiecteze un sub-sistem de selecție a circuitelor de


memorie, având 16 KO de memorie ROM, începând cu adresa 0, implementați
cu cipuri de 4 KO/cip și 32 KO memorie SRAM, începând de la adresa 8000H
(hexazecimal), implementați cu cipuri de 8 KO/cip. Magistralele de date ale
circuitelor de memorie, ca și cea a microprocesorului, sunt de tip T.S. Fiecare
circuit de memorie deține un semnal de tip chip select (CS) care, atunci când
este activ (1 logic), îl validează. Dacă acest semnal este inactiv, magistrala de
date a circuitului va fi în starea de înaltă impedanță.
Rezolvare. În Figura 1.6.b este prezentată harta de memorie (memory map)
a microsistemului (64 KO). Harta de memorie este adresabilă cu 16 adrese
emise de microprocesor, notate A15 (c.m.s.) – A0 (c.m.p.s.). Spațiul de 16 KO
de memorie ROM va fi acoperit de 4 circuite de memorie ROM, având
semnalele de selecție CSROM1 – CSROM4. Spațiul de 32 Ko de memorie
SRAM va fi acoperit de alte 4 circuite de memorie SRAM, având semnalele de

38
selecție CSRAM1 – CSRAM4. Sub-sistemul de selecție a circuitelor de
memorie este prezentat în Figura 1.6.c. Fiecare dintre cele 4 circuite de memorie
ROM vor fi adresate cu adresele A11-A0 ale microprocesorului. Fiecare dintre
cele 4 circuite de memorie SRAM vor fi adresate cu adresele A12-A0.
Magistralele de date ale circuitelor de memorie fiind de tip T.S. se vor lega la
magistrala de date a microprocesorului.

Figura 1.6.1. Harta de memorie a microsistemului

39
Figura 1.6.2. Decodificatorul de memorie proiectat

1.2. MODURI DE LUCRU ÎNTRE MICROPROCESOR ŞI


INTERFEŢELE I/O

1.2.1. MODUL DE LUCRU PRIN INTEROGARE (“POLLING”)

Se bazează pe testarea de către microprocesor a unui bit de stare al


interfeței, de tip Read-only din punct de vedere al microprocesorului, asociat
dispozitivului periferic. Microprocesorul nu va iniţializa transferul cu perifericul

40
decât în momentul în care bitul de stare semnifică faptul că perifericul este
pregătit pentru transfer (nu lucrează la un transfer iniţiat anterior). Să
considerăm, spre exemplu, interfaţa cu o tastatură. Această interfaţă trebuie să
conţină minimum două registre logice (Rbuff, Rstat).

Registrul RBuff va memora un octet care reprezintă codul ASCII (Unicode)


al tastei apăsate de către utilizator.
Exemple:
“A” = 41h 0100.0001 în binar
“a” = 61h 0110.0001
“0” = 30h
“ ” = 20h

Bitul Ready din registrul de stare este un bit de tip Read Only, cu
următoarea semnificaţie: dacă registrul RBuff se încarcă cu un octet (utilizatorul
a apăsat o tastă), atunci Ready se pune automat pe “1” logic, arătând
microprocesorului că poate să preia codul din RBuff. Bitul Ready se va reseta
automat, odată cu preluarea codului din registrul de date Rbuff de către
microprocesor. Un program - absolut principial - de gestiune a tastaturii s-ar
scrie ca mai jos:

41
Dezavantajul acestei metode constă în faptul că microprocesorul aşteaptă
un timp neacceptabil de mare la nivelul vitezei sale de procesare a
instrucțiunilor, pentru a inspecta dacă perifericul este, sau nu este pregătit,
pentru transferul de date. Considerând că utilizatorul apasă o tastă la interval de
500 ms şi că o instrucţiune a microprocesorului durează cca. 250 ns (vezi
justificarea anterioară), rezultă că acesta “pierde” practic 500 ms / 250 ns = 2
milioane instrucţiuni mașină în bucla de aşteptare, în loc să execute instrucţiuni
utile în acest interval de timp. Acest dezavantaj este eliminat de metoda
următoare de comunicare procesor-interfaţă de I/O.

1.2.2. MODUL DE LUCRU PRIN ÎNTRERUPERI HARDWARE

Se bazează pe generarea unui semnal de întrerupere INT de la o interfaţă


(port) spre microprocesor, ori de câte ori aceasta doreşte un serviciu programat
de la microprocesor. Ca urmare a recepţionării semnalului INT, microprocesorul
va abandona programul principal (PP), urmând să intre într-o aşa numită rutină
tratare a întreruperii (Interrupt Service Routine) în care va satisface cererea
interfeţei (perifericului). La finele rutinei de tratare a întreruperii, printr-o
instrucţiune de tip RETURN, microprocesorul va reveni in PP, în general, dar nu
chiar întotdeauna, pe instrucţiunea imediat următoare ultimei instrucţiuni din PP
deja executate. În cazul exemplului cu tastatura anterior considerat, interfaţa va
genera întreruperea INT ori de câte ori utilizatorul a apăsat o tastă, adică
registrul RBuff este “plin”, deci conţine codul (ASCII, Unicode etc.)
caracterului tastat.

42
Figura 1.7. Modelul de lucru prin întreruperi

Aşadar, RTI după ce execută serviciul necesar perifericului (în cazul acesta
preluare caracter din interfață şi depozitare caracter în memorie) revine în PP,
unde până când un periferic va cere un nou serviciu (spre ex., se apasă din nou o
tastă), microprocesorul va executa instrucţiuni utile din PP (sistem de operare,
program utilizator etc.) și deci nu mai este necesar să mai aştepte inutil după
periferic, ca în cazul transferului prin interogare.
Totalitatea acţiunilor executate de către microprocesor, din momentul
apariţiei semnalului de întrerupere INT, până în momentul procesării primei
instrucţiuni din RTI, formează aşa numitul protocol hardware de acceptare a
întreruperii (săgeţile 1 şi 3 din figura anterioară). În principiu, acest protocol se
desfăşoară în următoarele etape succesive:
1.) Odată sesizată întreruperea INT de către microprocesor, acesta îşi va
termina instrucţiunea în curs de execuţie, după care, dacă anumite condiţii sunt
îndeplinite (nu există activată o cerere de întrerupere sau de bus – DMA – mai
prioritare etc.), va trece la pasul 2. În general, microprocesoarele examinează
activarea întreruperilor la finele (ultimul tact) ultimului ciclu, aferent
instrucţiunii în curs de execuţie.
2.) Recunoaşterea întreruperii: microprocesorul va iniţia aşa numitul ciclu
de achitare a întreruperii. Pe parcursul acestui ciclu extern, va genera un
semnal de răspuns (achitare) a întreruperii, numit generic INTACK (interrupt
acknowledge) spre toate interfeţele de intrare – ieşire (oricare, sau chiar mai
multe, i-au putut solicita întrerupere). Ca urmare a recepţionării semnalului de
recunoaștere INTACK, interfaţa care a întrerupt va furniza microprocesorului,

43
prin intermediul bus-ului de date, un aşa numit octet vector de întrerupere
(VI). Dacă au existat mai multe întreruperi simultane, acestea au fost arbitrate
după un anumit protocol (priorități fixe, rotitoare – Round Robin etc.), astfel
încât microprocesorul să trateze primordial cererea cea mai prioritară la un
moment dat. Acest octet VI este diferit pentru fiecare dispozitiv periferic în
parte, individualizându-l deci într-un mod unic. Pe baza acestui VI şi conform
unui algoritm care diferă de la microprocesor la microprocesor, acesta va
determina adresa de început a RTI, adresă ce va urma să o introducă in registrul
intern PC. Fireşte, la VI diferiţi vor corespunde adrese de început ale RTI
diferite. Spre exemplu, la procesoarele din familia Intel x86, noul PC (CS:IP) se
află la adresa de memorie din spațiul de cod (instrucțiuni), data de (VI)x4.
3.) Microprocesorul va salva într-o zonă specială de program, numită
memorie stivă și situată în memoria RAM, PC-ul aferent instrucţiunii imediat
următoare instrucţiunii executate de către microprocesor din PP (PCrev), pentru
a putea şti la finele RTI unde să revină exact în PP. De asemenea, va salva și
conținutul anumitor regiștri de stare.
Memoria stivă este o zonă de memorie RAM, caracterizată la un moment
dat de aşa numitul vârf al stivei, adică, în general, de ultima locaţie ocupată
din stivă. Acest vârf al stivei este pointat (adresat) permanent de conţinutul unui
registru special dedicat, existent în orice microprocesor modern, numit SP (Stack
Pointer).
În memoria stivă sunt posibile două tipuri de operaţii:

operaţia PUSH Reg care se desfăşoară astfel:


SP ← (SP) − 1 (cuvânt = octet )

(Reg) → Mem | adr. SP
operaţia POP Reg:
(Reg) ← Mem | adr. SP

SP → (SP) + 1

44
Figura 1.8. Modul de lucru al stivei

Dacă un cuvânt din stivă este reprezentat pe 16/32/64 biți atunci registrul
SP se decrementează/incrementează cu 2, 4 respectiv 8. Stiva este o memorie de
tip LIFO (last in, first out) și care, spre deosebire de PC în procesarea
secvenţială, "creşte" (PUSH) de obicei înspre adrese descrescătoare, evitându-se
astfel suprapunerea zonelor de program (cod), cu cele de memorie stivă.
4.) Intrarea în RTI se face simplu, prin introducerea adresei de început a
RTI, calculată în pasul 2, în registrul PC. Normal, în continuare microprocesorul
va aduce şi executa prima instrucţiune din RTI, protocolul de tratare fiind în
acest moment încheiat şi controlul fiind preluat de RTI a perifericului care a
solicitat întrerupere.
După cum s-a observat, protocolul de tratare salvează în stivă doar PC-ul
de revenire (la anumite microprocesoare se mai salvează registrele de stări -
flags). Acest fapt se poate dovedi insuficient, având în vedere că în cadrul RTI
pot fi alteraţi așa numiţii regiştri interni ai microprocesorului. Această alterare a
regiştrilor poate fi chiar catastrofală, la revenirea în PP. Din acest motiv, cade în
sarcina celui care scrie RTI să salveze (instrucţiuni PUSH) respectiv să
restaureze corespunzător (instrucţiuni POP) conținutul acestor regiştri ai CPU.

45
Figura 1.9. Efectul RTI asupra stivei

Acţiunea instrucţiunii RETURN este echivalentă cu o operaţie (instrucțiune


virtuală) de tip POP PC.
a ) PC ← Mem | adrSP
RET : 
b) SP ← SP + 1
Acum devine evident de ce instrucţiunea RETURN implementează
revenirea în PP pe instrucţiunea imediat următoare celei întrerupte.
Observație: Prin mecanismul de stivă se pot gestiona perfect şi
întreruperile de tip imbricat (apariţia unei întreruperi INT, în chiar rutina de
tratare a altei întreruperi, atunci când este permis). Desigur că întreruperile de la
interfețe nu vor intra direct în microprocesor, ci într-un așa numit circuit de
arbitrare a întreruperilor (ex. Intel 8259A). Acesta este un circuit programabil,
care arbitrează întreruperile după un anumit algoritm, generează semnalul de
achitare INTACK către microprocesor și generează câte un vector de întrerupere
pentru fiecare nivel de întrerupere. Acest circuit permite și mascarea/demascarea
unor anumite nivele de întrerupere.

1.2.3. MODUL DE LUCRU PRIN TRANSFER DMA (DIRECT MEMORY


ACCESS)

Există dispozitive periferice a căror rată de transfer (octeţi /secundă) este


atât de ridicată încât, din motive de timing, face imposibil modul de lucru prin
întreruperi. Astfel, spre exemplu, discurile magnetice şi interfeţele video, impun
rate de transfer de 4-20 Mo /s, rezultând transmiterea câte unui octet la fiecare
interval de 250 ns până la 40 ns respectiv. Este evident că, fără un buffer FIFO
(First In First Out) între periferic şi memorie, implementat în interfața

46
respectivă, transferul prin întreruperi este imposibil în acest caz, întrucât rata de
transfer periferic-interfață este comparabilă cu durata unei instrucţiuni a
microprocesorului. Aşadar, în aceste cazuri, durata RTI ar fi mult mai mare
decât rata de transfer a perifericului (octeţi per secundă). Un monitor video este
un alt periferic rapid de vreme ce, pe durata unei curse directe a baleiajului pe
orizontală a spotului, de câteva zeci de microsecunde, trebuie afişate zeci sau
chiar sute de octeţi (caractere, pixeli). De aceea, în aceste cazuri se impune un
transfer direct între memorie şi dispozitivul periferic (interfață). Mai precis, în
acest ultim caz, să considerăm un monitor video având o rezoluție de 1024x768
pixeli = 786432 pixeli. Un pixel reprezintă, practic, un punct (evident având o
dimensiune nenulă, nu ca punctul din geometrie) de pe monitorul video, având o
anumită culoare asociată. Considerând că un pixel de pe ecranul monitorului
este reprezentat prin 3 octeți Red/Green/Blue (RGB), memorați la adrese
succesive în memoria video, rezultă că un ecran de monitor video este codificat
prin 786432x3=2359296 octeți RGB. Să presupunem că această cantitate de date
trebuie trimisă din memoria video la monitor, de 100 de ori pe secundă (rată de
refresh de 100 Hz). Altfel spus, cei 2359296 octeți RGB trebuie trimiși din
memoria video spre monitor, la fiecare 10 ms. Rezultă că intervalul temporal
între doi octeți succesivi din memoria video este 10ms/2359296 octeți = 4,24 ns.
Este evident acum că transferul octeților din memoria video la monitor nu se
poate face prin instrucțiuni ale microprocesorului, deoarece chiar și o singură
instrucțiune poate dura mai mult decât cele 4,24 ns. Rezultă deci că transferul se
face direct între memoria video și monitorul video, prin intermediul unei
interfețe specializate (Direct Memory Access), fără implicarea
microprocesorului.

Figura 1.10. Modul de lucru prin transfer DMA

47
Atunci când se doreşte prin program transferul unor octeţi din memorie pe
disc sau citirea de pe disc în memoria RAM, microprocesorul va scrie în
interfaţa DMA aferentă (prin instrucţiuni de tip OUT succesive), următoarele
informaţii:

- adresa de început de pe disc (nr. cilindru, nr. cap citire/scriere, nr. sector =
header). Header reprezintă adresa de început sector, deci un identificator al
sectorului, care se scrie la formatarea fizică a discului.

Figura 1.11. Structura discului magnetic

- adresa de început a zonei de memorie (RAM) utilizată în transfer


- nr. octeţi (sectoare) care trebuie transferate
- sensul transferului de date (Write sau Read pe / de pe disc)

În urma recepţionării acestor informaţii, interfaţa DMA va activa un semnal


numit cerere de bus (HOLD) spre microprocesor. Ca urmare a recepţionării
semnalului HOLD, la finele ciclului maşină în curs (reamintesc, ciclul este
unitate atomică de procesare !), microprocesorul îşi va pune bus-urile de adrese,
date şi comenzi in starea TS, permiţând astfel controlul acestora de către
interfața DMA (EN1=1, microprocesor master pe bus, EN2=1, DMA master pe
bus).

48
Simultan cu acest proces, microprocesorul va activa semnalul de răspuns la
HOLD, numit semnal de achitare a cererii (HLDA - Hold Acknowledge). Ca
urmare a recepţionării achitării HLDA, DMA-ul va starta transferul de date
efectiv între disc şi memorie, având toate informaţiile necesare pentru aceasta.
Spre exemplu, dacă s-a comandat citire de pe disc (scriere în memorie), DMA-ul
va adresa memoria RAM pe bus-ul de adrese, simultan cu punerea pe bus-ul de
date a cuvântului (octetului) care trebuie scris în memorie. Evident că interfața
DMA va avea un buffer de tip FIFO (First In, First Out), având o capacitate
multiplu de numărul de octeți dintr-un sector, din care va prelua octeții de la
disc, spre a-i memora în RAM. Discul va scrie octeții în acest buffer în mod
sincron (în rafală - burst), în timp ce interfața DMA îi va prelua, pentru a-i scrie
în memoria principală prin cicli succesivi de scriere în memorie (deci nu prin
instrucțiuni, nu în mod programat). La finele transferului DMA, interfaţa va
dezactiva semnalul HOLD. Ca urmare, microprocesorul va dezactiva şi el
semnalul HLDA și îşi va continua activitatea întreruptă, prin procesarea
următorului ciclu maşină. O cerere de bus (HOLD) este prioritară faţă de o
cerere de întrerupere (INT). Dacă prima se acceptă la finele ciclului (fazei) în
curs de procesare, a doua se acceptă abia la finele instrucțiunii curente.
De remarcat că diferenţa de principiu între transferurile prin interogare -
întreruperi şi respectiv transferul DMA, constă în faptul că, în cazul primelor
două, transferul se face programat (prin instrucțiuni), prin intermediul
microprocesorului care serveşte perifericul (în cadrul rutinei de tratare), pe când
în cazul DMA se face fără intervenţia microprocesorului (care pur și simplu stă
în acest caz, din punct de vedere al activităților externe), direct între memorie şi
interfaţa DMA, prin cicli mașină succesivi de scriere/citire. Pe timpul HLDA=1,
microprocesorul îşi întrerupe orice activitate externă, master pe bus fiind DMA-
ul. Un sistem de calcul cu DMA este un arhetip al sistemelor multiprocesor.
Există unele sisteme de calcul care au busuri (adrese, date, comenzi, stări)
diferite microprocesor – memorie respectiv microprocesor interfețe de I/O.
Avantajul unei asemenea arhitecturi constă în faptul că pe durata unui ciclu
DMA, microprocesorul ar mai putea efectua transferuri de date cu porturile de
I/O.

49
Figura 1.12. Cronograma unui transfer DMA

Tipuri de interfețe (porturi)

Porturi (interfețe) paralele

• Facilitează transferul de date între memoria microprocesorului sau a


microcontrolerului (calculator implementat pe un singur cip. Conține CPU
- Central Processing Unit, memorie și interfețe I/O. Este destinat
controlului în timp real a unor procese.) și dispozitivele periferice, prin
intermediul unor magistrale paralele (transfer simultan al mai multor biți
de date). Sens transfer: input sau output.
• Transferul se face de obicei prin intermediul unor protocoale asincrone de
tip hand-shake (intrebare-răspuns; vezi spre ex. modul de lucru prin
interogare).
• Exemplu generic protocol hand-shake: Activare Adresă Port In (de către
microprocesor-MP), Activare Comandă Read – tot de la microprocesor
(MP), Stări Wait (eventual), Activare Read_Ack – de la interfață
(periferic) + Activare și Date de la interfață, Citire date de către MP,
Dezactivare Comandă Read de către MP, Dezactivare Read_Ack de către
interfață, Dezactivare Bus Date (interfață), Dezactivare Adresă Port In
(MP).
• O magistrală este caracterizată de protocolul logic de transfer aferent
(scriere/citire/întreruperi/DMA etc.)

Porturi seriale

50
• Transfer serial asincron (Universal Asynchronous Receiver Transmitter –
UART)
• START (1 bit activ pe 0 logic), DATE (5-8 biți), PARITATE (pară-
impară, calculată prin SAU EXCLUSIV), STOP (1-2 biți activi pe 1).
Dacă paritatea emisă este diferită de cea calculată la receptor, se consideră
că datele recepționate sunt alterate.
• Este necesar ca durata unui bit să fie aceeași la Emițător și la Receptor
(ex. 600, 1200, 2400, 4800, 9600 etc. biți pe secundă)
• Emisie: buffer emisie gol (empty) se poate înscrie un nou cuvânt de
date (paralel), care va fi serializat de către UART
• Recepție: buffer recepție plin (full) trebuie citit cuvântul asamblat de
UART din buffer (conține doar datele.)
• UART – cuvintele se transmit-recepționează asincron, chiar dacă biții din
cadrul unui cuvant sunt, evident, sincroni.
• Evident, există și interfețe seriale sincrone, în care cuvintele se transmit /
recepționează în mod sincron, prin protocoale specifice (nivelul fizic de
rețea de calculatoare). Avantajul față de cele asincrone este dat de
eficiența transferului, pentru că informația de control nu este la nivelul
fiecărui octet în parte, ci la nivelul unui cadru conținând mai mulți octeți
(sincroni).

Timere

• Oferă funcții de timp (real). Practic, este obligatoriu la microcontroler


(MC).
• Măsoara timpul (ex. bucle de întârziere) și generează semnale de diferite
frecvențe / durate (spre ex., pe post de întreruperi de timp real – comutare
între aplicații, startare secvență periodică de program etc.)
• Spre exemplu, programatorul setează un registru timer cu o anumită
valoare. Acesta este decrementat cu un semnal de frecvență cunoscută.
Când conținutul acestui registru este zero, se generează o întrerupere etc.
• Alt exemplu: măsoară perioada unui semnal (nr. impulsuri de timer între
două fronturi crescătoare succesive ale semnalului).
• Funcție watchdog – un numărător intern al MC, care este activat prin
software. Când ajunge la valoarea maximă resetează sistemul. Ca să nu se
întâmple asta, programatorul trebuie să-l încarce înainte de resetare. Util
în depanare (detecție eroare de program).

51
Module PWM (Pulse Width Modulation)

• Folosite la comanda motoarelor de curent continuu, comanda surselor de


alimentare etc.
• Generat periodic, fără intervenția CPU. Perioada (T) și factorul de
umplere se pot modifica în mod controlat, prin software.

Module conversie A/D și D/A

• Practic obligatorii la microcontroler (MC).


• Timpi de conversie între 8-20 microsecunde. Semnalul analogic trebuie
menținut constant pe durata conversiei (circuite de
eșantionare/memorare).
• Rezoluții 8-12 biți. Tensiunea de referință (valoarea maximă convertită),
GND (masă).
• Declanșarea și terminarea conversiei sunt semnalizate prin biți de control.
Rezultatul conversiei este memorat într-un registru de date. Se pot genera
întreruperi la finele procesului.
• Declanșarea conversiei poate fi internă (prin soft) sau externă (ex. prin
timer)

Controler de întreruperi

• Arbitrează cererile (priorități fixe - PF, rotitoare – Round Robin)


• Generează INTACK și vectori de întrerupere pentru fiecare nivel de
cerere (Interrupt Request Level)
• Poate masca anumite cereri. Demascare prin software. Spre exemplu, în
modul de lucru cu PF, întreruperea de un anumit nivel poate bloca toate
cererile mai puțin prioritare la finele rutinei de tratare (RTI), trebuie
demascate (prin scrierea într-un registru special al controlerului).
• În general, RTI aferentă unei întreruperi poate fi întreruptă de o cerere mai
prioritară. A nu se uita însă demascarea întreruperii mai puțin prioritare la
finele rutinei celei mai prioritare.

52
2. ARHITECTURA SISTEMULUI IERARHIZAT DE MEMORIE

2.1. MEMORII CACHE

Cache: a safe place for hiding or storing things. (Webster’s New World
Dictionary of the American Language, Third College Edition - 1988)

Acest capitol are la bază părți ale unei lucrări anterioare, scrise și publicate
de autor sub forma unei monografii tehnice [Vin00b], cu revizuiri și adăugiri
semnificative în versiunea prezentată aici, în speranța că sub această formă nouă,
integrată într-un tratat universitar unitar, va fi mai ușor asimilabil de către
studenții și cititorii interesați. Acest capitol este dedicat prezentării a două soluții
consacrate, în vederea reducerii prăpastiei tehnologice de comunicație între
procesorul rapid și memoria principală lentă, respectiv memoria secundară, și
mai lentă. Este vorba despre memoriile cache și mecanismul de memorie
virtuală.
Memoria cache este o memorie situată din punct de vedere logic între CPU
(Central Processing Unit - unitate centrală, procesor) şi memoria principală
(uzual DRAM - Dynamic Random Access Memory), mai mică, mai rapidă şi mai
scumpă (per byte) decât aceasta şi gestionată – în general prin hardware – astfel
încât să existe o cât mai mare probabilitate statistică de găsire a datei accesate de
către CPU, în cache. Aşadar, cache-ul este adresat de către CPU în paralel cu
memoria principală (MP): dacă data dorită a fi accesată se găseşte în cache,
accesul la MP se abortează, dacă nu, se accesează MP cu penalizările de timp
impuse de latenţa mai mare a acesteia, relativ ridicată în comparaţie cu frecvenţa
de tact a CPU. Oricum, datele accesate din MP se vor introduce şi în cache, în
speranța că la următorul acces, nu va mai fi necesară accesarea memoriei
principale.
Memoriile cache sunt implementate în tehnologii electronice de înaltă
performanţă, având deci un timp de acces foarte redus, mai ales dacă sunt
integrate în microprocesor (cca. 1 – 3 ns la ora actuală). În prezent, presiunea
asupra acestor memorii cache este foarte ridicată, rolul lor fiind acela de a

53
apropia performanţa memoriilor principale (DRAM), a căror latenţă scade cu
doar cca. 5-7 % pe an, de aceea a microprocesoarelor (a căror performanță
creştea cu cca. 50 – 60 % pe an, cel puțin până prin anul 2004). În general,
pentru a accesa o locaţie DRAM, un procesor “pierde” 20 – 120 de impulsuri de
tact (~ timp acces DRAM / TCLK, unde TCLK = perioada ceasului
microprocesorului), în schimb accesarea cache-ului intern (de nivel 1) se face în
doar 1 – 3 impulsuri de tact. Cu alte cuvinte, memoria cache reduce timpul
mediu de acces al CPU la MP, ceea ce este foarte util.
Se defineşte un acces al CPU cu hit în cache, ca fiind un acces care găseşte
o copie validă în cache a datei accesate. Un acces cu miss în cache este unul care
nu găseşte o copie în cache a datei accesate de către CPU şi care, prin urmare,
adresează MP, cu toate penalizările de timp care derivă din accesarea acesteia.
Se defineşte, ca un parametru de performanţă al unei memorii cache, rata
de hit, ca fiind raportul statistic între numărul acceselor cu hit în cache, respectiv
numărul total al acceselor CPU la memorie. Măsurat pe benchmark-uri
(programe de test) reprezentative, la ora actuală sunt frecvente rate de hit de
peste 90 %. Rata de miss (RM) este complementara ratei de hit (RH), astfel că:
RH [%] + RM [%] = 100 %. În esenţă, utilitatea cache-ului derivă din următorul
fapt: la o citire cu miss (din MP), data adusă din MP este introdusă şi în cache,
în speranţa că la o următoare citire a aceleiaşi date, aceasta se va găsi în cache
(hit). În realitate, în cazul unei citiri cu miss în cache se aduce din MP nu doar
data (cuvântul) dorită de către CPU, ci un întreg bloc (4 – 32 cuvinte) care,
evident, conţine și data respectivă. Rațiunea constă în faptul că s-ar putea ca
microprocesorul să acceseze în continuare datele situate în vecinătatea spațială a
datei curente (v. în continuare, principiile statistice de vecinătate temporală și
spațială.) O citire cu miss presupune aducerea blocului din MP, dar înainte de
aceasta se impune evacuarea în MP a unui bloc din cache. Aşadar, transferul din
cache în MP se face tot la nivel de bloc şi nu de cuvânt. Astfel, se optimizează
traficul între cache şi MP pe baza a două principii statistice care vor fi discutate
în continuare. Practic memoria cache reduce timpul mediu de acces la memorie.
Spre exemplu, considerând o memorie DRAM cu timp de acces de 40 ns și un
microprocesor cu frecvența de tact de 2 GHz (perioada 0,5 ns), timpul de
așteptare al microprocesorului în cazul unui acces la DRAM este 40ns/0,5ns=80
tacte CPU. Dacă am avea o memorie cache cu rata de hit de 90% (rata de miss
fiind implicit 10%) și considerând că memoria cache introduce doar două tacte
de așteptare, timpul mediu de acces al microprocesorului la memorie ar fi: Rata

54
de hit x Nr. tacte acces cache + Rata de miss x Nr. tacte acces la MP = 0,9x2 +
0,1x80 = 9,8 tacte < 80 tacte.
Se pune întrebarea, de unde, totuși, eficiența memoriilor cache? La o primă
vedere, eficiența lor poate părea a fi nulă. Considerând, spre exemplu, o
memorie cache de capacitate 128 KO și un spațiu virtual de adresare al
microprocesorului de 64 TO, rata de hit pare a fi 128 KO/64 TO = 0 (practic).
Raționamentul acesta are însă la bază o presupunere implicită, care este falsă,
anume că accesul microprocesorului (prin program) la spațiul de memorie
(virtuală) ar fi unul uniform aleator (atenție însă, nu întotdeauna, pornind de la o
ipoteză falsă și raționând corect, se obține o concluzie falsă. Aceasta ar putea fi,
la fel de bine, adevărată, având în vedere că, din punct de vedere logic falsul, ca
și adevărul, implică adevăr). În esenţă, eficienţa memoriilor cache se bazează pe
două principii de natură statistică şi care caracterizează intrinsec noţiunea de
program aflat în rulare: principiile de vecinătate (localitate) temporală şi spaţială
(spatial and temporal locality). Conform principiului de localitate (vecinătate)
temporală, există o mare probabilitate ca o dată (instrucţiune) accesată la un
moment dat de către CPU, să fie accesată din nou, în viitorul imediat. Conform
principiului de localitate spaţială, există o probabilitate semnificativă ca o dată
situată în imediata vecinătate (spațială, de adrese) a unei date accesate curent de
către CPU, să fie şi ea accesată în viitorul apropiat (pe baza acestui principiu
statistic se aduce din MP în cache un întreg bloc şi nu doar strict cuvântul dorit
de către CPU). O buclă de program – structură esenţială în orice program –
exemplifică foarte clar aceste principii şi justifică eficienţa conceptului de cache.
O combinare a celor două principii anterior expuse, conduce la celebra
“regulă 90/10” care afirmă că cca. 90 % din timpul de rulare al unui program se
execută doar cca. 10 % din codul acestuia (practic, codul din bucle). Personal,
cred că mai puţin. Pe baza acestor principii statistice empirice se situează întreg
eşafodajul conceptului de cache; eficienţa sa deosebită nu poate fi explicată prin
considerente analitice, pentru simplul fapt că este practic imposibil a descrie
analitic noţiunea de program dinamic, cu mare acuratețe. În fond, ce este un
program dinamic? Care este distribuţia instrucţiunilor dinamice sau a
primitivelor structurale, corpusurilor de program etc., într-un program? Poate fi
descris concret, comportamentul unui program generic, pe parcursul rulării sale,
pe baza unor modele deterministe sau nedeterministe (stohastice, aleatoare)? Se
pot accepta modele nedeterministe pentru programe deterministe? Dificultatea
unor răspunsuri exacte la aceste întrebări – dată în fond de imposibilitatea
punerii în ecuaţie a minţii umane, cea care creează infinita diversitate de

55
“programe” – face ca cea mai bună explicaţie asupra eficienţei memoriilor cache
să stea în cele două principii empirice anterior schiţate, caracterizând intrinsec
noţiunea de program aflat în procesare (dinamic, run-time). În [Vin07] se arată
că principiul statistic de vecinătate temporală a instrucțiunilor și datelor a fost
extins. Astfel, a fost descoperit principiul statistic de vecinătate a valorilor
instrucțiunilor (value locality). Acesta afirmă că există o probabilitate
semnificativă ca valoarea produsă de instanța curentă a unei anumite instrucțiuni
dinamice să aparțină mulțimii anterioarelor k valori produse de anterioarele k
instanțe ale aceleiași instrucțiuni. Spre exemplu, pentru instrucțiunile de citire
din memorie (LOAD) s-a arătat ca 1-Value Locality (pentru k=1) este de cca.
50%, iar 16-Value Locality (k=16) este de cca. 80%! Așadar, instrucțiunea
curentă nu doar că va fi adusă probabil din nou de către microprocesor din
memorie, dar, mai mult, există șanse considerabile ca aceasta să producă o
valoare situată în vecinătatea temporală a anterioarelor k valori produse de
aceeași instrucțiune. Acest principiu statistic a condus la ideea predicției
dinamice a valorilor instrucțiunilor, cu beneficii importante asupra performanței
microprocesoarelor [Vin07].
Din punct de vedere arhitectural, există trei tipuri distincte de memorii
cache, în conformitate cu gradul lor de asociativitate: cu mapare directă,
semiasociative şi total asociative.

Figura 2.1. Scheme tipice de mapare în cache

La cache-urile cu mapare directă (direct mapped), ideea principală constă


în faptul că un bloc din MP poate fi găsit în cache (hit) într-un bloc unic
determinat. În acest caz, regula de mapare a unui bloc din MP în cache este dată
de următoarea formulă:

(Adresa blocului din MP) modulo (Nr. blocuri din cache)

56
Adresa emisă de către microprocesor ar putea fi cea virtuală (efectivă) sau
cea fizică, după cum vom discuta în contextul memoriei virtuale. În caz de hit,
pentru a ști care bloc din MP există la o anumită locație din cache, această
locație trebuie să aibă, pe lângă câmpul de date și un așa-numit câmp de
identificare (Tag), care codifică adresa blocului respectiv din MP. Așadar,
condiția necesară pentru un hit în cache este dată de identitatea dintre tag-ul
adresei de memorie emisă de către microprocesor (mai precis adresa blocului din
MP care se doreste a fi accesat) și tag-ul blocului corespunzător din cache.
Stricteţea regulii de mapare conduce la o simplitate constructivă a acestor
memorii (avantaj), dar şi la fenomenul de interferenţă a blocurilor din MP în
cache (dezavantaj). Astfel, spre exemplu, blocurile 12, 20, 28, 36, 42 etc. nu pot
coexista în acest cache la un moment dat, întrucât toate se mapează pe blocul 4
din cache (evident, cu tag-uri diferite). Prin urmare, o buclă de program care ar
accesa alternativ blocurile 20 şi 28 din MP ar genera o rată de hit egală, practic,
cu zero, întrucât cele două blocuri s-ar evacua reciproc din cache. Un avantaj al
acestui tip de memorie cache este dat de faptul că citirea unei date se poate face
speculativ, în paralel cu procesul de comparare a tag-urilor. În caz de miss, data
citită în avans nu va fi utilizată de microprocesor. Această speculație, care poate
reduce timpul de acces la memoria cache, nu mai este posibilă în cazul cache-
urilor asociative.
În scopul reducerii ratei de interferență s-au construit cache-urile
semiasociative sau complet asociative, unde blocurile conflictuale pentru cache-
urile cu mapare directă (cele de la adresele 20 și 28, în exemplul nostru) ar putea
coexista în cache. La cache-urile semiasociative există mai multe așa numite
seturi, fiecare set având mai multe, dar același număr, de blocuri componente.
Aici, regula de mapare precizează strict doar setul în care se poate afla blocul
dorit, astfel:

(Adresa blocului din MP) modulo (Nr. seturi din cache)

În principiu, blocul dorit se poate mapa oriunde în setul respectiv (numărul


setului fiind dat de formula anterioară). Pentru a afla unde este concret mapat în
setul respectiv blocul căutat, trebuie comparat tag-ul blocului emis de către
microprocesor cu tag-urile tuturor blocurilor din setul respectiv. În cazul unei
coincidențe avem hit, iar în caz contrar, miss. De remarcat că acest proces de
căutare se face în paralel, prin utilizarea unor circuite comparatoare și a altor
circuite digitale. Evident că aceste circuite complică microarhitectura și cresc

57
timpul de acces la cache. Desigur, identificarea hit/miss și accesul propriu zis la
dată sunt procese secvențiale în acest caz, nicidecum concurente, ca la cache-ul
cu mapare directă. În plus, la un miss în cache, înainte de încărcarea noului bloc
din MP, trebuie evacuat (evicting) un anumit bloc din setul respectiv. Iată cum
asociativitatea introduce o problemă nouă, pe care cache-urile cu mapare directă
nu o aveau (evacuarea se făcea în acel caz implicit; în cazul unui miss, se evacua
blocul interferent din cache). În principiu, în mod uzual, există implementate
două-trei tipuri de algoritmi de evacuare: pseudo-random (cvasi-aleator, relativ
uşor de implementat), FIFO (sau round-robin, se evacuează blocul cel mai vechi
din cache. Contorul aferent blocului se încarcă doar la încărcarea blocului în
cache şi nu la fiecare hit per bloc, ca la algoritmul LRU – v. în continuare) şi
LRU (“Least Recently Used”). Algoritmul LRU evacuează blocul din cache cel
mai de demult neaccesat de către CPU (sau, eventual, neaccesat deloc de când a
fost alocat în cache; cu alte cuvinte, se evacuează acel bloc pentru care durata
din momentul ultimei sale accesări de către CPU, până la momentul curent, este
maximă), în acord cu principiul de localitate temporală, care afirmă că cel mai
recent bloc accesat va fi accesat din nou, cu o probabilitate mare (așadar, cel mai
de demult neaccesat, va fi accesat din nou, cu o probabilitate mică, deci merită
evacuat.) În practică, din motive de costuri, implementările LRU sunt
simplificate şi deci, aproximative. Spre exemplu, se poate asocia fiecărui bloc
dintr-un set un contor. În caz de hit la acel bloc, contorul respectiv va fi setat la
o valoare maximă (încărcare), iar celelalte contoare din set vor fi decrementate.
Astfel, la miss se va evacua blocul având valoarea contorului minimă (în caz că
mai multe blocuri au valoarea minimă, oricare dintre ele poate fi evacuat). De
remarcat faptul că circuistica aceasta mărește complexitatea, dar și consumul de
energie electrică al acestor tipuri de memorii cache. Deşi acest model pare
intuitiv corect, ca orice model statistic el poate genera şi rezultate eronate
uneori. Spre exemplu, numărul total de accese cu miss poate uneori să crească
când creşte asociativitatea, iar politica de înlocuire LRU este departe de a fi cea
optimă pentru unele din programe. Această „anomalie” poate fi evitată dintr-un
punct de vedere pur teoretic prin folosirea algoritmului “optim” (OPT - atribuit
lui Laszlo Belady), în loc de LRU. Algoritmul OPT, înlocuieşte întotdeauna
blocul din cache care va fi adresat cel mai târziu în viitor (sau care, eventual, nu
va mai fi adresat deloc). Un astfel de algoritm s-a dovedit a fi cvasi-optimal
pentru toate pattern-urile de adrese de program, ratele de miss fiind cele mai
mici în acest caz, dintre mai toate politicile de înlocuire (evacuare) folosite.
Totuși, politica aceasta se dovedeşte optimă doar pentru fluxuri de instrucţiuni

58
de tipul read-only. Pentru cache-urile cu modalitate de scriere write-back, prin
care se scrie data doar în cache (v. în continuare), algoritmul de înlocuire OPT
nu este întotdeauna optimal. Spre exemplu, poate fi mai costisitor să se
înlocuiască blocul cel mai târziu referit în viitor, dacă blocul acela trebuie scris
şi în memoria principală, fiind "murdar" (scris deja în cache), faţă de un bloc
"curat" (nescris încă în cache), referit în viitor (puţin) mai devreme decât blocul
"murdar" anterior, și deci care nu mai trebuie evacuat în MP, ci doar supra-scris
în cache. Algoritmul OPT este evident un algoritm speculativ, practic imposibil
de implementat în practică (pentru că nu putem ști care este blocul care va fi
adresat cel mai târziu, sau chiar deloc, în viitor). Totuşi el are două calităţi
majore: (1) reprezintă o metrică de evaluare teoretică a ratei de hit, de tip prag
optimal, a eficienţei algoritmului de evacuare implementat în realitate, absolut
necesară, pentru că dă seamă asupra cașabilității intrinseci a programului
(instrucțiuni și date) însuși şi (2) induce ideea fertilă a predictibilităţii dinamice
(run-time) a valorilor de folosinţă ale blocurilor din cache, conducând astfel la
algoritmi predictivi rafinaţi de evacuare, de tip adaptiv (vezi în continuare
memoria cache de tip VC-SVC, care implementează un astfel de algoritm
aproximat). Reproșul care poate fi făcut algoritmilor statistici de tip LRU este ca
sunt neadaptivi la contextul rulării programului. Așadar, este de dorit
implementarea unor algoritmi de evacuare adaptivi, cum este algoritmul SVC,
prezentat în continuare.
Dacă un set din cache-ul semiasociativ conţine N blocuri, atunci cache-ul
se mai numeşte “N-way set associative”. Mai nou, se implementează algoritmi
de evacuare predictivi dinamici (run-time), care anticipează pe baze euristice
utilitatea de viitor a blocurilor memorate în cache, evacuându-l pe cel mai puţin
valoros la un anumit moment dat (un algoritm euristic rezolva o problemă care
nu s-ar putea rezolva în timp util prin algoritmi clasici, datorită complexității
prohibite, prin metode care aproximează soluția ideală; se aplică pentru
probleme NP-hard). Deşi aceşti algoritmi depăşesc în mod normal cadrul acestui
curs de iniţiere în domeniul arhitecturii microprocesoarelor, în continuare se va
prezenta totuşi unul, integrat în micro-arhitectura numită Selective Victim
Cache.
Este evident că într-un astfel de cache semiasociativ rata de interferenţă se
reduce odată cu creşterea gradului de asociativitate (N “mare”, deci mai multe
blocuri/set). Aici, spre exemplu, blocurile 12, 20, 28 şi 36 pot coexista în setul 0.
Prin reducerea posibilelor interferenţe ale blocurilor, creşterea gradului de
asociativitate determină îmbunătăţirea ratei de hit şi deci a performanţei globale.

59
Pe de altă parte însă, asociativitatea impune căutarea după conţinut (se caută
deci într-un set dacă există memorat blocul respectiv), ceea ce conduce la
complicaţii structurale şi deci la creşterea timpului de acces la cache şi implicit
la diminuarea performanţei globale. Aceeași cauză (creșterea gradului de
asociativitate) are, iată, efecte contrare (creșterea/scăderea performanței)!
Optimizarea gradului de asociativitate, a capacităţii cache, a lungimii blocului
din cache etc., nu se poate face decât prin laborioase simulări software (design
space exploration), variind toţi aceşti parametri în vederea maximizării ratei
globale de procesare a instrucţiunilor [număr mediu de instrucțiuni
dinamice/ciclu].
În fine, memoriile cache total asociative, implementează un singur set, cât
întregul cache, permiţând maparea blocului practic oriunde în acest cache. Ele
nu se implementează deocamdată în siliciu, decât având capacități extrem de
reduse, datorită complexităţii deosebite şi a timpului prohibit de căutare/acces.
Reduc însă (practic) total interferenţele blocurilor la aceeaşi locaţie din cache şi
constituie, la nivel de simulare prin software, o metrică superioară utilă în
evaluarea ratei de hit pentru celelalte tipuri de cache-uri (prin comparaţie). Cele
trei scheme următoare prezintă implementări realizate pentru tipurile de cache
anterior discutate.

Cache semiasociativ pe două căi

Figura 2.2. Cache semiasociativ pe două căi

Cache complet asociativ

60
Figura 2.3. Cache complet asociativ

Cache direct mapat

Figura 2.4. Cache direct mapat

În toate aceste trei figuri s-a considerat un bloc compus din 4 cuvinte. Bitul
V este un bit de validare a blocului, V = 1 fiind o condiţie necesară a obţinerii
hitului. Bitul este util îndeosebi în sistemele multiprocesor (multicore), în
vederea menţinerii coerenţei memoriilor cache locale datorită redundanţei
informaţionale. Mai precis, aici apare necesitatea citirii din cache-ul propriu a
ultimei (celei mai recente) copii modificate a datei respective. Când un procesor
modifică în cache-ul propriu o copie locală a unei date, toate blocurile care
conţin acea dată din cadrul celorlalte procesoare trebuie invalidate, prin resetarea
bitului V = 0, pentru a invalida un (fals) hit. Necesitatea invalidării blocurilor (V

61
= 0) apare chiar şi în sistemele uniprocesor. Imediat după resetarea sistemului,
uzual, procesorul execută un program încărcător (bootstrap), rezident în
memoria EPROM. Cum imediat după iniţializarea sistemului conţinutul cache-
ului este practic aleator (memorie SRAM), pentru a evita false hituri la citirea
programului încărcător din EPROM, se iniţializează biţii V cu zero. La prima
încărcare a unei date (instrucţiuni) din memoria principală în cache, bitul V
aferent se va seta pe ‘1’, validând astfel hitul. Evident că un algoritm eficient de
evacuare a blocurilor din cache, va trebui să considere blocurile având bitul
V=0, prioritare în procesul de evacuare.
Bitul D (Dirty, “murdar”, scris deja) este pus pe ‘0’ la încărcarea iniţială a
blocului în cache. La prima scriere a acelui bloc, bitul se pune deci pe ‘1’.
Evacuarea propriu-zisă a blocului se face doar dacă bitul D = 1. Practic, prin
acest bit de stare se minimizează evacuările de blocuri din cache în MP, pe baza
principiului că un bloc trebuie evacuat numai dacă a fost scris în cache (altfel,
copia din MP este o clonă a datei din cache).
În acest sens, din punct de vedere al acceselor de scriere în cache a unui
procesor, există două posibilităţi majore:
• Strategia “Write Through” (WT), prin care informaţia este scrisă de către
procesor atât în blocul aferent din cache cât şi în blocul corespunzător din
memoria principală. Este mai uşor de implementat hardware decât
strategia WB (v. mai jos), iar în plus, nu mai este necesară evacuarea
blocului din cache în MP (pentru că cele două blocuri sunt identice).
Scrierea se face la viteza redusă a MP, îngreunată şi de accesarea busului
sistem (cca. 25% din accesele la memorie sunt scrieri). Pentru a reduce
acest dezavantaj, deseori se foloseşte un aşa numit Data Write Buffer
(DWB). DWB reprezintă o coadă FIFO multiport, de lungime (capacitate)
parametrizabilă, a cărei valoare trebuie să fie minim egală cu numărul
maxim de instrucțiuni cu referință la memorie care pot fi trimise în mod
simultan spre execuție. Fiecare locaţie conţine un bit de Busy, care arată
dacă locația din DWB este ocupată sau nu, adresa de memorie (virtuală
sau fizică) şi data de scris. Cu DWB sunt posibile deci STORE-uri (scrieri
în memorie) simultane (multiport), fără el acestea trebuind serializate, cu
penalităţile de rigoare. În plus, DWB va putea rezolva prin "bypassing",
într-un mod foarte elegant, hazarduri de tip "LOAD after STORE" cu
adrese identice, nemaifiind deci necesară accesarea sistemului de
memorie de către instrucţiunea LOAD (citire din memorie) subsecventă
celei de STORE. În acest caz, bitul de stare Dirty nu-și mai are sensul.

62
• Strategia “Write - Back” (WB), prin care informaţia este scrisă de
procesor numai în cache, blocul modificat fiind transferat în MP numai la
evacuarea din cache. Asigură coerenţă mai facilă a datelor din cache (v. în
continuare), sincronizare la scriere cu cache-ul, consum redus de putere
(nu accesează busul sistem la scriere cu hit) etc.
În vederea menţinerii coerenţei cache-urilor, cu precădere în sistemele
multimicroprocesor – există două posibilităţi majore, în funcţie de ce se
întâmplă la o scriere (vezi pentru detalii capitolul dedicat sistemelor
multimicroprocesor):
a) Write invalidate (WI) – prin care CPU care scrie, determină ca toate
copiile datei din celelalte memorii cache să fie invalidate, înainte ca el
să-şi modifice blocul din cache-ul propriu.
b) Write Broadcast – CPU care scrie pune data de scris pe busul comun
spre a fi actualizate toate copiile din celelalte cache-uri.
Ambele strategii de menţinere a coerenţei pot fi asociate cu oricare dintre
protocoalele de scriere (WT, WB), dar de cele mai multe ori se preferă scriere
tip WB cu invalidare (WI). Nu detaliem aici problemele de coerenţă întrucât
acestea se referă cu deosebire la problematica sistemelor multiprocesor şi deci
depăşesc cadrul strict al acestei prezentări. Se va relua problema în paragraful
din Capitolul 4 în care vom prezenta câteva elemente mai importante, referitoare
la sistemele multiprocesor. Se va considera totuși un exemplu care arată cum două
procesoare pot "vedea" două valori diferite pentru aceeaşi locaţie (X) de memorie
globală, adică un caz tipic de incoerenţă a unei valori globale, partajate.

Pas Eveniment Conţinut Conţinut Conţinut


cache cache Memorie
CPU1 CPU2 globală (X)
0  1
1 CPU1 citeşte X 1 1
2 CPU2 citeşte X 1 1 1
3 CPU1 scrie 0 în X 0 1 0
(WT) 0 1 1
WB
Tabelul 2.1. Exemplificarea unei incoerenţe

În Tabelul 2.1 s-a presupus că iniţial, nici una din cele două cache-uri nu
conţine variabila globală X şi că aceasta are valoare 1 în memoria globală. De

63
asemenea, s-au presupus cache-uri de tip WT la scriere (un cache WB ar
introduce o incoerenţă asemănătoare). În pasul 3, CPU 2 are o valoare
incoerentă a variabilei X.
În continuare (Tabelul 2.2), se prezintă un exemplu de protocol de coerenţă
WI, bazat pe un protocol de scriere în cache de tip WB.

Pas Activitate Activitate Loc.X Loc.X Loc. X


procesor pe bus cache cache Memorie
comun CPU1 CPU2 globală
0 0
1 CPU1 citeşte Cache 0 0
X Miss (X)
2 CPU2 citeşte Cache 0 0 0
X Miss (X)
3 CPU1 scrie Invalidare 1 INV. 0
‘1’ în X X
4 CPU2 citeşte Cache 1 1 0
X Miss (X)
Tabelul 2.2. Coerenţa prin protocol WI

În pasul 4, CPU1, ca singur deținător al valorii corecte (copie exclusivă),


pune pe busul comun valoarea lui X ("1", copie exclusivă) spre a fi citită de
CPU2. Astfel se scrie (actualizează) noua valoare a lui X în cache-ul lui CPU2,
iar X devine acum o variabilă partajată (shared). Actualizarea datei în memoria
principală se va face la evacuarea acesteia dintr-un cache.
Apar posibile 4 procese distincte într-un cache ca în tabelul următor:

Tip Hit / Acţiune în cache


acces Miss
Citire Miss Evacuare bloc (LRU) +
încărcare bloc nou, data si tag
(alocare cache)
Citire Hit Comparare tag-uri si citire dată
Scriere Miss (Evacuare bloc dacă Dirty=1) +
încărcare bloc nou (modificare
tag+data) + scriere dată în bloc
(WB)

64
Scriere Hit Scriere dată în blocul din cache
(WB)
Tabelul 2.3.Tipuri de acces în cache

Aşadar, memoriile cache îmbunătăţesc performanţa, îndeosebi pe citirile cu


hit, iar în cazul utilizării scrierii de tip “Write Back”, inclusiv pe scrierile cu hit.
Îmbunătăţirea accesului la memorie pe citirile CPU este normală, având în
vedere că acestea sunt mult mai frecvente decât scrierile (orice instrucţiune
implică cel puţin o citire din memorie, pentru aducerea sa - fetch; statistic, cca.
75 % din accesele la memorie sunt citiri. Explicația este simplă, având în vedere
că multe instrucțiuni mai au și operanzi în memorie, care trebuie aduși în CPU.
Scrierile în memorie le fac doar instrucțiunile de tip STORE). Cauzele miss-
urilor în cache-uri, conform literaturii acestui domeniu [Hen11], sunt de trei
tipuri:

• datorită faptului că, în fond, primul acces la un bloc în cache generează


întotdeauna miss (compulsory misses); sunt inevitabile.
• datorită capacităţii fatalmente limitate a cache-ului, care evident că nu poate
conţine la un moment dat toate blocurile din MP, ceea ce implică evacuări /
încărcări (capacity misses).
• datorită interferenţelor (conflictelor) unor blocuri din MP pe acelaşi bloc din
cache (conflict misses); acestea se reduc odată cu creşterea capacităţii cache
şi a gradului de asociativitate.

Reducerea penalizărilor la un miss în cache se poate face prin


implementarea unor niveluri multiple de cache-uri (multi-level inclusion, multi-
level exclusion, hibrid). Prezentăm mai jos strategiile de evacuare în cazul a
două niveluri de cache (L1 - mai mic și mai rapid, L2 - mai mare dar mai lent,
oricum mai rapid decât nivelul superior):

• L1 Read_Miss – aduce bloc din L2 în L1 și blocul din L1 se evacuează în


MP (Multilevel Inclusion) SAU Swap_L2&L1 (Multilevel Exclusion)
• L1 Write_Hit – Write_Back (Multilevel Exclusion); Write_Through
(Multilevel Inclusion)
• L1 Write_Miss – (Multilevel Exclusion) aduce blocul din L2/MP și-l
scrie apoi în L1 (WB); (Multilevel Inclusion) aduce bloc din L2/MP L1
și-l scrie apoi în ambele niveluri ale sub-sistemului de cache (WT).

65
Aşadar:

• Multilevel Inclusion L2 mare (redundanță informațională)


• Multilevel Exclusion L2 mai mic

În figura următoare se prezintă rata de miss în sub-sistemul de cache-uri


(L1 și L2), funcție de capacitatea memoriei L2-Cache, exprimată în număr de
intrări. Cache-urile de nivel 1 (L1-Cache) s-au considerat de capacități fixe,
atât pe spațiile de instrucțiuni, cât și pe cele de date. Capacitățile memoriilor
cache simulate sunt extrem de mici, pentru că simulările s-au realizat pe
benchmark-urile Stanford C, compilate pentru arhitectura superscalară
Hatfield Superscalar Architecture, dezvoltată la Universitatea din
Hertfordshire, Anglia, sub conducerea profesorului dr. Gordon B. Steven, la
care și autorul a contribuit, printr-un stagiu de 3 luni la această universitate,
în anul 1996. Aceste benchmark-uri au până la 300 de instrucțiuni mașină,
ceea ce explică faptul că memoriile cache au capacități foarte reduse.

I-CACHE=32, D-CACHE=128

60
miss rate %

50
40
30
20
10
0
256 512 1024 2048 4096

s-cache capacity

Figura 2.4.b. Rata de miss în cache-uri, funcție de capacitatea L2-cache

Reducerea penalizărilor la un miss în cache se poate face și prin


implementarea unor memorii de tip victim – cache (v. în continuare).

Reducerea ratei de miss în cache se poate face prin:

1. Creşterea mărimii blocului (din păcate, cresc şi penalităţile de miss la


evacuare-încărcare bloc)

66
2. Creşterea capacităţii cache (dar implică mărire timp acces hit şi costuri
suplimentare)
3. Creştere asociativitate cache (creşte însă și timpul de acces la hit.)
4. Optimizarea de programe prin compilator
- intrarea într-un basic-block să reprezinte începutul unui bloc în cache.
- exploatarea localităţilor spaţiale ale datelor din cache – loop
interchange etc.

Exemplu:

for (j=0; j<1000; i++)


for (i=0; i<2000; j++)
A[i,j]=4*A[i,j];

Dezavantaj: pas (a[i,j]; a[i+1, j]) este 1000 așadar a[i,j] și a[i+1, j] sunt
în blocuri diferite în cache nu se exploatează vecinătatea spațială din cadrul
blocului căruia îi aparține a[i,j], primul accesat scade Rhit.

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


for (j=0; j<1000; j++)
A[i,j]=4*A[i,j];
Avantaj: pas (a[i,j] si a[i, j+1]) este 1 sunt în același bloc în cache
crește Rhit.

Se arată în literatura de specialitate că, la ora actuală, ierarhia de cache-uri


este departe de a fi optimal proiectată. Circa 50% din blocurile din cache sunt
“moarte”, adică nefolosite sau folosite foarte puțin după alocarea lor în cache
(analog, în subsistemele de memorie virtuală, cca. 50 % din datele dintr-o
pagină sunt, practic, nefolosite). Sunt deci necesare noi cercetări in vederea
optimizarii ierarhiei de memorii cache (mai ales în sistemele multiprocesor),
prin exploatarea mai agresivă a localităților (vecinătăților) spațiale și temporale.
Primul care a pus în lumină conceptul de memorie cache a fost prof.
Maurice Wilkes (Univ. Cambridge, Anglia), laureat al celui mai prestigios
premiu în știința calculatoarelor (Turing Award) – un pionier al calculatoarelor,
care a inventat în 1951 şi tehnica microprogramării unităţilor de comandă
aferente procesoarelor – într-un articol publicat în 1965 (“Slave memories and
dynamic storage allocation”, IEEE Trans. on Electronic Computers, April,

67
1965). Prima implementare a unui cache (cu mapare directă) aparţine probabil
lui Scarrott, în cadrul unui sistem experimental, construit tot la Universitatea din
Cambridge. Primul sistem comercial care utiliza cache-urile a fost IBM 360/85
(1968). Conceptul de cache s-a dovedit a fi extrem de fertil, nu numai în
hardware, dar şi în software, prin aplicaţii dintre cele mai diverse în sistemele de
operare (memoria virtuală), reţele de calculatoare, baze de date (replicarea
datelor), compilatoare, browsere web etc.
Pentru a reduce rata de miss a cache-urilor mapate direct (fără să se
afecteze însă timpul de hit sau penalitatea în caz de miss), cercetătorul Norman
Jouppi (pe atunci la compania Digital Equipment Corporation) – laureat in anul
2015 al Premiului Eckert-Mauchly acordat anual de organizațiile profesionale
IEEE și ACM pentru realizări remarcabile în arhitectura calculatoarelor – a
propus conceptul de “victim cache”. Aceasta reprezintă o memorie de capacitate
foarte mică (5-8 blocuri), complet asociativă, plasată între primul nivel de cache
mapat direct şi memoria principală (sau nivelul următor de cache). Blocurile
înlocuite din cache-ul principal datorită unui miss sunt temporar memorate în
victim cache. Dacă sunt referite din nou, înainte de a fi înlocuite din victim
cache, ele pot fi extrase direct din victim cache, cu o penalitate mai mică decât
cea a memoriei principale. Deoarece victim cache-ul este complet asociativ,
multe blocuri care ar genera conflict în cache-ul principal mapat direct, ar putea
rezida în victim cache, fără să dea naştere la conflicte (interferențe). Practic,
victim cache-ul corectează deciziile eronate ale evacuării din cache-ul principal.
Decizia de a plasa un bloc în cache-ul principal sau în victim cache (în caz de
miss) este făcută cu ajutorul unei informaţii de stare asociate blocurilor din
cache. Biţii de stare conţin informaţii despre istoria blocului. Această idee a fost
propusă inițial de McFarling, care foloseşte informaţia de stare pentru a exclude
blocurile sigure din cache-ul mapat direct, reducând înlocuirile ciclice implicate
de acelaşi bloc. Această schemă, numită excludere dinamică, reduce miss-urile
de conflict în multe cazuri. O predicţie greşită implică un acces în nivelul
următor al ierarhiei de memorie, contrabalansând eventuale câştiguri în
performanţă. Schema este mai puţin eficace în cazul blocurilor mari, de
capacităţi tipice cache-urilor microprocesoarelor curente. Pentru a reduce
numărul de interschimbări dintre cache-ul principal şi victim cache, cercetătorii
Stiliadis şi Varma au introdus un nou concept, numit Selective Victim Cache
(SVC), publicat în lucrarea intitulată “Selective Victim Caching: A Method to
Improve the Performance of Direct-Mapped Caches”, Technical Report, UCSC-

68
CRL-93-41, University of California, SUA, 1994 și sintetizat de autor în
[Vin00b].

Figura 2.5. Ierarhia de memorie pentru schema cu Selective Victim Cache

Cu SVC, blocurile aduse din memoria principală sunt plasate selectiv, fie în
cache-ul principal cu mapare directă, fie în selective victim cache, utilizând un
algoritm de predicţie euristic, bazat pe istoria folosirii blocului respectiv.
Blocurile care sunt mai puţin probabil să fie accesate în viitor, sunt plasate în
SVC şi nu în cache-ul principal. Predicţia este, de asemenea, folosită în cazul
unui miss în cache-ul principal, pentru a determina dacă este necesară o
interschimbare a blocurilor conflictuale (cel din SVC cu cel din cache-ul
principal). Obiectivul algoritmului este de a plasa blocurile care sunt mai
probabil a fi referite (accesate) din nou, în cache-ul principal, iar cele cu un
viitor mai puțin “roz”, în victim cache.
La referirea unui cache mapat direct, victim cache-ul este adresat în paralel;
dacă rezultă miss în cache-ul principal, dar hit în victim cache, data este extrasă
din victim cache. Penalitatea pentru miss în cache-ul principal, în acest caz este
mult mai redusă decât ar fi fost costul unui acces la nivelul următor de memorie.
Algoritmul de victim cache încearcă să izoleze blocurile conflictuale şi să le
memoreze, unul în cache-ul principal, iar celălalt în victim cache. Dacă numărul
blocurilor conflictuale este suficient de mic, încât acestea să fie memorate în
victim cache, atât rata de miss în nivelul următor de memorie cât şi timpul
mediu de acces, vor fi îmbunătăţite, datorită penalităţii de acces mai reduse,
implicate de prezenţa blocurilor în victim cache. Cache-ul mapat direct creşte cu

69
un bloc, pentru a implementa conceptul de selective victim cache. Acest bloc
adiţional se numeşte bloc tranzitoriu şi este necesar pentru două motive
importante. Primul, ar fi acela că blocul tranzitoriu este folosit de algoritmul de
predicţie pentru referiri secvenţiale într-un acelaşi bloc, în baza principiului
statistic de vecinătate spațială. Hardware-ul este capabil să determine accese
secvenţiale, folosind semnalul numit “Acces Secvenţial” activat de CPU, când
referirea curentă se face în acelaşi bloc ca şi cel anterior. Semnalul este folosit
de către cache pentru a evita actualizarea biţilor de stare folosiţi de algoritmul de
predicţie la referinţe repetate în acelaşi bloc tranzitoriu. Al doilea motiv constă
în faptul că, atunci când are loc un hit în victim cache şi algoritmul de predicţie
decide să nu se interschimbe blocurile, blocul corespondent este copiat din
victim cache în blocul tranzitoriu. Astfel, blocul tranzitoriu serveşte pe post de
buffer tranzitoriu, accesele secvenţiale la acel bloc fiind satisfăcute direct din
acest buffer, la timpul de acces al cache-ului principal (un avantaj). Similar, la
un miss în următorul nivel de memorie, algoritmul de predicţie va decide să
plaseze blocul sosit în victim cache dar şi în blocul tranzitoriu, în baza
principiului statistic spatial locality.
Întrucât un al doilea sau un al n-lea (n>2) acces consecutiv în acelaşi bloc
în cache-ul principal poate fi servit din blocul tranzitoriu, acestuia îi este adăugat
un bit de stare pentru a adresa cache-ul principal. Acest bit de stare urmăreşte
starea datei din blocul tranzitoriu. Când starea este normală, adresa sosită pe bus
este decodificată pentru a accesa cache-ul principal în mod obişnuit; când starea
este specială, accesul se face în blocul tranzitoriu. Figura următoare arată
tranziţiile dintre cele două stări. Mai jos se prezintă acest algoritm sub formă de
“maşină secvenţială de stare” (automat finit sincron).

Figura 2.6. Maşina secvenţială de stare SVC şi tranziţiile ei

70
Iniţial starea automatului este resetată în starea normală. Dacă apare un
miss în cache-ul mapat direct, acesta este servit fie de victim cache, fie de
nivelul următor de memorie. În fiecare din aceste cazuri, algoritmul de predicţie
este folosit pentru a determina care bloc urmează a fi memorat în cache-ul
principal. Dacă algoritmul de predicţie plasează blocul accesat în cache-ul
principal, starea automatului rămâne în starea normală. Altfel, blocul este copiat
în blocul tranzitoriu din acest cache şi automatul tranzitează în starea numită
specială. Referirea secvenţială a aceluiaşi bloc păstrează semnalul “Acces
Secvenţial” activat, iar automatul în starea specială. Datele se extrag în acest caz
din blocul tranzitoriu. Primul acces nesecvenţial resetează starea maşinii în
starea normală, distingându-se trei cazuri distincte, pe care le vom discuta mai
jos.
Algoritmul Selective Victim Cache
1. Hit în cache-ul principal: dacă cuvântul este găsit în cache-ul principal, el
este extras de aici de către CPU. Nu este nicio diferenţă faţă de cazul cache-
ului mapat direct. Singura operaţie suplimentară constă într-o posibilă
actualizare a biţilor de stare folosiţi de schema de predicţie. Actualizarea se
poate face în paralel cu operaţia de fetch (aducere) şi nu introduce întârzieri
suplimentare.
2. Miss în cache-ul principal, hit în selective victim cache: în acest caz,
cuvântul este extras din victim cache de către CPU. Un algoritm de predicţie
este invocat pentru a determina dacă va avea loc o interschimbare între blocul
referit şi blocul conflictual din cache-ul principal. Dacă algoritmul decide că
blocul din victim cache este mai probabil să fie referit din nou decât blocul
conflictual din cache-ul principal, se realizează interschimbarea; în caz
contrar, blocul din victim cache este doar copiat în blocul tranzitoriu al
cache-ului principal (pentru a profita de viitoarele – probabile – accese
secvențiale în el) iar maşină secvenţială de stare trece în starea specială. În
ambele cazuri, blocul din victim cache este marcat drept cel mai recent
folosit din lista LRU. În plus, biţii de predicţie sunt actualizaţi, pentru a
reflecta istoria acceselor.
3. Miss atât în cache-ul principal cât şi în victim cache: dacă cuvântul nu este
găsit nici în cache-ul principal și nici în victim cache, el trebuie extras din
nivelul următor al ierarhiei de memorie. Aceasta înseamnă că noul bloc
accesat de CPU este în conflict cu un alt bloc, memorat în cache-ul principal.
În acest caz, trebuie aplicat algoritmul de predicţie pentru a determina care
din blocuri este mai probabil să fie referit pe viitor. Dacă blocul care soseşte

71
din memoria principală are o probabilitate mai mare decât blocul conflictual
din cache-ul principal, ultimul este mutat în victim cache iar noul bloc îi ia
locul în cache; altfel, blocul sosit este direcţionat spre victim cache şi copiat
în blocul tranzitoriu al cache-ului mapat direct, de unde poate fi accesat mai
avantajos de către CPU. Maşina secvenţială de stare trece în starea specială,
iar biţii de predicţie sunt actualizaţi.
Diferenţa de esenţă dintre schema prezentată (selective victim cache) şi
conceptul de victim cache simplu se observă în cazurile 2 şi 3. În cazul 2,
blocurile conflictuale din cache-ul principal şi din victim cache sunt întotdeauna
interschimbate în cazul folosirii victim cache-ului tradiţional, pe când schema
îmbunătățită (SVC) face această interschimbare într-un mod selectiv, inteligent,
pe baze euristice. Similar, în cazul 3, prin folosirea victim cache-ului obişnuit,
blocurile din memorie sunt întotdeauna plasate în cache-ul principal, pe cînd în
cazul Selective Victim Cache-ului, se plasează aceste blocuri, în mod selectiv, în
cache-ul principal sau în victim cache. Orice algoritm de înlocuire poate fi
folosit pentru victim cache. LRU (cel mai puţin recent referit) pare să fie cea mai
bună alegere, întrucât scopul victim cache-ului este de a captura cele mai multe
“victime” recent înlocuite, iar victim cache-ul este de dimensiune redusă.
Algoritmul de predicţie
Scopul algoritmului dinamic de predicţie este de a determina care din cele
două blocuri conflictuale este mai probabil să fie referit pe viitor. Blocul
considerat cu o probabilitate mai mare de acces în viitor, este plasat în cache-ul
principal (cel mai avantajos plasament), celălalt fiind plasat în victim cache.
Astfel, dacă blocul din victim cache este pe viitor înlocuit datorită capacităţii
reduse a victim cache-ului, impactul ar fi mai puţin sever decât alegerea opusă
(interschimbarea permanentă a blocurilor, din cazul schemei cu victim cache
obişnuit).
Algoritmul de predicţie se bazează pe algoritmul de excludere dinamică
propus de McFarling. În principiu, acesta folosește două informații de stare
aferente blocului. Prima arată dacă, din momentul în care acesta a fost evacuat
din cache-ul cu mapare directă, blocul respectiv a fost accesat de către CPU sau
nu (așadar o măsură a utilității sale). A 2-a informație de stare arată, dacă atunci
când a fost memorat ultima dată în cache-ul cu mapare directă, blocul respectiv
a fost conflictual (interferent) sau nu. De remarcat că aceste informații de stare
sunt independente, ortogonale. Agregate, ele pot da o predicție rezonabilă asupra
utilității viitoare a respectivului bloc. Evident că un bloc care s-a dovedit
folositor și neconflictual, va fi predicționat ca fiind deosebit de valoros și, în

72
consecință, memorat în cache-ul cu mapare directă. În scopul predicției utilității
blocului accesat, algoritmul foloseşte doi biţi de stare asociaţi, numiţi hit bit şi
sticky bit. Hit bit este asociat logic cu blocul din nivelul 1 al cache-ului (L1 -
level one cache), care se află momentan pe nivelul 2 (L2-cache) sau în memoria
centrală. Hit bit egal cu 1 logic indică faptul că a avut cel puţin un acces cu hit la
blocul respectiv, de cînd el a părăsit cache-ul principal (cache-ul de pe nivelul
L1). Hit bit egal cu 0 înseamnă că blocul corespunzător nu a fost deloc accesat
de când a fost înlocuit din cache-ul principal. Într-o implementare ideală, biţii de
hit sunt menţinuţi în nivelul L2 de cache sau chiar în memoria principală şi aduşi
în nivelul L1 de cache odată cu blocul corespondent. Dacă blocul este înlocuit
din cache-ul principal (L1 cache), starea bitului de hit trebuie actualizată în L2
cache sau în memoria principală. Când un bloc, să-l numim α, a fost adus în
cache-ul principal, bitul său sticky este setat. Fiecare secvenţă cu hit la blocul α,
reîmprospătează bitul sticky la valoarea 1. La referirea unui bloc conflictual, fie
acesta β, dacă algoritmul de predicţie decide ca blocul să nu fie înlocuit din
cache-ul principal, atunci bitul sticky este resetat (a devenit, pentru prima dată,
conflictual!). Dacă un acces ulterior în cache-ul principal intră din nou în
conflict cu blocul care are bitul sticky resetat, atunci blocul va fi înlocuit din
cache-ul principal. De aceea, sticky bit de valoare 1 pentru blocul α semnifică
faptul că nu a avut loc nicio referire la un bloc conflictual cu α, de la ultima
referire a acestuia.
Acum este mai uşor de înţeles rolul blocului tranzitoriu în algoritmul de
predicţie. Dacă algoritmul tratează toate fetch-urile în acelaşi fel, accesele
secvenţiale în acelaşi bloc vor seta întotdeauna bitul sticky. Algoritmul de
predicţie va fi incapabil să determine dacă blocul a fost referit repetat în
interiorul unei bucle, sau dacă mai mult decât un cuvânt din acelaşi bloc a fost
extras din cache, fără o referinţă intervenită la un alt bloc.

73
Figura 2.7. Algoritmul Selective Victim Cache
În algoritmul Selective Victim Cache prezentat în figura anterioară, se
disting trei cazuri: în primul caz, un hit în cache-ul principal setează biţii de
stare hit şi sticky. În al doilea caz, blocul accesat, fie acesta β, se consideră
rezident în victim cache. Acest caz implică un conflict de acces între blocul β şi
cel din cache-ul principal, notat cu α. În acest caz, algoritmul de predicţie este
aplicat pentru a determina dacă va avea loc o interschimbare, sau nu. Dacă bitul
sticky al blocului α este 0, semnificând faptul că blocul nu a fost accesat de la
conflictul anterior la acest bloc, noul bloc β primeşte o prioritate superioară lui
α, determinând o interschimbare. De asemenea, dacă bitul hit al lui β este setat
pe 1, acestuia îi este dată o prioritate mai mare decât lui α şi ele sunt
interschimbate. Dacă bitul sticky al lui α este 1 şi bitul hit al lui β este 0, accesul
este satisfăcut din victim cache şi nu are loc nicio interschimbare (se consideră

74
că blocul β nu este suficient de “valoros” pentru a fi adus în cache-ul principal).
Bitul sticky aferent lui α este resetat astfel încât o secvenţă următoare care
implică conflict la acest bloc va determina mutarea lui α din cache-ul principal.
În final, cazul 3 al algoritmului prezintă secvenţa de acţiuni care au loc în cazul
unor accese cu miss atât în cache-ul principal, cât şi în victim cache. Secvenţa
este similară cu cea de la cazul 2, cu excepţia faptului că, destinaţia blocului
sosit se alege fie cache-ul principal, fie selective victim cache-ul. În situaţia cu
victim cache simplu, blocul conflictual din cache-ul principal era mutat în victim
cache, înainte să fie înlocuit. În cazul de faţă, când blocul sosit este plasat în
selective victim cache, el este de asemenea plasat şi în blocul tranzitoriu, pentru
a servi eventualele viitoare referinţe secvenţiale în cadrul acestui bloc.
Operaţiile algoritmului de Selective Victim Cache pot fi ilustrate printr-o
secvenţă de instrucţiuni repetate (αmβγ)n, implicând trei blocuri conflictuale α, β
şi γ. Notaţia (αmβγ)n reprezintă execuţia unei secvenţe compusă din două bucle
de program imbricate, bucla interioară constând în m referinţe la blocul α,
urmate de accesul la blocurile β şi γ în bucla exterioară, care se execută de n ori.
Primul acces îl aduce pe α în cache-ul principal şi atât bitul hit cât şi cel sticky
sunt setaţi după cel mult două referiri ale acestuia. Când β este referit, bitul său
hit este iniţial 0. De aceea, el nu-l înlocuieşte pe α în cache-ul principal şi este
memorat în victim cache. Conflictul generat determină resetarea bitului sticky al
lui α. Când γ este referit, bitul său hit este 0, dar bitul sticky al lui α este tot 0.
Așadar, γ îl înlocuieşte pe α. Blocul α este transferat în victim cache şi bitul său
hit rămâne pe 1, datorită referinţei sale anterioare. În ciclul următor, când α este
referit din nou, el este mutat înapoi în cache-ul principal datorită bitului său de
hit, rămas setat. Astfel, dacă victim cache-ul este suficient de mare pentru a
putea memora atât α şi β, sau β şi γ, doar trei referinţe ar fi servite de către al
doilea nivel de cache. Numărul total de interschimbări nu va depăşi 2n. În cazul
unei scheme simple de predicţie fără victim cache, numărul total de referiri cu
miss ar fi 2n, în cazul în care schema poate rezolva doar conflicte între două
blocuri. Un victim cache simplu, fără predicţie, ar fi capabil să reducă numărul
de accese cu miss la cel de-al doilea nivel de cache la 3, dar ar necesita 3n
interschimbări în timpul execuţiei buclei exterioare, cu influenţe evident
defavorabile asupra timpului global de procesare. Acest fapt arată avantajul
Selective Victim Cache-ului, care este superior altor scheme care tratează
conflicte implicând mai mult de două blocuri. De reţinut că, penalitatea pentru o
predicţie greşită în această schemă este limitată la accesul în victim cache şi o
posibilă interschimbare, presupunând că victim cache-ul este suficient de mare

75
pentru a reţine blocurile conflictuale între accese. Schematic, acest exemplu
poate fi sintetizat ca mai jos.
Exemplu: (α αmβγ)n implicând trei blocuri conflictuale în memoria cache
principală: α, β şi γ.

VC (α α, γ) de la a 2-a iterație, (β
β, α) a 2-a iterație, (γγ, β) a 2-a iterație 3n
interschimbări

SVC (γγ, α) prima iterație, (α


α, γ) a 2-a iterație, (γγ, α) a 2-a iterație etc.
maximum 2n interschimbări

În continuare prezentăm, preluat din lucrarea noastră [Arm98], câteva


rezultate semnificative obținute prin modelarea și simularea software a unui
(selective) victim cache integrat într-o arhitectura pipeline superscalară
parametrizabilă.

cache = 512 words , victim = 8 blocks


2,5 No victim

2 Simple Victim
Issue Selective Victim
1,5
Rate
1

0,5

0
AM
queens

tree
sort
perm
bubble

matrix

tower
puzzle

HM

Figura 2.7.1. Rata medie de execuție a instrucțiunilor pe 3 configurații

76
cache = 256 words, victim = 8 blocks
60000
Simple Victim
50000
Number of Selective Victim
40000
interchanges
between 30000
main cache
20000
and
victim cache 10000

AM
queens

sort
bubble

tree
tower
perm

HM
matrix

Figura 2.7.2. Numărul de interschimbări între cache și (S)VC puzzle

cache = 1Kwords , victim = 16 blocks


100 No victim
90
80 Simple Victim
HIT 70
Selective Victim
global 60
cache 50
40
30
20
10
0
queens
bubble

sort
matrix

puzzle

tower
perm

AM

HM
tree

Figura 2.7.3. Rata de hit în sub-sistemul de cache, pe 3 configurații

Figura 2.7.1 arată rata medie de procesare a instrucțiunilor (numărul mediu


de instrucțiuni/ciclu), măsurată pe benchmarkurile Stanford C (J. Hennessy),
compilate pentru procesorul HSA (Hatfield Superscalar Architecture) [Flo03],
într-un CPU fără victim cache/cu victim cache (VC)/cu selective victim cache
(SVC). Se observă faptul că performanța globală crește corespunzător, cu VC
respectiv cu SVC adiționale. Precizăm că în această figură am agregat
performanțele individuale per benchmark, utilizând mediile aritmetică (AM)
respectiv armonică (HM) ale performanțelor Pk individuale,
N

∑ Pk N
AM= k =1
≥ HM = N
. Figura 2.7.2 explică creșterea de performanță
N 1

k =1 Pk

77
globală prin faptul că arhitectura SVC reduce semnificativ numărul de
interschimbări între cache-ul principal cu mapare directă și SVC, față de un VC
simplu. În fine, Figura 2.7.3 arată că SVC nu îmbunătățește rata de hit în cache
față de un VC simplu.

Notă. Deviația statistică standard reprezintă o măsură a împrăștierii


datelor față de media valorilor lor și este definită ca:
n

∑(X i − X mediu ) 2
s( X ) = i =1
.
n −1
Alternativ, se folosește varianța unui set de date X, ca fiind:
n

∑(X i − X mediu ) 2
Var ( X ) = i =1
, practic echivalentă cu deviația standard. Spre exemplu,
n −1
ar fi util ca, pe baza valorilor date în figurile anterioare, să se calculeze cele două
varianțe – Var(VC) și Var(SVC) – aferente datelor obținute cu VC respectiv cu
SVC.
Între două seturi de date X și Y având același cardinal (n) este util calculul
covarianței statistice, definită astfel:
n

∑(X i − X mediu )(Yi − Ymediu )


Co var( X , Y ) = i =1

n −1
Nu are mare importanță valoarea acesteia, ci semnul. Dacă acesta este
pozitiv, semnifică faptul că cele două seturi de date cresc sau descresc împreună,
oarecum sincronizat, într-o corelație pozitivă. Invers, dacă semnul covarianței
este negativ, arată că în timp ce un set de date crește, celălalt descrește (corelație
negativă). În fine, dacă această covarianță este zero, înseamnă că cele două
seturi de date sunt independente mutual. Este evident că operatorul de covarianță
este comutativ. Spre exemplu, ar fi util ca, pe baza valorilor date în figurile
anterioare, să se calculeze covarianța Covar(VC, SVC) dintre datele obținute cu
VC respectiv cu SVC.
Un indicator statistic care agreghează (combină) cei doi indicatori
anteriori, în vederea normalizării, este așa numita corelație Pearson, definită ca
fiind:

n n

Co var( X , Y )
∑ ( X i − X mediu )(Yi − Ymediu ) ∑a b i i
R( X , Y ) = = i =1
= i =1

s ( X ) s (Y ) n n n n

∑(X − X mediu ) 2 ∑ (Y − Ymediu ) 2 ∑a ∑b


2 2
i i i i
i =1 i =1 i =1 i =1

78
S-a notat ai = X i − X mediu și bi = Yi − Ymediu .

Vom arăta că R( X , Y ) ∈ [−1,1]. În acest scop considerăm ecuația:

(a1x+b1)2+(a2x+b2)2+… +(anx+bn)2=0.

Este evident că determinantul acestei ecuații (D) este D ≤ 0. (Are rădăcini


complexe sau una reală, dublă.) Acest fapt este echivalent cu inegalitatea:

n n n
(∑ ai bi ) 2 ≤ ∑ ai ∑b
2 2
i (inegalitatea Cauchy-Buniakovski-Schwarz), adică
i =1 i =1 i =1
n

∑a b
i =1
i i
echivalent cu relația: ∈ [−1,1] , q.e.d.
n n

∑a ∑b
2 2
i i
i =1 i =1

Rezultă că dacă R(X,Y)=1 există o corelație pozitivă între vectorii X și Y,


adică există o relație liniară între X și Y astfel încât Y crește pe măsură ce X
crește. Dacă R(X,Y)= -1 există o corelație negativă între X și Y, adică există o
relație liniară între X și Y astfel încât Y crește pe măsură ce X descrește.

Metrici de performanţă utilizate în evaluarea memoriilor cache


Metricile de performanţă cele mai folosite sunt rata de miss la nivelul L1 de
cache şi timpul mediu de acces la ierarhia de memorie. În cazurile cu victim
cache simplu şi cel cu victim cache selectiv, folosim, de asemenea, şi numărul
de interschimbări între cache-ul principal şi victim cache, ca metrică de
comparaţie. Rata de miss la nivelul L1 de cache se bazează pe numărul de
referinţe propagate în nivelul următor al ierarhiei de memorie. În caz de miss în
cache-ul principal, acestea sunt servite de către victim cache, fiind plătită o
penalitate de timp pentru accesul în victim cache precum şi pentru eventualele
interschimbări de blocuri rezultate între cache-ul principal şi victim cache.
Timpul mediu de acces la ierarhia de memorie ia în calcul şi aceste penalizări şi,
de aceea, este un bun indicator al performanţei memoriei sistemului, desigur mai
complet decât rata de miss în nivelul L1 de cache. Deoarece obiectivul principal
al victim cache-ului este de a reduce numărul de miss-uri de conflict în cache-ul
mapat direct, este de asemenea important să comparăm procentajul de miss-uri
de conflict eliminate prin fiecare din scheme. Miss-urile de conflict sunt de

79
obicei calculate ca miss-uri suplimentare ale unui cache, comparate cu un cache
complet asociativ de aceeaşi mărime şi care dezvoltă un acelaşi algoritm de
înlocuire. Algoritmul folosit este LRU (Least Recently Used, cel mai de demult
nefolosit) sau variaţiuni ale acestuia.
Modelarea timpului de acces la ierarhia de memorie
Estimarea timpului de acces se face ca o funcţie de mărimea cache-ului,
dimensiunea blocului, asociativitatea şi organizarea fizică a cache-ului.
Presupunem că penalitatea medie pentru un miss în cache-ul de pe nivelul L1
este aceeaşi pentru toate arhitecturile şi este de p ori ciclul de bază al cache-lui
principal, unde p variază între 1 şi 100. Considerăm un bloc având dimensiunea
de 32 de octeţi, penalitate în caz de miss de 10-50 cicli, în caz că nu există un
nivel L2 de cache. Câteva studii, raportează că penalitatea pentru un miss poate
fi până la 100-200 de cicli, când nu este inclus un al doilea nivel de cache.

Parametri Cache Victim Selective 2-way


Mapat Cache Victim cache
Direct Simplu Cache
Referinţe totale R
Număr total de miss-uri în Md Mv Ms M2
L1 cache
Hit-uri în victim cache hv hs
Interschimbări între victim Iv Is
cache şi cache-ul principal
Timp mediu de acces Td Tv Ts T2
Timp mediu de penalizare p
(în cicli CPU)
Perioada de tact CPU clk clk2-
way
Tabelul 2.4. Notaţiile folosite în calculul timpului de acces

Tabelul 2.4. rezumă toate notaţiile privitoare la calculul timpului de acces


la memorie. R reprezintă numărul total de referinţe generate de programele de
tip trace (un fișier care conține toate instrucțiunile dinamice executate de
procesor, în ordinea execuției acestora). În cazul cache-ului simplu, cu mapare
directă, Md reprezintă numărul total de accese cu miss în cache. În cazul folosirii
unui victim cache obişnuit sau a unui Selective Victim Cache, Mv şi Ms sunt
folosite pentru a nota numărul de accese cu miss în primul nivel de cache, care

80
sunt servite de al doilea nivel al ierarhiei de memorie. Numărul total de hituri în
victim cache pentru aceste scheme le-am notat cu hv şi respectiv hs.
Timpul mediu de acces pentru un cache mapat direct se calculează astfel:

R + p× Md  M 
Td = clk × = clk1 + p ⋅ d 
R  R 

Așadar, pentru fiecare miss, sunt necesari p cicli suplimentari de acces.


Presupunem că cei p cicli includ toate “cheltuielile” CPU-ului. În cazul victim
cache-ului simplu, o penalitate de p cicli este produsă la fiecare miss în primul
nivel al ierarhiei de memorii cache. În plus, pentru fiecare referinţă servită de
către victim cache, o penalizare suplimentară de cel puţin un ciclu este plătită
pentru accesarea în victim cache, iar operaţia de interschimbare dintre cache-ul
principal şi victim cache necesită o penalitate de câţiva cicli (presupunem doar 3
cicli, de altfel minimali). Această penalitate ar putea fi chiar mai mare dacă
matricea memoriei cache-ului mapat direct sau a victim cache-ului este
organizată fizic în cuvinte egale cu o fracţiune din mărimea blocului de cache.
Spre exemplu, blocul poate avea dimensiunea de 32 de octeţi, dar cache-ul poate
fi organizat în cuvinte de 8 sau 16 octeţi. În acest caz penalitatea pentru
interschimbare va fi multiplicată cu raportul:

Dimensiunea blocului de cache


Mãrimea cuvântului de date al cache - ului

Astfel, timpul mediu de acces la memorie pentru un sistem cu victim cache


simplu, se calculează utilizând relația:

 M h + 3× Iv 
Tv = clk1 + p ⋅ v + v 
 R R 

Într-un sistem cu Selective Victim Cache, timpul mediu de acces la


memorie poate fi calculat în acelaşi fel ca în cazul victim cache-ului simplu. O
penalitate de p cicli este aplicată ori de câte ori este accesat nivelul următor al
ierarhiei de memorie. Un ciclu suplimentar este necesar la un hit în victim cache
şi 3 cicli suplimentari pentru operaţia de interschimbare de blocuri. Timpul
mediu de acces la memorie este dat de formula:

81
 M h + 3 × Is 
Ts = clk1 + p ⋅ s + s 
 R R 

Se observă că, chiar dacă rata de miss Ms este foarte aproape de cea a
victim cache-ului simplu, sistemele ce folosesc selective victim cache pot totuşi
oferi o îmbunătăţire substanţială a performanţei, superioară sistemelor cu victim
cache simplu, din următoarele două motive principale:
1. Rata de miss locală în cache-ul principal poate fi îmbunătăţită printr-un
plasament mai adecvat al blocurilor în acesta.
2. Numărul de interschimbări poate descreşte ca rezultat al algoritmului
adaptiv de predicţie. Aceasta reduce media penalizării pentru accesele
care sunt servite de victime cache, în special atunci când numărul de cicli
folosiţi la o interschimbare este ridicat.
În fond, un sistem de calcul având un cache cu mapare directă de capacitate
N cuvinte (intrări) și, asociat, un (selective) victim cache de capacitate K cuvinte
(N > K), încearcă să aproximeze o memorie cache de tip K-way set associative,
de capacitate N*K cuvinte. O comparație echitabilă ar trebui deci să evalueze o
memorie cache de tip K-way set associative, de capacitate N cuvinte (deci N
divizibil cu K), cu o memorie cache cu mapare directă, de capacitate N cuvinte,
având asociat un (selective) victim cache de capacitate K cuvinte. Personal nu
am văzut în literatura de specialitate o comparație cantitativă și calitativă a
acestor două subsisteme - unul ideal și altul aproximat - de memorii cache. În
schimb, autorii Stiliadis și Varma au utilizat timpul mediu de acces la memorie
pentru un sistem cu un cache de tip “2-way associative”, ca o referinţă pentru
evaluarea performanţei sub-sistemului cu selective victim cache. Pentru
estimarea timpului de acces la un cache cache “2-way associative”, se presupune
că penalitatea (în nanosecunde) pentru al doilea nivel al ierarhiei de memorie
rămâne aceeaşi ca şi în cazul cache-ului mapat direct. Pot exista unele
constrângeri de implementare care afectează penalitatea în caz de miss.
Accesarea magistralei sistem, poate implica o secvenţă de operaţii care necesită
un număr fix de perioade de tact. Astfel, numărul de cicli necesari pentru
deservirea unui miss nu poate descreşte proporţional cu creşterea perioadei de
tact a CPU, rezultând într-o penalizare mai mare în cazul cache-ului de tip “2-
way associative”. Timpul mediu de acces la memorie al acestui cache este
estimat de relaţia:

82
 clk 2-way M 
T2 = clk  + p⋅ 2 
 clk R 

Primul termen reprezintă timpul de acces la cache-ul principal, iar al doilea


termen este timpul de acces la nivelul următor de memorie (cache). Comparând
această ecuaţie cu prima (care exprima timpul mediu de acces pentru un cache
mapat direct), orice îmbunătăţire a performanţei se datorează celui de-al doilea
termen, în timp ce primul termen reprezintă câştigul introdus prin asociativitatea
cache-ului, asupra timpului de acces. Dacă îmbunătăţirea datorată celui de-al
doilea termen nu este adecvată, pentru a compensa acest câştig, performanţa
cache-ului asociativ cu două căi poate fi inferioară celei a cache-ului mapat
direct.
Îmbunătăţirea în performanţă obţinută atât prin victim cache cât şi prin
selective victim cache variază în funcţie de trace-ul (benchmark-ul) folosit în
simulare/evaluare, depinzând de mărimea capacității acestor memorii rapide
precum şi de numărul de conflicte de acces pe care schema de predicţie le
elimină. Chiar pentru programe mici, selective victim cache asigură o
îmbunătăţire semnificativă, comparativ cu victim cache-ul simplu, atunci când
cache-ul nu este suficient de mare pentru a memora întreg programul. Stiliadis şi
Varma afirmă că cea mai bună îmbunătăţire a performanţei, în termenii ratei de
miss, de aproximativ 33%, este obţinută pentru cache-uri de instrucţiuni de 8
până la 16 Kocteţi. Pentru cache-uri mai mari, de dimensiuni de 64 până la 128
Kocteţi, majoritatea trace-urilor pot fi uşor memorate în cache iar miss-urile de
conflict reprezintă un mic procentaj din numărul total de accese cu miss. În
aceste cazuri, victim cache-ul simplu este capabil să elimine majoritatea
conflictelor şi performanţa sa este comparabilă cu cea a selective victim cache-
ului.
O problemă conexă cu algoritmul de predicţie dezvoltat în selective victim
cache este aceea că performanţa sa s-ar putea degrada odată cu creşterea
dimensiunii blocului, ca rezultat al partajării biţilor de stare de cuvinte din
interiorul aceluiaşi bloc. În realitate, s-a arătat că Selective Victim Cache asigură
o îmbunătăţire semnificativă a ratei de miss, indiferent de dimensiunea blocului.
Spre exemplu, pentru blocuri de dimensiune de 4 octeţi, selective victim cache
reduce rata de miss cu aproximativ 30% faţă de o arhitectură cache cu mapare
directă, în timp ce pentru blocuri de dimensiuni de 64 octeţi, rata de miss este
redusă cu aproape 50%. Aceste rezultate contrazic comportamentul excluziunii
dinamice, unde reducerea ratei de miss scade cu creşterea dimensiunii blocului.

83
Rezultatele se datorează menţinerii la aceeaşi dimensiune a victim cache-ului în
termenii numărului de blocuri, astfel că, o creştere a dimensiunii blocului
determină o creştere efectivă a capacităţii cache-ului. Această creştere în
capacitate compensează mai mult decât orice degradare a ratei de predicţie, prin
creşterea dimensiunii blocului. Acest fapt nu creşte semnificativ complexitatea
implementării victim cache-ului, deoarece asociativitatea rămâne aceeaşi.
Indiferent de mărimea cache-ului, numărul de interschimbări prin folosirea
selective victim cache-ului este redus cu 50% sau chiar mai mult, faţă de
folosirea unui victim cache simplu. Când dimensiunea blocului este mai mare, în
funcţie de implementare, operaţia de interschimbare poate necesita câţiva cicli.
Prin îmbunătăţirea atât a ratei de hit cât şi a numărului de interschimbări,
selective victim cache-ul poate creşte semnificativ performanţa primului nivel de
cache, superioară victim cache-ului simplu dar şi cache-ului “two-way set
associative”. Pentru diverse dimensiuni de cache principal, îmbunătăţirea ratei
de miss la cache-ul two-way semiasociativ nu este suficientă pentru a compensa
creşterea timpului de acces, rezultând într-o creştere netă a timpului mediu de
acces la memorie, superior celui aferent cache-urilor mapate direct. Cea mai
mare creştere în performanţă a selective victim cache-ului, superioară cache-ului
semiasociativ, este de aproximativ 25%, obţinută pentru dimensiuni de cache-uri
de 16-64 Kocteţi.
Politica de scriere implementată este write back cu write allocate (blocul
este încărcat în cache la o scriere cu miss, urmată de un proces de scriere a
aceluiași bloc, evident, de astă dată cu hit în cache – write back; alternativ, la o
scriere cu miss în cache se poate scrie data doar în memoria principală, fără
încărcarea blocului scris în cache – no write allocate). Pentru a menţine
proprietatea de incluziune multinivel, blocurile din cache-ul de pe nivelul L1 au
fost invalidate când au fost înlocuite pe nivelul L2 de cache. Deşi selective
victim cache-ul produce îmbunătăţiri semnificative ale ratei de hit, comparativ
cu cache-urile mapate direct de dimensiune redusă, performanţa sa este
inferioară celei obţinute folosind victim cache simplu. De fapt, îmbunătăţirile
ratei de miss variază semnificativ în funcţie de trace-urile folosite ca benchmark-
uri. Programele care implică o alocare statică a datelor şi structurilor de date
arată o îmbunătăţire cu selective victim cache, ca rezultat al folosirii
algoritmului de predicţie. Structurile de date principale ale acestor programe
sunt vectori. Algoritmul de predicţie este capabil să rezolve un număr mare de
conflicte în aceste cazuri, fără să acceseze al doilea nivel. În programele cu
alocare dinamică a memoriei şi folosire a extensiei de pointeri, conflictele sunt

84
mai greu de rezolvat de către algoritmul de predicţie. Ratele de miss, în situaţia
folosirii selective victim cache-ului pentru aceste trace-uri, sunt mai mari decât
în cazul folosirii unui simplu victim cache. În timp ce pentru selective victim
cache presupunem un al doilea nivel de cache şi menţinem proprietatea de
incluziune, simularea victim cache-ului simplu presupune că al doilea nivel al
ierarhiei este memoria principală.
Chiar dacă îmbunătăţirile asupra ratei de miss sunt mai puţin convingătoare
în cazul cache-urilor de date comparativ cu cele de instrucţiuni, selective victim
cache poate reduce timpul mediu de acces la memorie pentru primul nivel al
cache-ului de date, prin reducerea numărului de interschimbări. Pentru cache-uri
de dimensiuni până la 64 Kocteţi, numărul de interschimbări pentru selective
victim cache este mult mai mic decât cel pentru victim cache simplu. În câteva
cazuri îmbunătăţirea este mai mare de 50%. Numărul de interschimbări pentru
un victim cache simplu descreşte sub selective victim cache, pentru dimensiuni
mai mari sau egale cu 128 Kocteţi. Astfel, pentru cache-uri de 128 Kocteţi,
selective victim cache este capabil să reducă rata de miss prin creşterea
numărului de interschimbări. Performanţa cache-ului semiasociativ este
inferioară ambelor (simplu victim cache şi selective victim cache), dar
superioară cache-ului mapat direct.
Trace-urile de date sunt caracterizate în general de o rată de miss mai mare
decât trace-urile de instrucţiuni. În plus miss-urile de conflict sunt răspunzătoare
de procentajul ridicat din rata totală de miss. Sunt două consecinţe ale acestui
fapt: prima, efectul reducerii ratei de miss asupra timpului de acces la memorie
este mai pronunţat, iar a doua, cache-urile semiasociative pe două căi asigură
îmbunătăţiri ale timpului mediu de acces, chiar şi pentru cache-uri mari, spre
deosebire de cache-urile de instrucţiuni, unde avantajul obţinut prin reducerea
ratei de miss datorată creşterii asociativităţii este mai mare decât câştigul obţinut
asupra timpului de acces la cache. Concluzionăm că, atât victim cache-ul simplu
cât şi selective victim cache-ul sunt mai puţin atractive pentru folosire în cache-
ul de date, comparativ cu cel de instrucţiuni.
În continuare se analizează modul în care informaţiile de stare de care are
nevoie schema de predicţie dezvoltată în selective victim cache, pot fi stocate în
interiorul ierarhiei de memorie. După cum s-a arătat, schema selective victim
cache-ului necesită doi biţi de stare pentru a păstra informaţii despre istoria
blocurilor din cache - bitul sticky şi bitul hit. Bitul sticky este asociat logic cu
blocul din cache-ul principal. De aceea, este normal să se memoreze acest bit în
cache-ul mapat direct, ca parte a fiecărui bloc. Pe de altă parte, bitul hit este

85
asociat logic cu fiecare bloc din memoria principală. Astfel, într-o implementare
perfectă, biţii de hit ar trebui memoraţi în blocurile din memoria principală.
Această abordare este impracticabilă în majoritatea cazurilor, din motive
evidente.

Figura 2.8. Implementarea schemei de memorare a biţilor de hit


Dacă ierarhia de memorie include un la doilea nivel de cache, este posibil
să se memoreze biţii de hit în cadrul blocurilor din acest nivel. Când un bloc este
adus pe nivelul L1 de cache din nivelul L2, o copie locală a bitului de hit este
memorată în blocul de pe nivelul L1. Aceasta elimină nevoia de acces a
nivelului L2 de cache de fiecare dată când bitul hit este actualizat de către
algoritmul de predicţie. Când blocul este înlocuit din nivelul L1 de cache, bitul
hit corespondent este copiat în nivelul L2. O problemă ar fi însă aceea că,
multiple locaţii din memoria principală sunt forţate să împartă acelaşi bit de pe
nivelul L2. Astfel, când un bloc este înlocuit de pe nivelul L2 de cache, toate
informaţiile lui de stare se pierd, reducând deci eficacitatea algoritmului de
predicţie. De fiecare dată când un bloc este adus pe nivelul L2 de cache din
memoria principală, bitul său de hit trebuie setat la o valoare iniţială. Pentru o
secvenţă specifică de acces, valori iniţiale diferite pot produce rezultate diferite.
Cu cache-urile de pe nivelul L2 de dimensiuni mari, efectul valorilor iniţiale este
probabil mai mic.
O tratare alternativă ar fi aceea de a menţine biţii de hit în interiorul CPU,
doar în cadrul nivelului L1 de cache. În abordarea lui Stiliadis şi Varma, un şir
de biţi de hit, numit hit array, este menţinut ca parte a nivelului L1 de cache.
Fiecare bloc de memorie este asociat unuia din biţii acestui şir. Lungimea şirului

86
este inevitabil și asumat mai mică decât numărul maxim de blocuri care pot fi
adresate. Deci, mai multe blocuri vor fi mapate aceluiaşi bit de hit, cauzând,
datorită interferenţelor potențiale , un aleatorism ce trebuie introdus în procesul
de predicţie. Deşi această implementare poate, teoretic, reduce performanţa
selective victim cache-ului, rezultatele simulărilor nu confirmă acest lucru, chiar
şi pentru şiruri de hit de dimensiune modestă.
Implementarea nivelului L1 de cache sistem este prezentată în Figura 2.8.
Bitul sticky este menţinut cu fiecare bloc în cache-ul principal. Niciun bit de
stare nu este necesar în victim cache. Biţii de hit sunt păstraţi în structura hit
array, adresaţi de o parte a adresei de memorie. Dimensiunea şirului de hit bits
este aleasă ca un multiplu al numărului de blocuri din cache-ul principal, astfel:

Dimensiunea şirului hit array = Număr de blocuri în cache-ul principal ×


H

unde H determină gradul de partajare a biţilor de hit de către blocurile


memoriei principale. Se presupune că H este o putere a lui 2, H=2h. Structura Hit
array poate fi adresată de adresa de bloc, concatenată cu cei mai puţin
semnificativi biţi h, din partea de tag a adresei. O valoare mare pentru H implică
mai puţine interferenţe între blocurile conflictuale la biţii de hit. Dacă H este
ales ca raportul dintre dimensiunea cache-ului de pe nivelul L2 şi cea a cache-
ului de pe nivelul L1 (principal), atunci efectul este similar, cu menţinerea biţilor
de hit în nivelul L2 de cache.
O problemă a implementării schemelor, atât a victim cache-ului cât şi a
selective victim cache-ului, o reprezintă costul implementării victim cache-ului
complet asociativ. Chiar şi atunci când aceste cache-uri sunt foarte mici, costul
hardware al memoriei adresabilă contextual (după conținut), poate fi
semnificativ. Cache-urile complet asociative cu algoritm de înlocuire LRU pot
uneori suferi de o rată de miss mai ridicată decât cache-urile two-way asociative,
deoarece algoritmul de înlocuire nu este cel optimal. Efectul ambelor probleme
de mai sus poate fi diminuat prin reducerea asociativităţii victim cache-ului. Cu
un victim cache semiasociativ pe două căi, nu se observă nicio creştere a ratei de
miss la nivelul următor al ierarhiei de memorie pentru nicio instrucţiune din
trace-urile simulate, atât în victim cache-ul simplu cât şi în selective victim
cache. Poate surprinzător, victim cache-ul semiasociativ pe două căi poate
îmbunătăţi rata de miss şi timpul mediu de acces pentru mai multe trace-uri de
programe. Acest comportament se datorează algoritmului de înlocuire LRU

87
dezvoltat în victim cache-ul complet asociativ. Blocurile mutate în victim cache-
ul complet asociativ, ca rezultat al conflictelor din cache-ul principal, sunt
înlocuite frecvent, înainte de a fi accesate din nou. Victim cache-ul
semiasociativ pe două căi asigură o mai bună izolare pentru blocurile sale în
multe cazuri, micşorând rata de miss în victim cache. În plus, datorită
dimensiunii sale reduse, miss-urile de conflict formează doar o mică fracţiune
din numărul total de accese cu miss în victim cache, comparativ cu miss-urile de
capacitate. Aceasta limitează îmbunătăţirea ratei de miss prin creşterea
asociativităţii victim cache-ului, chiar cu un algoritm “optimal” (OPT) de
înlocuire. Se observă că, victim cache-ul complet asociativ poate îmbunătăţi
semnificativ rata de miss în cazul conflictelor ce implică mai mult de trei
blocuri, blocurile conflictuale fiind reţinute în victim cache între accese.
Cu un victim cache simplu, conţinutul cache-ului principal mapat direct
este neafectat de asociativitatea acestuia. Astfel, rata de miss locală rămâne
practic neschimbată, în timp ce se variază asociativitatea victim cache-ului. Prin
urmare, toate îmbunătăţirile efectuate asupra ratei de miss la nivelul L1 de cache
pot fi atribuite îmbunătăţirii ratei de miss locale a victim cache-ului. Cu victim
cache-ul selectiv, asociativitatea poate afecta potenţial atât rata de miss locală a
cache-ului principal, cât şi numărul de interschimbări dintre cele două cache-uri.
Comparaţia timpului de acces ţine cont de schimbările apărute în rata de miss şi
numărul de interschimbări (substanţial micşorat) şi de aceea timpul mediu de
acces reprezintă o măsură mai bună pentru caracterizarea efectului de
asociativitate al victim cache-ului asupra performanţei sistemului. Chiar cu un
victim cache mapat direct, timpul mediu de acces este mai mare sau egal decât
cel din cazul victim cache-ului complet asociativ. Când folosim un victim cache
de date semiasociativ pe două căi, rezultatele sunt mai modeste decât acelea cu
un victim cache complet asociativ, atât pentru victim cache-ul simplu, cât şi
pentru victim cache-ul selectiv. Acest lucru nu surprinde, dând conflictelor de
acces la date o natură cvasi-aleatorie. Astfel, un cache complet asociativ poate fi
încă atractiv, când este folosit ca şi cache de date. Totuşi, îmbunătăţirile
observate sunt mai modeste.
Chiar dacă nu implică nici o îmbunătăţire a ratei de hit în cache-ul
principal, schema victim cache-ului selectiv poate asigura totuşi o îmbunătăţire a
performanţei, superioară victim cache-ului simplu. Pentru schema cu SVC
rezultatele demonstrează că îmbunătăţirile de performanţă sunt puternic
determinate de impactul algoritmului de predicţie asupra numărului de
interschimbări cu cache-ul mapat direct. Algoritmul poate de asemenea contribui

88
la o mai bună plasare a blocurilor în cache-ulprincipal, reducând numărul de
accese în victim cache şi generând rate de hit mai ridicate în cache-ul mapat
direct.
Folosirea victim cache-ului selectiv determină îmbunătăţiri ale ratei de miss
precum şi ale timpului mediu de acces la memorie, atât pentru cache-uri mici,
cât şi pentru cele mai mari (4Kocteţi - 128 Kocteţi). Simulări făcute pe trace-
urile de instrucţiuni a 10 benchmark-uri din suita SPEC’92 arată o îmbunătăţire
de aproximativ 21% a ratei de miss, superioară folosirii unui victim cache
simplu de 16 Kocteţi cu blocuri de dimensiuni de 32 octeţi; numărul blocurilor
interschimbate între cache-ul principal şi victim cache s-a redus cu aproximativ
70%.

2.2. MEMORIA VIRTUALĂ

Memoria virtuală reprezintă o tehnică de organizare a memoriei, prin


intermediul căreia programatorul “vede” (dispune de) un spaţiu virtual de
adresare de capacitate foarte mare şi care, fără ca programatorul să “simtă” în
mod explicit, este mapat dinamic în memoria fizic disponibilă (memoria
principală). Uzual, spaţiul virtual de adrese corespunde suportului disc magnetic
(memoria secundară), programatorul având impresia, prin mecanismele de
memorie virtuală (MV), că are la dispoziție o memorie unică, de capacitatea
hard-discului şi nu de capacitatea, mult mai redusă, a memoriei principale,
implementată preponderent sub forma de memorie DRAM, în calculatoarele de
uz general (limitată la 2 ÷ 8 Go la ora actuală, în laptopurile uzuale– anul 2016).
De asemenea, MV oferă și funcții de protecție a accesului la memorie, prin
implementarea unor drepturi de acces. În urma comparării acestor drepturi cu
nivelul de privilegiu al programelor aflate în rulare, acestea primesc, sau nu (în
acest ultim caz rezultând o excepție de tip violare de privilegiu), accesul la o
anumită informație stocată în memorie.
În cazul MV, memoria principală este oarecum analoagă memoriei cache
între CPU (Central Processing Unit) şi memoria principală, numai că de această
dată ea se situează, din punct de vedere logic, între CPU şi discul magnetic. Deci
memoria principală (MP) se comportă oarecum analog cu un “cache”, situat
logic între CPU şi discul hard. Prin mecanismele de MV se măreşte
probabilitatea ca informaţia (instrucțiuni și date) ce se doreşte a fi accesată de

89
către CPU din spaţiul virtual (disc), să se afle în MP, reducânduse astfel
dramatic timpul de acces de la 8 ÷ 15 ms, cât este uzual timpul de acces la disc,
la doar 25 ÷ 50 ns (timpul de acces la DRAM) în tehnologiile actuale (2016) !
De obicei, spaţiul virtual de adresare este împărţit în entităţi de capacitate fixă (4
÷ 64 Ko actualmente), numite pagini. O pagină poate fi mapată în MP sau pe
disc.

Figura 2.9. Maparea adreselor virtuale în adrese fizice (capacități la nivelul


anului 2004)

În general, prin mecanismele de MV, MP conţine paginile cel mai recent


accesate de către un programul dinamic (task, proces), ea fiind, după cum am
mai arătat, pe post de “cache” între CPU şi discul magnetic (memoria
secundară). Un task reprezintă un program dinamic căruia sistemul de operare i-
a alocat resurse de memorie (cod, stivă, date) și timp de CPU, în vederea rulării.
Transformarea adresei virtuale, emisă de către CPU, într-o adresă fizică
(existentă în spaţiul MP), se numeşte mapare sau translatare de adrese. Aşadar,
mecanismul de MV oferă o funcţie de relocare a programelor (adreselor de
program), pentru că adresele virtuale utilizate de un program (task) sunt relocate
spre adrese fizice (în general diferite), înainte ca ele să fie folosite pentru
accesarea memoriei principale. Această mapare permite aceluiaşi program să fie

90
încărcat şi să ruleze oriunde ar fi încărcat în MP (deci să fie relocabil în mod
automat), modificările de adrese realizându-se automat prin mapare (fără MV,
un program depinde, de obicei, în execuţia sa, de adresa de memorie unde este
încărcat).
MV este un concept deosebit de util, în special în cadrul sistemelor de
calcul multiprogramate care - de exemplu prin “time-sharing” - permit execuţia
cvasi-simultană (concurentă) a mai multor programe (vezi sistemele de operare
WINDOWS 2000, NT, Unix, Ultrix etc.) Fiecare dintre aceste programe
dinamice are propriul său spaţiu virtual de cod, stivă şi de date (având alocate un
număr de pagini virtuale), dar în realitate toate aceste programe vor partaja
aceeaşi MP, care va conţine, în mod dinamic, pagini aferente diverselor procese.
Paginile vor fi dinamic încărcate de pe disc în MP, respectiv evacuate din MP pe
disc (spre a permite încărcarea altora, mai “proaspete”).
Când o pagină accesată nu se găseşte în MP, ea va trebui adusă prin
declanşarea unui mecanism de excepţie, de pe disc. Acest eveniment de excepție
– analogul miss-urilor de la cache-uri – se numeşte “page fault” (PF).
Evenimentul PF va declanşa un mecanism de excepţie, care va determina
intrarea într-o subrutină de tratare a evenimentului PF. Aici – prin software deci
– se va aduce de pe disc în MP pagina dorită după ce, fireşte, în prealabil s-a
evacuat eventual o altă pagină deja scrisă (acea pagină pentru care durata din
momentul ultimei sale accesări, până la momentul curent, este maximă – LRU)
din MP. Acest proces este unul de lungă durată, necesitând câteva ms bune la
ora actuală. Având în vedere multitaskingul, MV trebuie să asigure şi
mecanismul de protecţie a programelor (spre exemplu, să nu permită unui
program utilizator să scrie zona de date sau de cod a sistemului de operare sau a
altui program privilegiat, să nu permită scrierea într-o pagină accesibilă numai
prin citire etc.)
În implementarea MV trebuie avute în vedere următoarele aspecte
importante:
♦ Paginile (analoagele blocurilor de la memoria cache) să fie suficient de mari
(4 ko ÷16 ko … 64 ko), astfel încât să compenseze timpul mare de acces la
disc (8 ÷12 ms), în virtutea principiului statistic de vecinătate spațială a
datelor.
♦ Sunt necesare organizări care să reducă rata de evenimente de excepție de
tipul PF, rezultând un plasament flexibil al paginilor în memoria principală
(MP).

91
♦ PF-urile trebuie tratate prin software şi nu prin hardware (spre deosebire de
miss-urile în cache-uri), timpul de acces al discurilor permițând lejer acest
lucru.
♦ Scrierile în cadrul MV se fac după algoritmi de tip “Write Back” (se scrie
numai în pagina respectivă situată în MP), ca să păstrăm analogia cu
memoriile cache, şi nu “Write Through” (accesul la disc ar consuma timp
enorm).

Figura 2.10. Translatare adresă virtuală în adresă fizică

Observație: Fiecare program (task) are propria sa tabelă de pagini, care mapează
spaţiul virtual de adresare al programului, într-un spaţiu fizic, situat în M.P.

Tabela de pagini + PC + registrele logice ale microprocesorului formează


starea unui anumit program. Programul + starea asociată caracterizează un
anumit proces (task). Un proces executat curent este activ, altfel, el este inactiv.
Comutarea de task-uri implică inactivarea procesului curent şi activarea altui
proces (prin alocarea de resurse de memorie și CPU), inactiv până acum,
rezultând deci ca fiind necesară salvarea/restaurarea stării proceselor. Desigur,
sistemul de operare (S.∅.) trebuie doar să reîncarce registrul pointer al adresei

92
de bază a tabelei de pagini (PTR), pentru a pointa la tabela de pagini aferentă
noului proces activ.
Excepţia Page Fault (P.F.)
Apare în cursul mecanismului de translatare a adresei virtuale în adresă
fizică, dacă bitul de prezență a paginii în MP P = 0 (semnificând deci că pagina
accesată nu există momentan în memoria principală). Ca urmare, printr-o
procedură de excepţie, se dă controlul unui handler (rutină, driver) al S.∅. în
vederea tratării acestei excepții. Aici S.∅. va trebui să decidă care pagină din
M.P. va trebui evacuată, în vederea încărcării ulterioare a noii pagini de pe disc.
În general, ca principiu, se poate merge pe ideea LRU (“Least Recently Used”),
adică va fi evacuată pagina care nu a mai fost accesată de către CPU de cel mai
mult timp (se merge deci implicit pe principiul statistic al localităţii temporale).
Exemplu: CPU a accesat în ordine paginile: 10,12,9,7,11,10 iar acum
accesează pagina 8, care nu este prezentă în MP ⇒ evacuează pagina 12 ! Dacă
următorul acces generează PF ⇒ evacuează pagina 9, ş.a.m.d.

Observație: Unele maşini (spre exemplu microprocesoarele I-Pentium)


implementează în tabela de pagini câte un bit de referinţă pentru
fiecare pagină. Acest bit este setat la fiecare accesare a acelei pagini.
S.∅. şterge periodic aceşti biţi – nu înainte de a le memora starea –
astfel încât să implementeze pentru fiecare pagină un contor; astfel,
bazat pe starea de moment a acestor contoare, se stabileşte care pagină
va fi evacuată. Se realizează astfel o implementare simplificată, bazată
pe politica LRU.

Scrierile în MP se desfăşoară după următoarele principii:


♦ strategie similară cu cea de tip write-back de la memoriile cache (copy-back)
♦ se adaugă un “Dirty Bit” (D) în tabela de pagini pentru fiecare pagină. Bitul D
este setat la fiecare scriere în pagină ⇒ la evacuare, o pagină având bitul D=0,
nu are rost să se evacueze efectiv pe discul magnetic ⇒ pierdere mare de timp
⇒ minimizare scrieri !
Translation – Lookaside Buffers (TLB)
Prin paginare, fiecare acces la o dată necesită două accese la memorie: unul
pentru obţinerea adresei fizice din tabela de pagini, iar celălalt pentru accesarea
propriu-zisă a datei în M.P., cu adresa fizică anterior aflată. În vederea reducerii
acestui timp de acces (dublu), tabela de pagini este deseori “caşată” (memorată

93
parţial) în CPU. Memoria cache care memorează maparea tabelei de pagini se
numeşte TLB (Translation Lookaside Buffer). Ca orice cache, TLB-ul poate
avea diferite grade de asociativitate. Există evacuări/încărcări între TLB şi tabela
de pagini din M.P.
Deoarece TLB-ul este implementat în general “on-chip”, capacitatea sa este
relativ mică (32÷1024 intrări) în comparare cu tabela de pagini care are 1 ÷ 4 M
intrări. De obicei TLB-ul se implementează complet asociativ (full-associative),
pentru a avea o rată de miss scăzută (0,01 % ÷ 0,1 % ÷ 1 %). Missurile în TLB
se pot rezolva atît prin protocol hardware cât şi printr-un handler software.
Atenție, TLB-ul accelerează translatarea dar sistemul de MV poate funcționa și
fără această structură.

Figura 2.11. Relaţia TLB - cache într-un microsistem DEC 3100


(microprocesor MIPS-R2000)

Observație: Ar fi mai rapid dacă s-ar adresa cache-ul cu adresa virtuală (cache-
uri virtuale) şi nu cu cea fizică, întrucât în acest caz nu s-ar mai aștepta

94
după translatarea adresei virtuale. Pentru a contracara acest dezavantaj al
adresării cache-ului cu adrese fizice, procesul de adresare este uneori
pipeline-izat. Totuși, adresarea cache-urilor cu adrese virtuale rămâne
atractivă, datorită timing-ului avantajos. Ce determină totuși dificultatea
implementării unor memorii cache adresate cu adrese virtuale (așa numite
cache-uri virtuale)? Un motiv este dat de faptul că de fiecare dată când se
face o comutare de procese (taskuri), o anumită adresă virtuală a noului
task va genera de obicei o adresă fizică diferită de cea pe care, aceeași
adresă virtuală a generat-o în vechiul task (așadar, schematic: AV AF1,
AV AF2, cu AF1, AF2 diferite), implicând deci necesitatea golirii
(invalidării tuturor locațiilor) memoriei cache (altfel, aceasta ar putea
genera false hituri pe adresa AV a noului task, prin confuzie cu cea a
vechiului task). Această golire crește în mod evident rata de miss în cache
(cold misses). O soluție la această problemă ar putea consta în adăugarea
unui câmp, numit identificator de proces (task), asociat fiecărui cuvânt din
cache (PID - Process-Identifier). Sistemul de operare ar urma să asigneze
PID-uri diferite unor procese diferite. Evident că logica din cache nu va
genera hit pentru accesul unui proces la data/instrucțiunea altuia, ci va
genera în acest caz un miss. O altă problemă a cache-urilor virtuale constă
în faptul că task-uri diferite pot utiliza adrese virtuale diferite pentru
aceeași adresă fizică (aliases). Aceste alias-uri pot determina existența în
cache a două copii ale aceleiași date, situată evident la aceeași adresă fizică
(așadar, schematic: AV1 AF, AV2 AF, cu AV1, AV2 diferite). Dacă
una dintre aceste copii este modificată în cache, cealaltă copie poate avea o
valoare eronată (incoerență). Asignarea de PID-uri poate fi o soluție și în
acest caz. O altă soluţie simplă şi imediată la această problemă ar consta în
adresarea cache-ului cu biţii P∅ care sunt nemodificaţi prin procesul de
translatare (AV AF). Practic AV1 și AV2 sunt identice pe biții lor cei mai
puțin semnificativi, cei care codifică P∅. Astfel, AV1 și AV2 vor accesa
același bloc din cache, evitând astfel existența unor copii diferite ale
aceleiași locații fizice de memorie principală. Desigur că în acest caz este
necesar ca dimensiunea cache ≤ dimensiunea paginii. Există și soluții
hardware care garantează fiecărui bloc din cache o adresă fizică unică.

Protecţia în sistemele cu M.V.


Deşi fiecare proces are propriul său spaţiu virtual de adresare, memoria
fizică (MP) este partajată între mai multe procese, având niveluri diferite de

95
privilegiu, în spectrul kernel (supervisor) - user (procese utilizator, S∅,
driverele I/O etc.) Desigur, trebuie să se controleze strict accesul unui proces în
zonele de cod, stivă şi date ale altui proces, rezultând necesitatea protecţiei la
scrieri/citiri. Spre exemplu, numai S.∅. trebuie să poată modifica tabelele de
pagini aferente diferitelor procese, nicidecum un simplu program al
utilizatorului. În vederea implementării protecţiilor, hardul trebuie să asigure cel
puţin următoarele trei condiţii:
1. Cel puţin două moduri distincte de rulare a unui program: modul
supervizor (kernel, executiv), în care un proces poate să execute orice
instrucţiuni (inclusiv a unora privilegiate deci) şi să acceseze oricare
resurse hardware şi respectiv modul utilizator (user), în care un proces
are o mulţime de restricţii legate de protecţia şi securitatea sistemului.
2. Să existe o parte privilegiată a stării CPU, în care un proces user să nu
poată scrie. De exemplu: biţi de stare user/kernel, registrul PTR, bitul
validare/invalidare, excepţii, pagini kernel (ACCES) etc. Posibilitatea
unui proces user să scrie astfel de resurse ar determina S.∅. (proces
supervizor) să nu poată controla procesele user.
3. Mecanismele de tranziţie a procesorului din modul supervizor în modul
user şi invers. Tranziţia user-supervizor din modul curent user, se poate
face printr-o excepţie (întrerupere) sau printr-o instrucţiune specială de
tip SYSTEM CALL, care transferă controlul la o adresă dedicată din
spaţiul de cod supervizor (CALL GATE – la microprocesorul Pentium).
Se salvează PC-ul şi contextul procesului curent şi CPU e plasat în
modul de lucru anterior (user aici).
De asemenea, din modul “supervizor” se poate trece în modul “user” prin
simpla modificare a biţilor de mod de lucru CPU (este permis !) Spre exemplu,
să presupunem că un proces P2 doreşte să îi transmită (citire) anumite date,
printr-o pagină proprie, unui alt proces P1. Pentru acest deziderat, P2 ar putea
apela o rutină a S.∅. (printr-un SYSTEM CALL), care, la rândul ei (fiind
privilegiată!) va crea o intrare în tabela de pagini a lui P1 care să se mapeze pe
pagina fizică pe care P2 doreşte s-o pună la dispoziţie. S.∅. (supervizor) poate
să utilizeze bitul “Write Protection” pentru a împiedica procesul P1 să altereze
respectiva pagină. Şi alţi biţi de control de tip “drepturi de acces" în pagină pot fi
incluşi în tabela de pagini şi în TLB.

Observație: În cazul unei comutări de taskuri de la procesul P1 la procesul P2,


TLB-ul trebuie golit din două motive principale: în caz de hit, P2 să nu

96
utilizeze paginile lui P1 şi respectiv să se încarce în TLB intrările din tabela
de pagini a procesului P2 (pointată de noul PTR). Asta se întâmplă numai
dacă P1 şi P2 folosesc VPN-uri identice (biţii 31 ÷ 12 din adresa virtuală).
Pentru a nu goli TLB-ul prea des, se preferă adăugarea la tag-ul acestuia a
unui câmp numit PID (“Process Identifier” – identificator al procesului),
care va contribui, în mod corepunzător, la HIT. Această informaţie (PID)
este ţinută de obicei într-un registru special, ce va fi încărcat de către S.∅.
la fiecare comutare de taskuri. Ca şi consecinţă, se evită în majoritatea
cazurilor golirea (şi implicit reumplerea!) TLB-ului.

În concluzie, foarte succint, protecţia este asigurată în principal prin:


♦ moduri de lucru CPU de diferite nivele de privilegiu
♦ control strict al S.∅. asupra tranziţiilor din modul user în kernel (prin CALL
GATES-uri - porţi de apel - la o anumită adresă determinată din spaţiul de cod
kernel)
♦ protecţie a paginilor prin “drepturi de acces” la pagină (read only, read/write
etc).
Tratarea miss-urilor în TLB şi a PF-urilor
Pe durata procesului de translatare a adresei virtuale în adresă fizică pot să
apară două evenimente de excepţie:
1. TLB miss, dar pagina accesată este prezentă în memoria fizică (M.P.)
2. Page Fault (PF), adică TLB miss urmat de faptul că pagina dorită nu este
prezentă în tabela de pagini rezidentă în M.P. (bitul P=0).
Un TLB miss generează de obicei o procedură hardware de aducere a
numărului paginii fizice din tabela de pagini. Această operaţie se poate
implementa prin hardware, ea durând un timp relativ scurt (cca. 20 – 50 tacte
CPU).
Tratarea PF în schimb, necesită un mecanism de tratare a excepţiei care să
întrerupă procesul activ, să transfere controlul rutinei de tratare a evenimentului
PF (S.∅.) şi apoi să redea controlul procesului întrerupt. PF va fi recunoscut pe
parcursul ciclilor de acces la memorie. Cum instrucţiunea care a cauzat PF
trebuie reluată, de astă dată cu succes, rezultă că trebuie salvat în stivă (automat,
de către CPU) PC-ul aferent acesteia. Pentru implementarea acestei operații,
există un registru special EPC (Exception PC), întrucât PC-ul propriu-zis poate
să fie mult incrementat sau chiar complet altul (din motive de prefetch, branch-
uri etc.) Mai apoi, printr-un sistem de întreruperi (vectorizate) se dă controlul

97
rutinei de tratare din cadrul S.∅. Aici, se află cauza excepţiei, prin consultarea
registrului “cauză excepţie” iar apoi se salvează întreaga stare (context) a
procesului întrerupt (regiştrii generali, PTR, EPC, registri “cauză excepţie“ etc.)
Dacă PF-ul a fost cauzat de un “fetch sau write data”, adresa virtuală care a
cauzat PF trebuie calculată din însăşi formatul instrucţiunii pe care PF s-a
produs (PC-ul aferent acesteia este memorat în EPC), de genul “base + offset”.
Odată ştiută adresa virtuală care a cauzat PF, rutina de tratare a S.∅. aduce
pagina de pe disc în MP, după ce mai întâi a evacuat (LRU) o pagină din MP pe
disc. Cum accesul pe disc durează mii de tacte, uzual S.∅. va activa un alt
proces pe această perioadă.
Segmentarea
Constituie o altă variantă de implementare a MV, care utilizează în locul
paginilor de lungime fixă, entităţi de lungime variabilă, zise segmente. În
segmentare, adresa virtuală este constituită din două cuvinte: o bază a
segmentului şi respectiv un deplasament (offset, index) în cadrul segmentului.
Datorită lungimii variabile a segmentului (spre exemplu, 1 octet ÷ 2³² octeţi la
arhitecturile Intel Pentium I), trebuie făcută şi o verificare a faptului că adresa
virtuală rezultată (baza + offset) se încadrează în lungimea adoptată a
segmentului. Desigur, segmentarea oferă posibilităţi de protecţie puternice şi
sofisticate a segmentelor. Pe de altă parte, segmentarea induce şi numeroase
dezavantaje precum:

♦ două cuvinte pentru o adresă virtuală, necesare având în vedere lungimea


variabilă a segmentului. Acest fapt complică sarcina compilatoarelor şi a
programelor.
♦ încărcarea segmentelor variabile în memorie este mai dificilă decât la
paginare.
♦ fragmentare a memoriei principale (porţiuni nefolosite)
♦ frecvent, trafic ineficient între MP și disc (de exemplu, pentru segmente
“mici”, transferul cu discul este complet ineficient – accese la nivel de sector
= 512 octeti)
Există în practică şi implementări hibride segmentare – paginare.

98
3. PROCESOARE PIPELINE SCALARE CU SET OPTIMIZAT DE
INSTRUCŢIUNI

3.1. MODELUL RISC. GENEZĂ ŞI CARACTERISTICI GENERALE

Acest capitol are la bază, în principal, părți ale unei lucrări anterioare scrise
și publicate de autor, sub forma unei monografii științifico-tehnice [Vin00], cu
revizuiri și adăugiri semnificative în această versiune, în speranța că sub această
formă nouă, de curs universitar, va fi mai ușor asimilabil de către studenții și
specialiștii interesați.
Microprocesoarele RISC (Reduced Instruction Set Computer) au apărut ca
o replică la lipsa de eficienţă a modelului convenţional de procesor, de tip CISC
(Complex Instruction Set Computer). Multe dintre instrucţiunile maşină ale
procesoarelor CISC sunt foarte rar folosite în softul de bază, cel care
implementează sistemele de operare, utilitarele, translatoarele etc. Lipsa de
eficienţă a modelului convenţional CISC a fost pusă în evidenţă prin anii '80 de
arhitecturi precum INTEL 80x86, MOTOROLA 680x0, iar în domeniul
(mini)sistemelor de calcul, în special de către arhitecturile VAX-11/780 şi IBM -
360, 370, probabil cele mai cunoscute și utilizate la acea vreme.
Modelele CISC sunt caracterizate de un set foarte bogat de instrucţiuni -
maşină, neortogonal, formate de instrucţiuni de lungime variabilă (de la unul la
vreo 22 de octeți la procesoarele I-Pentium, spre exemplu), numeroase moduri
de adresare deosebit de sofisticate etc. Evident că această complexitate
arhitecturală barocă are o repercursiune negativă asupra performanţei maşinii.
Primele microprocesoare RISC s-au proiectat la Universităţile din Stanford
(coordonator prof. John Hennessy) şi respectiv Berkeley (coordonator prof.
David Patterson, cel care a şi propus denumirea RISC, nu foarte sugestivă în
opinia autorului acestei cărți), din California, SUA (1981). Spre deosebire de
CISC-uri, proiectarea sistemelor RISC are în vedere că înalta performanţă a
procesării se poate baza pe simplitatea şi eficacitatea proiectului ("Keep it
simple!). De fapt, cel care a introdus pentru prima dată ideea de procesor RISC,
a fost cercetătorul american Dr. John Cocke, în proiectarea sistemului IBM 801

99
(1980). Lui Cocke i se datorează și ideile legate de procesoarele superscalare
(implementate în calculatorul Cheetah), dar și cele legate de optimizarea statică
a programelor, prin compilator. Ideile sale fertilizatoare au determinat cercetări
publice și în mediul academic. Strategia de proiectare a unui microprocesor
RISC trebuie să ţină cont de analiza aplicaţiilor posibile, pentru a determina
operaţiile cele mai frecvent utilizate (în vederea accelerării lor prin hardware),
precum şi optimizarea structurii hardware în vederea unei execuţii cât mai rapide
a instrucţiunilor. Dintre aplicaţiile specifice sistemelor RISC se amintesc:
conducerea de procese în timp real, procesare de semnale (Digital Signal
Processing - DSP), calcule ştiinţifice cu viteză ridicată, grafică de mare
performanţă, elemente de procesare în sisteme multiprocesor şi alte sisteme cu
prelucrare paralelă etc.
Caracteristicile de bază ale modelului RISC sunt următoarele:
-Timp de proiectare şi erori de construcţie mai reduse decât la variantele
CISC.
-Unitate de comandă hardware în general cablată, cu firmware (structuri
microprogramate) redus sau chiar deloc, ceea ce măreşte rata de execuţie a
instrucţiunilor.
-Utilizarea tehnicilor de procesare pipeline a instrucţiunilor, ceea ce implică
o rată teoretică de execuţie de o instrucţiune / ciclu, pe modelele de procesoare
care pot lansa în execuţie la un moment dat o singură instrucţiune (procesoare
scalare).
-Memorie sistem de înaltă performanţă, prin implementarea unor arhitecturi
avansate de memorie cache şi MMU (Memory Management Unit). De
asemenea, conţin mecanisme hardware de memorie virtuală, bazate în special pe
paginare, ca şi sistemele CISC de altfel.
-Set relativ redus de instrucţiuni simple, ortogonale, majoritatea fără referire
la memorie şi cu puţine moduri de adresare. În general, doar instrucţiunile
LOAD (citire din memorie) / STORE (scriere în memorie) sunt cu referire la
memorie (arhitectură tip LOAD / STORE). La implementările recente
caracteristica de "set redus de instrucţiuni" nu trebuie înţeleasă add litteram, ci,
mai corect, în sensul de set optimizat de instrucţiuni în vederea implementării
aplicaţiilor propuse (în special implementării limbajelor de nivel înalt - C,
Visual C++, Pascal, Java, OpenCL etc.)
-Datorită unor particularităţi ale procesării pipeline (în special hazardurile
pe care aceasta le implică – v. în continuare), apare necesitatea unor
compilatoare optimizate (schedulere), cu rolul de a reorganiza programul sursă,

100
pentru a putea fi procesat optimal din punct de vedere al timpului de execuţie
dar, mai nou, și din punct de vedere al energiei consumate (esențială pentru
sistemele mobile).
-Format fix al instrucţiunilor, codificate în general pe un singur cuvânt de
32 de biţi, mai recent pe 64 de biţi (Alpha 21164, Power PC-620 etc.)
-Necesităţi de memorare a programelor mai mari decât în cazul
microsistemelor convenţionale, datorită simplităţii instrucţiunilor cât şi
reorganizatoarelor, care pot acţiona defavorabil asupra "lungimii" programului
obiect, după cum se va constata în continuarea lucrării.
-Set de registre generale substanţial mai mare decât la CISC-uri, în vederea
lucrului "în ferestre" (register windows), util în optimizarea instrucţiunilor
CALL / RET. Numărul mare de registre generale este util şi pentru mărirea
spaţiului intern de procesare, tratării optimizate a evenimentelor de excepţie,
modelului ortogonal de programare etc. Registrul R0 este cablat la zero în
majoritatea implementărilor, pentru optimizarea modurilor de adresare şi a
instrucţiunilor (v. în continuare).
Microprocesoarele RISC scalare reprezintă modele cu adevărat evolutive
în istoria tehnicii de calcul. Primul articol despre acest model de procesare a
apărut în anul 1981 (David Patterson, Carlo Sequin), iar peste numai 6-7 ani
toate marile firme producătoare de hardware realizau microprocesoare RISC
scalare, în scopuri comerciale sau de cercetare. Performanţa acestor
microprocesoare a crescut în medie cu cca. 60% în fiecare an, până prin anul
2004 când s-a trecut la producția masivă de multiprocesoare integrate pe un
singur cip (multicore).

3.2. SET DE INSTRUCŢIUNI. REGIŞTRI INTERNI LA MODELUL


ARHITECTURAL RISC

În proiectarea setului de instrucţiuni aferent unui microprocesor RISC


intervin o multitudine de consideraţii, dintre care se amintesc:
a). Compatibilitatea cu seturile de instrucţiuni ale altor procesoare pe care s-
au dezvoltat produse software consacrate (compatibilitatea de sus în jos, valabilă
de altfel şi la CISC-uri). Portabilitatea acestor produse pe noile procesoare este
condiţionată de această cerinţă, care vine în general în contradicţie cu cerinţele
de performanţă ale sistemului.

101
b). În cazul microprocesoarelor, setul de instrucţiuni este în strânsă
dependenţă cu tehnologia folosită, care de obicei limitează sever performanţele
(constrângeri legate de aria de integrare, numărul de terminale, cerinţe restrictive
particulare ale tehnologiei, necesitatea unui consum de putere limitat etc.)
c). Minimizarea complexităţii unităţii de comandă, precum şi minimizarea
fluxului de informaţie procesor - memorie.
d). În cazul multor microprocesoare RISC, setul de instrucţiuni maşină este
ales ca suport pentru implementarea facilă unor limbaje de nivel mediu / înalt
(compilatoare etc.)
Setul de instrucţiuni al unui microprocesor RISC este caracterizat de
simplitatea formatului precum şi de un număr limitat de moduri de adresare. De
asemenea, se urmăreşte ortogonalizarea setului de instrucţiuni (adică, orice
instrucțiune să poată utiliza orice mod de adresare și orice registru logic).
Deosebit de semnificativ în acest sens, este chiar primul microprocesor RISC
numit RISC I Berkeley. Acesta deţinea un set de 31 de instrucţiuni, grupate în 4
categorii: aritmetico-logice, de acces la memorie, de salt / apel subrutine şi
speciale. Microprocesorul deţinea un set logic de 32 de regiştri generali, pe 32
de biţi fiecare. Formatul instrucţiunilor de tip registru-registru şi respectiv de
acces la memorie (LOAD / STORE) este dat în Figura 3.1:

Figura 3.1. Formatul instrucţiunii la procesorul Berkeley I RISC


Câmpul IMM (Immediate) = 0 arată că cei mai puţini semnificativi
(c.m.p.s.) 5 biţi ai câmpului SOURCE2 codifică al 2-lea registru operand.
Câmpul IMM = 1 arată că SOURCE2 semnifică o constantă pe 13 biţi cu
extensie semn pe 32 biţi, atât pentru instrucțiunile aritmetice cu un operand
sursă imediat, cât și pentru indexul instrucțiunilor LOAD/STORE.
Câmpul SCC (Store Condition Codes) semnifică validare / invalidare a
activării indicatorilor de condiţie, corespunzător operaţiilor aritmetico-logice
executate.
În ciuda setului redus de instrucţiuni al procesorului Berkeley RISC I,
acesta poate sintetiza o multitudine de instrucţiuni aparent "inexistente" la acest
model. Practic nu se "pierd" instrucţiuni, ci doar "opcode"- uri, şi ca o
consecinţă, în plus se simplifică substanţial logica de decodificare şi unitatea de
comandă. Un alt format de instrucţiuni combină ultimele trei câmpuri ale acestui
format ca un offset pe 19 biţi, pentru instrucţiunile de salt relativ. Astfel, aceste
salturi relative vor putea fi mai adânci, atât “în sus” (offset negativ), cât și “în

102
jos” (offset pozitiv). Cu precizarea că registrul R0 este cablat la 0, se poate arăta
că practic pot fi sintetizate toate modurile de adresare de la procesorul CISC tip
VAX11-780, după cum se sugerează în tabelul următor:

Mod de Vax 11/780 Echivalent RISC 1


adresare
Registru Ri Ri
Imediat #Literal S2 (literal pe 13 biți), IMM
=1
Indexat Ri + offset Ri+S2(offset pe 13 biți)
Indirect-registru (Ri) Ri+S2, S2=0
Absolut @#Adresa R0+S2, R0=0
Tabelul 3.1. Formatul instrucţiunii la microprocesorul Berkeley I RISC

Tabelul următor arată că instrucțiunile simple și puține ale


microprocesorului Berkeley RISC I pot implementa practic oricare dintre
multele și complicatele instrucțiuni mașină ale procesorului CISC VAX 11/780.
Iată doar câteva exemple, în tabelul următor:

Instrucţiune VAX 11/780 Echivalent RISC I


Move registru- MOVL Ri, Rj ADD R0, Ri, Rj
registru (R0+Ri → Rj, R0=0,
SCC=1)
Comparare CMPL Ri, Rj SUB Ri, Rj, R0
registru-registru (Ri-Rj →R0, SCC=1);
R0=0
“Clear” locaţie CLRL <Adresa> STL R0, (R0) S2
memorie (Locaţie mem. (R0+S2)
← 0)
Incrementări INCL Ri ADD Ri, #1, Ri
Decrementări DECL Ri SUB Ri, #1, Ri
Incărcare contor MOVL $N, Ri ADD R0, #N, Ri
Complement de 1 MCOMPL Ri, Rj XOR Ri, #FFFFFFFFh,
Rj
Tabelul 3.2. Instrucţiuni emulate cu ajutorul setului de instrucţiuni al
procesorului RISC I

103
Următoarele trei formate de instrucţiuni sunt întâlnite, practic, în toate
arhitecturile RISC studiate (DLX, Intel 860, 960, MIPS 88000, SPARC,
APLHA, Power PC, HP PA etc.):
instrucţiuni registru – registru (RS1, RS2, RDEST), de tip aritmetico –
logice, cu doi operanzi sursă și o destinație, toți în registre CPU
instrucţiuni registru – constantă imediată (RS1, constantă, RDEST) sunt
instrucţiuni de transfer, aritmetico – logice etc.
instrucţiuni de ramificaţie/salt/apel în program (JUMP / CALL, BRANCH
etc.)
O altă categorie de microprocesoare RISC reprezentative sunt cele din
familia MIPS 2000 / 3000, dezvoltate iniţial la universitatea din Stanford,
SUA. La aceste procesoare există trei formate distincte de instrucţiuni
maşină, numite: R-type, I-type şi J-type. Formatul R-type înglobează
instrucţiuni aritmetico – logice, de salt / apel etc. şi este prezentat mai jos:

OPCODE RS(5) RT(5) RD(5) SHAMT FUNCT


(6) (5) (6)
Figura 3.2. Formatul R-type la procesorul MIPS R-2000.

Câmpurile RS, RT şi RD specifică regiştrii sursă respectiv registrul


destinaţie al instrucţiunii. Câmpul FUNCT codifică varianta de operaţie
aferentă unei anumite grupe de operaţii codificate de câmpul OPCODE.
Câmpul SHAMT specifică numărul poziţiilor binare cu care se face
deplasarea pentru instrucţiunile de deplasare aritmetice sau logice. Spre
exemplu, instrucţiunea SLL $10, $16, 8, introduce în registrul 10, conţinutul
registrului 16 deplasat logic la stânga cu 8 poziţii binare. Tot în formatul R-
Type se încadrează şi instrucţiuni de comparaţie. De exemplu, instrucţiunea
SLT $1, $2, $3 semnifică următoarea acțiune: if ($2 < $3) $1 = 1 else $1 = 0.
Tot aici se încadrează şi instrucţiunea de salt indirect registru JR $31.
Formatul I-type este ca în figură:

OPCODE (6) RS (5) RT (5) OFFSET (16)


Figura 3.3. Formatul I-type la procesorul MIPS R-2000.

104
Acest format este specific instrucţiunilor LOAD / STORE care utilizează
modul de adresare indexat. De exemplu, o instrucţiune de încărcare din memorie
în registru CPU:
LW $8, ASTART ($9) (încarcă în registrul 8 locaţia de memorie de la adresa
indexată). Tot aici se incadrează şi instrucţiunile de salt condiţionat pe diverse
condiţii precum:
BEQ $1, $2, ADR sau BNE 41, $2, ADR (branch pe equal sau non-equal)

OBSERVAȚIE: Distincţia între formatele R şi I se face prin câmpul de


OPCODE.
Formatul J – type este specific instrucţiunilor de salt.

OPCODE (6) ADRESĂ (26)


Figura 3.4. Formatul J-type la procesorul MIPS R-2000.

Spre exemplu, instrucțuinea J 10000 semnifică salt la adresa 10000.


Instrucţiunea “Jump and Link”: JAL ADR determină $31 = PC + 4, iar apoi salt
la adresa ADR (un fel de RETURN simplificat). De remarcat simplitatea şi
regularitatea setului de instrucţiuni. Câmpurile îşi păstrează locurile în cele trei
tipuri de formate de instrucţiuni, ceea ce determină simplificarea
decodificatorului de instrucţiuni al procesorului. Desigur însă că adresele şi
constantele din cadrul instrucţiunilor sunt limitate de lungimea fixă a câmpurilor
aferente. De exemplu apelul / revenirea la / din o subrutină se face prin
instrucţiunile JAL Subr / JR $31 respectiv, evitându-se astfel memoria stivă. Ce
se întâmplă însă în cazul a trei rutine imbricate? Considerând 3 rutine A, B, C
imbricate, situaţia se rezolvă elegant ca în continuare:

A:
JAL B B:
ADD $29, $29, $24
SW $31, 0($29)
JAL C C:...
JR $31
LW $31, 0($29)
SUB $29,$29,$24
JR $31

105
Şi la acest microprocesor, setul instrucţiunilor maşină a fost proiectat
ţinând cont de facilizarea implementării unor limbaje HLL (High Level
Languages). Exemplificăm prin implementarea următoarei secvenţe în limbajul
C:
switch (k) {
case 0: f = i + j; break
case 1: f = g + h; break
case 2: f = g – h; break
case 3: f= i – j; break
}

Considerând variabilele f, g, h, i, j, k stocate respectiv în regiştrii 16, 17,


18, 19, 20, 21 şi că $10 = 4, avem următoarea implementare în limbaj maşină
MIPS a secvenţei C anterioare :

LOOP: MULT $9, $10, $21; $9 k*4


LW $8, JT ($9)
JR $8
L0: ADD $16, $19, $20
J EXIT
L1: ADD $16, $17, $18
J EXIT
L2: SUB $16, $17, $18
J EXIT
L3: SUB $16, $19, $20
J EXIT
EXIT:

Menţionăm că la adresele JT, JT + 4, JT + 8, JT + 12 în memorie avem


stocate etichetele L0, L1, L2 şi L3. O variantă limită a ideii de RISC este
modelul “Single Instruction Computer” (SIC) de procesor, care execută o
singură instrucţiune cablată (vezi în Capitolul următor arhitectura tip TTA). De
exemplu un procesor SIC care ştie să execute doar instrucţiunea “Substract and
Branch if Negative”(SBN) după următoarea regulă:

SBN a, b, c; MEM MEM (a) – MEM(b)


if MEM (a) < 0 go to c

106
Prin această instrucţiune se pot emula multe alte instrucţiuni mai
complicate. De exemplu o instrucţiune de tipul MEM (a) MEM (b) s-ar
emula pe un procesor SIC SBN astfel:

START: SBN temp, temp, +1


SBN temp, a, +1
SBN b, b, +1
SBN b, temp, +1

Este surprinzător că printr-o singură instrucţiune se pot emula principalele


instrucţiuni ale unui procesor mai “matur”, mai complex. Ideea a fost de altfel
reluată de către arhitecţii anilor ‘90, care au introdus conceptul de Transport
Triggered Architecture - TTA (vezi capitolul următor). Încă o dovadă că
procesoarele SIC, RISC, superscalare, VLIW, EPIC, vectoriale etc. au repus în
discuţie aspecte absolut fundamentale ale proiectării acestora, dar
nefundamentate satisfăcător prin implementările CISC. Regândirea modurilor
optimale de proiectare a procesoarelor numerice a startat abia la începutul anilor
1980, odată cu cercetările legate de conceptul de procesor RISC. Potenţial,
programele RISC pot conţine mai multe CALL-uri decât cele convenţionale
(CISC), în principal pentru că instrucţiunile complexe implementate în
procesoarele CISC vor fi subrutine în cazul procesoarelor RISC. Din acest
motiv, procedura CALL / RET se impune a fi cât mai rapidă în procesarea
RISC. Modelul utilizării registrelor în ferestre (register windows) îndeplineşte
acest deziderat, prin reducerea traficului de date cu memoria sistem (Berkeley I
RISC, SUN-SPARC etc.) Principiul de lucru constă în faptul că fiecare
instrucţiune CALL alocă o nouă fereastră de registre, pentru a fi utilizată de
procedura apelată, în timp ce o instrucţiune RET va restaura vechea fereastră de
registre. Se consideră spre exemplificare, trei procese software, care se apelează
imbricat, ca în figura următoare (Figura 3.5):

107
Figura 3.5. Lucrul în ferestre de registre

Statistici deosebit de temeinice arată că în cadrul implementărilor de


limbaje de nivel mediu sau HLL (High Level Languages), instrucţiunile de tip
CALL / RET sunt frecvent folosite şi, totodată, cele mai mari consumatoare de
timp (ocupă între 5% - 45% dintre referirile la memorie). Fiecare proces are
alocată o fereastră de regiştri constând în trei seturi distincte : HIGH, LOCAL şi
LOW. Registrele globale R0 - R9 conţin parametrii globali ai proceselor
software. Aceşti regiştri sunt comuni tuturor proceselor şi nu se salvează /
restaurează niciodată. Regiştrii locali R16 - R25 sunt utilizaţi pentru memorarea
scalarilor locali ai procesului curent. O instrucţiune de tip CALL determină ca
registrele LOW din procesul master apelant să devină registre HIGH în procesul
SLAVE apelat. Odată cu schimbarea ferestrelor, o instrucţiune CALL
memorează registrul PC într-un anumit registru al noii ferestre. O instrucţiune
RET determină o acţiune reciprocă, adică regiştrii HIGH ai procesului curent
vor deveni regiştrii LOW pentru procesul MASTER în care se revine.
În regiştrii LOW procesul MASTER transmite în mod automat valorile
parametrilor spre procesul SLAVE, respectiv din aceşti regiştri procesul
MASTER curent preia rezultatele de la procesul SLAVE apelat. În regiştrii
HIGH procesul curent memorează rezultatele, respectiv din aceşti regiştri
procesul curent preia parametrii transmişi de către procesul MASTER apelant.
Se observă că aceste comutări de ferestre au drept principal scop eliminarea
accesului stivei şi deci a timpului semnificativ consumat cu accesarea acesteia.
Referitor la cele expuse succint mai sus apar două tipuri de excepţii ca fiind
posibile:

108
- "window overflow" (WO) poate să apară după o instrucţiune CALL în care
schimbarea setului de regiştri LOW ai procesului curent, în regiştrii HIGH ai
procesului viitor, devine imposibilă, datorită numărului limitat de regiştri
generali implementaţi;
- "window underflow" (WU) poate să apară după o instrucţiune RET, care
determină ca schimbarea regiştrilor HIGH ai procesului curent, în regiştrii LOW
ai procesului viitor, să devină imposibilă.
Dintre microprocesoarele RISC consacrate, cele din familia SPARC
(sistemele SUN) au implementat un set de maximum 32 de ferestre a câte 32 de
regiştri fiecare. Numărul mare de regiştri generali necesari pentru implementarea
conceptului de "register windows" implică reducerea frecvenţei de tact a
procesorului, fapt pentru care conceptul nu este implementat decât relativ rar în
microprocesoarele RISC comerciale. Studii statistice realizate încă din perioada
de pionierat, în special de către Halbert şi Kessler, arată că dacă procesorul
deţine 8 ferestre de regiştri, excepţiile WO şi WU vor apărea în mai puţin de 1%
din cazuri. Evident că supra-plasarea trebuie evitată în cadrul rutinei de tratare a
excepţiei, prin trimiterea / recepţionarea parametrilor proceselor în / din buffere
de memorie dedicate (stive). De precizat că setul larg de regiştri generali este
posibil de implementat la microprocesoarele RISC prin prisma tehnologiilor
VLSI (Very Large Scale Integration) disponibile (HC-MOS, ECL, GaAs etc.),
întrucât unitatea de comandă, datorită simplităţii ei, ocupă un spaţiu relativ
restrâns (la Berkeley RISC I de exemplu, ocupa doar aproximativ 6% din aria de
integrare). Restul spaţiului rămâne disponibil pentru implementarea unor
suporturi de comunicaţie interprocesor (ex. transputere INMOS), memorii
cache, logică de management al memoriei, set regiştri generali extins, arii
sistolice specializate de calcul (la DSP-uri, neuroprocesoare), acceleratoare
hardware etc.

3.2.1. DUALITATEA ARHITECTURĂ – APLICAŢIE:


IMPLEMENTAREA GESTIUNII STIVELOR DE DATE ASOCIATE
FUNCŢIILOR C

După cum se ştie, stiva de date asociată unei funcţii scrise în limbajul C
reprezintă o zonă de memorie unde sunt stocate toate variabilele locale şi
parametrii aferenţi respectivei funcţii (”activation record”). Fiecare funcţie C
are propriul context, păstrat în această structură de date specială. Stiva de
date se asociază în mod dinamic fiecărei funcţii în curs. Aşadar, o funcţie

109
poate avea la un moment dat mai multe stive (instanţe), doar una fiind însă
activă. Spre exemplu, recursivitatea se poate implementa facil în C tocmai
datorită acestei caracteristici. Structura stivei de date asociate unei funcţii C
este prezentată în figura următoare, corespunzător secvenţei de program de
mai jos.

int NoName (int a, int b)


{
int w,x,z;
/* Corpul funcţiei */
.
.
.
return y;
}

Figura. 3.6. Structura stivei de date asociate funcţiei NoName

RETURN VALUE: aici se plasează valoarea variabilei y, chiar înainte de


revenirea din funcţie. Acest câmp există şi dacă funcţia nu ar returna practic
nicio valoare.

110
RETURN ADDRESS (PC): reprezintă PC-ul de revenire în funcţia
apelantă.
DYNAMIC LINK: memorează adresa de început a stivei de date aferente
funcţiei apelante. În continuare, se va considera că registrul R6 (frame pointer)
va conţine adresa de început a stivei de date asociată funcţiei respective.
Notă. Unele microprocesoare (spre exemplu, cele din familia MIPS) dețin
și un așa numit registru frame pointer (RFP). Acesta pointează la adresa de bază
a stivei unei anumite funcții (subrutine) și nu-și modifică valoarea înscrisă pe
durata procesării subrutinei apelate, spre deosebire de registrul SP. Astfel,
parametrii care sunt transmiși subrutinei sunt situați la o distanță constantă
relativ la valoarea înscrisă în registrul RFP. Modul de adresare indexat, cu RFP
ca registru de bază, este ideal pentru transmiterea acestor parametri respectiv
pentru preluarea rezultatelor din stiva de date asociată subrutinei respective.
Aşadar funcţia NoName procesează asupra parametrilor trimişi de către
funcţia apelantă (a, b) respectiv asupra variabilelor locale ale acesteia (w,x,y).
Fireşte, ea trimite rezultatele (y) către funcţia apelantă prin stiva de date curentă.
În continuare se consideră că atunci când funcţia NoName este apelată, pointerul
RFP la stivele de date (R6, aici) va pointa la începutul stivei de date aferente
funcţiei.
Pentru a înţelege implementarea apelurilor/revenirilor funcţiilor, se
consideră următorul exemplu de program:

main()
{
int a;
int b;
.
.
b=NoName(a,10);
.
.
}
int NoName(int a, int b)
{
int w,x,y;
/* Corpul funcţiei */
.

111
.
.
return y;
}

Se consideră aici că stiva de date începe la o locaţie de memorie


determinată de către proiectanţii sistemului de operare şi creşte înspre adrese
crescătoare. Execuţia programului începe cu un apel al sistemului de operare
către funcţia “main”. În acest punct, stiva datelor funcţiei “main” se structurează
în memorie, iar registrul R6 (RFP) pointează la începutul ei. În translatarea
apelului unei funcţii, compilatorul generează automat cod maşină pentru a
înscrie o stivă de date în memorie. În translatarea revenirii dintr-o funcţie apelată
în funcţia apelantă, compilatorul generează automat cod pentru preluarea stivei
de date din memorie.
Apelul şi revenirea se fac în 4 paşi, aşa cum se prezintă în continuare:

1) Apelul funcţiei NoName

Se face prin asignarea b=NoName(a,10); Stiva de date a funcţiei main()


respectiv a funcţiei NoName(a,10) sunt prezentate în figura următoare.

Figura 3.7. Stivele de date asociate funcţiilor main() şi NoName(a,10)

112
Secvenţa compilată a apelului funcţiei NoName este următoarea:

ld R8, (R6)3; R8 a
st R8, (R6)8; a Stiva NoName
and R8, (R8), #0 ; R8 0
add R8, R8, #10; R8 10
st R8, (R6)9; (b=10) Stiva NoName
st R6, (R6)7; (R6)main Stiva NoName (Dynamic
Link)
add R6, R6, #5; R6 (R6) +5, actualizare nou început al
stivei de date
jsr NoName; apel funcţie, R7 PCnext şi
PC (NoName)
PCnext: ld R8, (R6)5
st R8, (R6)4

2) Startarea funcţiei apelate (NoName)

Începe cu instrucţiunea care salvează în stiva de date a funcţiei NoName


adresa de revenire în funcţia principală main(). Adresa de revenire se află
stocată în registrul R7 (conţine adresa următoare a instrucţiunii JSR
NoName)
st R7, (R6)1

3) Sfârşitul funcţiei apelate (NoName)

ld R8, (R6)7
st R8, (R6)0; Se memorează valoarea lui y în RET VALUE din stiva
de date.
ld R7, (R6)1; R7 RET ADDRESS
ld R6, (R6)2; R6 adresa de început a stivei de date aferente
funcţiei main ().
RET; PC adresa de revenire în funcţia main ().

4) Revenirea în funcţia apelantă (main)

113
JSR NoName; pe această instrucţiune se face revenirea (la finele
execuţiei acesteia).
PCnext: ld R8, (R6)5; R8 valoarea lui y din funcţia NoName
st R8, (R6)4; se face asignarea : b=NoName(a,10).

Observație: Stivele de date asociate funcţiilor C sunt structuri de tip


tablou, având o adresă de bază stocată în registrul RFP R6 şi un număr (variabil)
de elemente. Având în vedere frecvenţa deosebită a accesării acestor structuri de
date, modul de adresare indexat (Rbază + offset) este esenţial în facilitarea
manipulării datelor prin aceste structuri de tip tablou.

3.2.2. IMPLEMENTAREA RECURSIVITĂŢII

Se consideră şirul recurent liniar omogen de ordinul doi, cunoscut sub


numele de şirul lui Fibonacci: Fibo(n) = Fibo(n-1) + Fibo(n-2), Fibo(1) =
Fibo(0) = 1 (Se arată elementar, rezolvând ecuația caracteristică asociată și
identificând coeficienții soluției din condițiile inițiale, că Fibo(n)=
1+ 5 n 1− 5 n
( ) +( ) ; prima bază este celebra secțiune de aur, dată de împărțirea
2 2
unui segment AB, printr-un punct C, în medie și extremă rație, problemă
cunoscută de anticii greci). O implementare C recursivă a calcului elementului
Fibo(n) din cadrul acestui şir recurent este prezentată mai jos:

#include<stdio.h>
int Fibo(int n);
main ()
{
int in;
int numar;
printf (“Care termen din şir?”);
scanf (“%d”, &in);
numar = Fibo (in);
printf (“Termenul are valoarea %d\n”, numar);
}
int Fibo(int n)
{
if (n = = 0 || n = = 1)

114
return 1;
else
return (Fibo(n-1) + Fibo(n-2));
}

Esenţa implementării recursivităţii constă în manipularea stivelor de date


asociate funcţiilor dinamice (adică funcţiilor în curs de execuţie la un moment
dat). În continuare se prezintă, la nivel de cod obiect, implementarea funcţiei
Fibo (int n), cu referire la stivele de date accesate.

Figura 3.8. Stiva de date asociată funcţiei Fibo(n)

Fibo:
st R7, (R6)1; salvează PC-ul de revenire în stiva de date.
ld R8, (R6)3; R8 valoare “n”
brz Fib_end
add R8,R8, # -1
brz Fib_end
ld R8, (R6)3; R8 n
add R8,R8, # -1; R8 n-1
st R8, (R6)8; pune (n-1) ca parametru în stiva funcţiei Fibo(n-1).
st R6, (R6)7; pune adresa de început a stivei funcţiei Fibo(n) în stiva
funcţiei Fibo(n-1).

115
add R6, R6,#5; pune în R6 noua adresă de început aferentă stivei de
date a lui Fibo(n-1).
JSR Fibo; apel funcţie Fibo (recursiv)
ld R8, (R6)5; R8 valoarea returnată de Fibo (n-1)
st R8, (R6)4; memorează variabila locală aferentă stivei de date a lui
Fibo (n).
ld R8, (R6)3; R8 (n-1)
add R8, R8, #-1; R8 (n-2)
st R8, (R6)8
st R6, (R6)7
add R6, R6, #5; pregăteşte stiva de date a lui Fibo (n-2).
JSR Fibo; apel recursiv
ld R8, (R6)5; R8 Fibo (n-2)
ld R1, (R6)4; R1 Fibo (n-1)
add R8, R8, R1; R8 Fibo (n-1) + Fibo (n-2)
st R8, (R6)0; Fibo (n) RET VALUE
ld R6, (R6)2; revenire în stiva precedentă
RET
Fib_end:
and R8, R8, #0
add R8, R8, #1
st R8, (R6)0
ld R6, (R6)2
RET

Observație: Legătura între arhitectura unui microprocesor si aplicaţiile


scrise în limbaje de nivel înalt este una complexă şi extrem de subtilă. Există
microarhitecturi de calcul optimizate, în mod special în vederea rulării eficiente
a unor clase de aplicaţii bine precizate, scrise în anumite limbaje de nivel înalt
(spre exemplu, microprocesoare Java implementate pe baza mașinii virtuale
Java).

3.3. ARHITECTURA SISTEMULUI DE MEMORIE LA


PROCESOARELE RISC

116
Principalele caracteristici ale sistemului de memorie aferent unui sistem
RISC sunt prezentate succint în continuare. Astfel, în general, microprocesoarele
RISC deţin un management intern de memorie (MMU - Memory Management
Unit) în majoritatea implementărilor. MMU-ul are rolul de a translata adresa
virtuală emisă de către microprocesor, într-o aşa numită adresă fizică de acces la
memoria principală şi respectiv de a asigura un mecanism de control şi protecţie
- prin paginare sau / şi segmentare a memoriei - a accesului la memorie. În
general, MMU lucrează prin paginare în majoritatea implementărilor cunoscute,
oferind şi resursele hardware necesare mecanismelor de memorie virtuală. De
asemenea, sistemele RISC deţin memorii cache interne şi externe, cu spaţii în
general separate pentru instrucţiuni şi date (arhitecturi Harvard de memorie) şi
structuri de tip DWB (Data Write Buffer), cu rol de gestionare a scrierii efective
a datelor în memoria sistem, prin degrevarea totală a procesorului propriu - zis.
Pentru a asigura coerenţa şi consistenţa datelor scrise în cache, acestea trebuie
scrise la un moment dat şi în memoria principală. În scopul eliberării
microprocesorului de această activitate, DWB capturează data şi adresa emise de
către microprocesor şi execută efectiv scrierea în memoria cache de date sau în
cea principală, permiţând astfel microprocesorului să-şi continue activitatea fără
să mai aştepte până când data a fost efectiv scrisă in memoria cache/principală.
Aşadar, DWB se comportă ca un mic procesor de ieşire, lucrând în paralel cu
microprocesorul. Arhitectura de principiu a memoriei unui sistem RISC este
caracterizată în Figura 3.9:

Figura 3.9. Arhitectura de memorie tip Harvard la procesoarele RISC


În afara spaţiilor separate de instrucţiuni şi date şi a procesorului de ieşire

117
(DWB) nu există alte probleme majore specifice microprocesoarelor RISC, în
arhitectura sistemelor de memorie. În principiu, ierarhizarea sistemului de
memorie este aceeaşi ca şi la sistemele CISC (memorii cache, memorie
virtuală). Nu intrăm aici în detalii teoretice legate de organizarea cache-urilor, a
mecanismelor de memorie virtuală etc., întrucât aceste probleme sunt similare
cu cele de la sistemele CISC, fiind deci foarte cunoscute şi bine documentate
bibliografic. De altfel, acestea au fost prezentate anterior, în mod sintetic, chiar
în această lucrare.
Problema majoră a sistemelor de memorie pentru ambele variante constă în
decalajul tot mai accentuat între performanţa microprocesoarelor, care creşte
anual cu 50 -75% şi respectiv performanţa tehnologică a memoriilor (latenţa),
care are o rată de creştere de doar 5% - 7% pe an (practic, timpul de acces al
memoriilor DRAM descrește în acest ritm). Prin urmare acest "semantic gap"
microprocesor - memorie reprezintă o provocare esenţială pentru îmbunătăţirea
performanţelor arhitecturilor de memorie aferente arhitecturilor actuale de
microprocesoare. În caz contrar, având în vedere frecvenţele de tact actuale ale
microprocesoarelor (1-5 GHz) şi timpii de acces ai memoriilor tip DRAM (cca.
30 ns la ora actuală - 2016), sistemul de calcul va funcţiona practic "cu frâna de
mână trasă". Cercetarea mai aprofundată a acestei interfeţe procesor - cache,
într-un mod pragmatic şi cantitativ, va constitui un obiectiv major în domeniul
sistemelor de calcul.

3.4. PROCESAREA PIPELINE ÎN CADRUL PROCESOARELOR


SCALARE

Cea mai importantă caracteristică arhitecturală a acestor microprocesoare


RISC scalare o constituie procesarea pipeline a instrucţiunilor şi datelor. În
fapt, aproape toate celelalte caracteristici arhitecturale ale RISC-urilor, anterior
menționate, au scopul de a adapta structura procesorului la procesarea pipeline.
Acest concept a migrat de la sistemele mari de calcul (mainframes), la
microprocesoare, datorită progresului tehnologiei microprocesoarelor,
caracterizat prin "legea lui Gordon Moore" (care afirmă, într-o formă
actualizată, că densitatea de integrare a circuitelor integrate digitale se dublează
cam la 18 luni). Această lege empirică a fost formulată în anul 1965 de Moore
(co-fondator Intel) și caracterizează de atunci evoluția microprocesoarelor și

118
memoriilor, până astăzi. Toate supercalculatoarele au implementat acest concept
al procesării pipeline. Din punct de vedere istoric primul calculator pipeline
viabil a fost - la acea vreme - supersistemul CDC 6600 (firma Control Data
Company, şef proiect Seymour Cray,1964).

3.4.1. DEFINIREA CONCEPTULUI DE ARHITECTURĂ PIPELINE


SCALARĂ

Tehnica de procesare pipeline reprezintă o tehnică de procesare paralelă a


informaţiei, prin care un proces secvenţial este divizat în subprocese, fiecare
subproces fiind executat într-un segment special dedicat (stagiu de procesare,
fază) şi care operează în paralel cu celelalte segmente, aferente celorlalte
procese. Fiecare segment execută o procesare parţială a informaţiei. Rezultatul
obţinut în segmentul i este transmis, dacă se poate chiar în tactul următor, spre
procesare segmentului (i+1). Rezultatul final este obţinut numai după ce
informaţia a parcurs toate segmentele, de obicei la ieşirea ultimului segment.
Denumirea de pipeline provine de la analogia cu o bandă industrială de
asamblare (uzinele Ford au implementat, pentru prima dată, în cadrul producției
de automobile, acest concept). Este caracteristic acestor tehnici faptul că
diversele procese se pot afla în diferite faze de prelucrare, în cadrul diverselor
segmente, simultan. Prin urmare, se exploatează un paralelism temporal (în
timp), la nivelul fazelor succesive de procesare. Suprapunerea procesărilor este
posibilă prin asocierea unui registru de încărcare (latch, implementat cu bistabili
de tip D, comandați cu același tact; Bistabilul de tip D are ecuația de stare
Q(n+1)=D, derivată ca și caz particular din ecuația de stare a bistabilului J-K
− − _
master-slave, Q(n + 1) = J Q(n) + K Q(n) , pentru J = K = D ) fiecărui segment din
pipeline. Registrele produc o separare între segmente astfel încât fiecare segment
să poată prelucra date separate (vezi Figura 3.10).

Figura 3.10. Structură de procesare tip pipeline

119
În Figura 3.10 s-au notat prin Ti - regiştrii tampon (latch), iar prin Ni -
nivelele de prelucrare (combinaţionale sau chiar secvenţiale). Este evident că
nivelul cel mai lent de prelucrare va stabili viteza de lucru a benzii de asamblare.
Aşadar, se impune în acest caz partiţionarea unui eventual proces mai lent în
sub-procese, cu timpi de procesare cvasi-egali şi interdependenţe minime. Există
două soluţii principiale, discutate în literatură, de a compensa întârzierile diferite
pe diversele nivele de prelucrare:

Figura 3.11. Balansarea unei structuri pipeline prin divizarea nivelului lent

Prima soluţie, zisă de "balansare a benzii", necesită circuite suplimentare şi


poate fi realizată doar cu condiţia ca sarcinile alocate respectivului nivel să poată
fi descompuse, ca în Figura 3.11. O a doua soluţie, se bazează pe conectarea în
paralel a unui alt nivel, cu aceeaşi întârziere ca cel mai lent. Şi aici sunt necesare
circuite suplimentare de demultiplexare a informației de la intrarea nivelului. În
plus, apare problema sincronizării şi controlului nivelelor care lucreează în
paralel. Soluţia aceasta este utilizată atunci când sarcinile alocate nivelului mai
lent nu mai pot fi descompuse (vezi Figura 3.12).

Figura 3.12. Balansare prin conectarea unui nivel lent, în paralel

120
În Tact (i) informaţia se trimite spre procesare nivelului următor, pe calea
A. În Tact (i+1) informaţia se trimite în mod analog pe calea B, implementându-
se deci o demultiplexare.
Se defineşte rata de lucru (R) a unei benzi de asamblare (structuri
pipeline), ca fiind numărul de procese executate în unitatea de timp T (ciclu
maşină sau tact). Considerând m posturi de lucru, prin care trec n procese,
rezultă că banda le va executa într-un interval de timp Dt = (m + n -1) * T.
Normal, având în vedere că m*T este timpul de "setup" al benzii, adică timpul
necesar terminării primului proces. Aşadar, rata de execuţie a unei benzi prin
care trec n procese este:
Rn = n/ (m+n-1)T
Rata ideală este de un proces per ciclu, întrucât:
R = n→∞
lim Rn = 1/T

După cum se va arăta în continuare, această rată teoretică nu se va putea


atinge în practică, nu numai datorită faptului că se prelucrează un număr finit de
procese şi datorită timpului de "setup", ci, mai ales, datorită unor blocaje
(stagnări) în funcţionarea normală a benzii, numite hazarduri [Hen11].
În continuare, prezentăm un model analitic care generează numărul optim
de stagii al unei structuri pipeline, pe baza [Hay98]. Se definește Raportul
Performanță-Cost (RPC) al unei structuri pipeline având m stagii (nivele) de
f
procesare, ca fiind RPC = , unde f este frecvența tactului care pilotează
K
structura, iar K reprezintă costul hardware al acesteia. Notăm perioada de tact cu
1
TC = . Să considerăm că latența unei structuri nepipeline-izate echivalente este
f
a. În acest caz putem scrie:
a
TC = +b,
m
unde b este întârzierea datorată unui registru de tip latch dintr-un stagiu pipeline.
Rezultă că K=cm+d, unde c reprezintă costul registrului latch dintr-un stagiu al
structurii pipeline, iar d este costul logicii combinaționale de prelucrare din
întreaga structură pipeline. Rezultă că:
1 m u ( m)
RPC (m) = = =
KTC bcm + (ac + bd )m + ad v(m)
2

Evident că dorim să aflăm valoarea lui m pentru care RPC este maximum.
Pentru a calcula derivata lui RPC vom utiliza binecunoscuta relație de derivare a
raportului a două funcții u și v:

121
'
u u ' v − uv
( )' =
v v2
Rezultă deci că:
1 m(2bcm + ac + bd )
RPC ' = −
bcm + (ac + bd )m + ad [bcm 2 + (ac + bd )m + ad ] 2
2

Egalând acum (RPC(m))’=0 și rezolvând ecuația se obține:


ad
moptim = , reprezentând numărul optim de stagii al unei structuri pipeline de
bc
procesare (în practică, se va considera partea întreagă). Lăsăm în seama
cititorului să demonstreze că moptim este un punct de maxim al funcției RPC(m).
Considerând a=500 (ns), b=20 (ns) c=1, d=1 se obține funcția
m
RPC ( m) = , a cărei reprezentare grafică este prezentată în
20m + 520m + 500
2

continuare (cu ajutorul programului Wolfram Alpha). Se observă că m=5 nivele


pipeline este optimal, atât din formula lui moptim cât și din reprezentarea grafică.

Figura 3.12.1. Reprezentarea grafică a funcției RPC(m)

3.4.2. PRINCIPIUL DE PROCESARE ÎNTR-UN PROCESOR PIPELINE

Procesarea pipeline a instrucţiunilor reprezintă o tehnică de procesare prin


intermediul căreia fazele (ciclii) aferente unor multiple instrucţiuni consecutive,
sunt suprapuse în timp. Se înţelege printr-o fază aferentă unei instrucţiuni
maşină, o prelucrare atomică (care nu poate fi întreruptă sau divizată în entități
mai mici) a informaţiei, care se desfăşoară după un algoritm implementat în
hardware (firmware) şi care durează unul sau mai mulţi tacţi. În acest sens, se
exemplifică: faza de aducere a instrucţiunii din memorie (instruction fetch), faza
de decodificare, faza de execuţie, faza de citire / scriere dată etc. Arhitectura
microprocesoarelor RISC este mai bine adaptată la procesarea pipeline decât cea

122
a sistemelor convenţionale de tip CISC, datorită instrucţiunilor de lungime fixă,
a modurilor de adresare specifice, a structurii interne bazate pe registre generale
etc. Microprocesoarele RISC uzuale deţin o structură pipeline de instrucţiuni
care operează pe numere întregi, pe 4 - 6 nivele logice. De exemplu,
microprocesoarele MIPS au următoarele 5 nivele tipice:
1. Nivelul IF (instruction fetch) - se calculează adresa instrucţiunii ce
trebuie citită din cache-ul de instrucţiuni sau din memoria principală şi se aduce
instrucţiunea în buffer-ul de prefetch al procesorului (o structură tip FIFO).
2. Nivelul RD (ID – instruction decode) - se decodifică instrucţiunea adusă
şi se citesc operanzii sursă din setul de regiştri generali. În cazul instrucţiunilor
de salt, pe parcursul acestei faze se calculează adresa de salt condiționat.
3. Nivelul ALU (Arithmetic and Logic Unit) - se execută operaţia ALU
asupra operanzilor selectaţi în cazul instrucţiunilor aritmetico-logice; se
calculează adresa de acces la memoria de date pentru instrucţiunile LOAD /
STORE (acces la memoria de date). Uneori, în cazul unei instrucțiuni de tip
LOAD R5, (R7) offset sau STORE R5, (R7) offset, adunarea R7+offset poate fi
înlocuită cu operația logică R7 SAU offset, care este mai rapidă. Evident, această
înlocuire a adunării aritmetice cu un SAU logic este echivalentă doar dacă
valoarea din R7 este un multiplu de (offsetmax+1).
4. Nivelul MEM - se accesează memoria cache de date sau memoria
principală, însă numai pentru instrucţiunile LOAD / STORE (cu referire la
memorie). Acest nivel, pe funcţia de citire din memorie, poate pune probleme,
datorate neconcordanţei între rata de procesare internă a CPU şi timpul de acces
la memoria principală. Rezultă deci că într-o structură pipeline cu N nivele de
procesare a instrucțiunii, memoria ar trebui să fie, în principiu, de N ori mai
rapidă decât într-o structură de calcul convenţională (pentru a nu avea stagnări
ale procesării structurii). Acest lucru se realizează prin implementarea de
arhitecturi rapide de memorie (cache, memorii cu acces întreţesut etc.) Desigur
că un ciclu cu MISS în cache pe acest nivel (ca şi pe nivelul IF, de altfel), va
determina stagnarea temporară a acceselor la memorie sau chiar a procesării
pipeline interne. La scriere, problema aceasta nu se pune datorită procesorului de
ieşire specializat DWB care lucrează în paralel cu procesorul central, după cum
deja am arătat.
5. Nivelul WB (write back) - se scrie rezultatul ALU sau data citită din
memorie (în cazul unei instrucţiuni LOAD) în registrul destinaţie din setul de
regiştri generali ai microprocesorului.
Prin urmare, printr-o astfel de procesare se urmăreşte o rată ideală de o

123
instrucţiune / ciclu maşină, ca în Figura 3.13, deşi după cum se observă, timpul
de execuţie pentru o instrucţiune dată nu se reduce.

Figura 3.13. Principiul procesării pipeline într-un procesor RISC

Se observă imediat necesitatea suprapunerii a două nivele concurenţiale:


nivelul IF şi respectiv nivelul MEM, ambele cu referire la memorie. În cazul
microprocesoarelor RISC, această situaţie se rezolvă deseori prin legături
(busuri) separate între procesor şi memoria cache de date, respectiv cea de
instrucţiuni (arhitectură Harvard). În cazul în care memoria cache este unificată
pe instrucțiuni și date (arhitectură Princeton), faza IF (pre-fetch) se amână (în
speranța că în ciclul următor faza MEM va fi inactivă), dându-se astfel prioritate
fazei MEM aferentă instrucțiunii cu referire la memorie. Desigur că această
prioritizare degradează performanța structurii pipeline, printr-o stagnare de cel
puțin un ciclu.
Deşi această împărţire pe 5 nivele este caracteristică mai multor
microprocesoare RISC, ea are o deficienţă importantă şi anume că nu orice
instrucţiune trece efectiv prin toate cele 5 nivele de procesare. Astfel, nivelul
MEM este exploatat doar de către instrucţiunile LOAD / STORE, nivelul ALU
de către instrucţiunile aritmetico-logice şi LOAD / STORE (calcul adresă de
acces), iar instrucţiunile de comparare sau memorare (STORE) nu folosesc
nivelul WB. Probabil că asemenea observaţii au determinat proiectanţii anumitor
microprocesore RISC (de ex. microprocesorul HARP - Hatfield Advanced RISC
Processor, proiectat la Universitatea din Hertfordshire, Marea Britanie), să
comprime nivelele ALU şi MEM într-unul singur. In acest caz, calculul
adreselor de memorie se face în nivelul fazei RD, prin mecanisme care reduc
acest timp de calcul.
În literatură se citează un model de procesor numit super-pipeline. Acesta
este caracterizat printr-un număr relativ mare al nivelelor de procesare pe

124
instrucțiunile în virgulă fixă (20-22 stagii pipeline). Desigur că în acest caz
detecţia şi corecţia hazardurilor este mai dificilă, după cum se va arăta în
continuare. Arhitecturile super-pipeline se pretează la tehnologiile cu grade de
împachetare reduse, unde nu este posibilă multiplicarea resurselor hardware, în
schimb sunt caracterizate prin viteze de comutaţie ridicate (ECL, GaAs). O
asemenea arhitectură caracterizează, spre exemplu, procesoarele din familia
DEC Alpha (Digital Equipment Corporation, actualmente înglobată în
concernul Hewlett Packard). Avantajul principal al arhitecturilor de tip super-
pipeline este că permit frecvenţe de tact deosebit de ridicate (800 - 5000 MHz la
nivelul tehnologiilor actuale, 2012), aspect normal, având în vedere super -
divizarea stagiilor de procesare pipeline-izate.

3.4.3. STRUCTURA PRINCIPIALĂ A UNUI PROCESOR RISC

În continuare, se prezintă o structură hardware de principiu a unui procesor


RISC pipeline, compatibil cu modelul de programare al microprocesorului RISC
Berkeley I, anterior prezentat. De remarcat că structura permite procesarea
pipeline a instrucţiunilor, adică, teoretic cel puțin, atunci când o instrucţiune se
află într-un anumit nivel de procesare, instrucţiunea următoare se află în nivelul
anterior. Stagiile de interconectare ale nivelelor structurii (IF / ID, ID / EX, EX /
MEM, MEM / WB) sunt implementate sub forma unor regiştri de încărcare
(latch, un registru de încărcare format din bistabili de tip D, după cum am mai
precizat de altfel), actualizaţi sincron, cu fiecare nou ciclu de procesare.
Memorarea anumitor informaţii de la un nivel la altul este absolut necesară,
pentru ca informaţiile conţinute în formatul instrucţiunii curente să nu se piardă,
prin suprapunerea fazelor de procesare pipeline.
În acest sens, să ne imaginăm, spre exemplu, ce s-ar întâmpla dacă câmpul
DEST nu ar fi memorat succesiv din nivel în nivel şi rebuclat la intrarea setului
de regiştri generali (vezi Figura 3.14). Utilitatea acestui câmp devine oportună
abia în faza WB, când şi data de înscris în setul de regiştri generali devine
disponibilă (cazul instrucţiunilor aritmetico-logice sau STORE). Să remarcăm că
setul de regiştri generali trebuie să permită simultan două citiri şi o scriere.
Schema internă a unui astfel de set de regiştri generali (REGISTER FILE), de
tip multiport (cu două porturi de citire şi unul de scriere), este prezentată în cele
trei figuri următoare.

125
Figura 3.13.1. Schema bloc a setului de regiștri biport

126
Figura 3.13.2. Setul de regiștri biport – partea de citire

127
Figura 3.13.3. Setul de regiștri biport – partea de scriere

Revenind la Figura 3.14, multiplexorul de la intrarea ALU are rolul de a


genera registrul sursă 2, în cazul instrucţiunilor aritmetico-logice, respectiv
indexul de calcul adresă (constantă pe 13 biţi, cu extensie semn), în cazul
instrucţiunilor LOAD / STORE, sau constanta imediată, în cazul instrucțiunilor
aritmetice cu mod de adresare imediat. Sumatorul SUM 2 calculează adresa de
salt în cazul instrucţiunilor de salt condiționat (branch) de tip Taken, după
formula:
PCnext = (PC) + Ext.semn (IR18 - 0)
În cazul unor instrucțiuni de salt condiționat de tip Not Taken, PCnext =
(PC) + 4 (operație realizată prin sumatorul sum1). Valoarea aferentă a adresei
de salt se va înscrie în registrul PC, prin intermediul multiplexorului 2:1
corespunzător.

128
Figura 3.14. Schema de principiu a unui procesor RISC pipeline
Multiplexorul 2:1 de la ieşirea nivelului MEM / WB multiplexează
rezultatul ALU, în cazul unei instrucţiuni aritmetico-logice, respectiv data citită
din memoria de date, în cazul unei instrucţiuni de tip LOAD. Ieşirea acestui
multiplexor se va înscrie în registrul destinaţie codificat în instrucţiune. Evident
că întreaga structură este comandată de către o unitate de control. Detalii
interesante și instructive despre proiectarea unei asemenea unităţi de control sunt
date în [Hen11].

3.4.4. PROBLEMA HAZARDURILOR ÎN PROCESOARELE RISC

Hazardurile constituie acele situaţii care pot să apară în procesarea


pipeline şi care pot determina blocarea (stagnarea) procesării, având deci o
influenţă negativă asupra ratei de execuţie a instrucţiunilor. Conform unei
clasificări consacrate, aceste hazarduri sunt de trei categorii : hazarduri
structurale, de date (dependențe de date) şi de ramificaţie (branches)
[Hen11].

129
3.4.4.1. HAZARDURI STRUCTURALE (HS): PROBLEME IMPLICATE
ŞI SOLUŢII

Sunt determinate de conflictele la resurse hardware comune, adică atunci


când mai multe procese simultane, aferente mai multor instrucţiuni în curs de
procesare, accesează o resursă hardware comună. Pentru a le elimina prin
hardware, se impune de obicei multiplicarea acestor resurse. De exemplu, un
procesor care are un set de regiştri generali de tip uniport şi în anumite situaţii
există posibilitatea ca două procese (instrucțiuni) să dorească să scrie în acest set
simultan. Se prezintă mai jos (Figura 3.15) o structură ALU implementată la
microprocesorul RISC superscalar HARP (Universitatea Hertfordshire, Anglia),
care permite 4 operaţii ALU simultane. Prin partiţionarea timpului afectat
nivelului WB în două jumătăți, în cadrul acestui nivel se pot face două scrieri (în
prima jumătate a nivelului WB se înscrie în setul de regiştri conţinutul căii A, iar
în a doua parte a nivelului WB se înscrie conţinutul căii B). În principiu, un
astfel de procesor poate să execute 4 instrucţiuni simultan, cu condiţia ca numai
două dintre ele să aibă nivelul WB activ. Evident că cele 4 seturi de regiştri
generali deţin conţinuturi identice în orice moment. Deci, prin multiplicarea
resurselor hardware, s-a creat posibilitatea execuţiei mai multor operaţii
(instrucţiuni), fără a avea conflicte la resurse.
O altă situaţie de acest fel, după cum deja am mai arătat, poate consta în
accesul simultan la memorie a două procese distincte: unul de aducere a
instrucţiunii (IF), iar celălalt de aducere a operandului sau de scriere a
rezultatului, în cazul unei instrucţiuni LOAD / STORE respectiv (nivelul MEM).
După cum am mai subliniat, această situaţie particulară se rezolvă, în general,
printr-o arhitectură Harvard a busurilor şi cache-urilor. Totuşi, există
microprocesoare care deţin busuri şi cache-uri unificate pe instrucţiuni şi date
(IBM Power PC 601, de exemplu). În acest ultim caz este necesară prioritizarea
celor două procese (MEM mai prioritar decât IF). Considerăm un procesor
pipeline cu un singur cache, unificat pe spațiile de instrucțiuni și date, iar
legătura între procesor și cache se face printr-un bus unic (adrese, date, comenzi,
stări). Dacă un procentaj de px100% dintre instrucțiunile dinamice sunt cu
referire la memorie (Load/Store), p ∈ [0, 1], atunci rata ideală de procesare este
de (1/1+x) instrucțiuni per ciclu (justificați!) Un CPU pipeline cu cache-uri
separate pe spațiile de instrucțiuni și date, fiecare cache fiind legat de CPU
printr-un bus propriu, ar avea o rată ideală de procesare de o instrucțiune per
ciclu (nu există coliziuni între fazele IF și MEM din pipeline). În [Vin00] Par.

130
4.1.1. se prezintă un model mai realist, bazat pe conceptul de vector de
coliziune, de determinare cantitativă a scăderii de performanţă pentru o
arhitectură superscalară având magistrale unificate (şi implicit cache-uri
unificate pe spațiile de instrucțiuni și date), faţă de una cu magistrale separate (şi
implicit cache-uri separate). S-au avut în vedere exclusiv procesele de coliziune,
implicate de către primul tip de arhitectură.

Figura 3.15. Structură de procesare multiplă


Să considerăm acum, fără a reduce din generalitate, o structură pipeline cu 5
nivele de procesare, având timpul de"setup" de 7 cicli, descrisă în funcţionare de
Tabelul 3.3.

Ciclu/ T1 T2 T3 T4 T5 T6 T7
Nivel
N1 X
N2 X X
N3 X X
N4 X X
N5 X
Tabelul 3.3. Descrierea funcţionării unei structuri pipeline cu 5
nivele

Un X în tabel semnifică faptul că în ciclul Ti, nivelul Nj este activ, adică


procesează informaţii. Să mai considerăm că această structură pipeline
corespunde unui anumit proces (spre exemplu, procesarea unei instrucţiuni). Se

131
observă că un alt proces de acest tip, nu poate starta în ciclul următor (T2),
datorită coliziunilor care ar putea să apară între cele două procese pe nivelul
(N2, T3) şi respectiv (N3, T4). Mai mult, următorul proces n-ar putea starta nici
măcar în ciclul T3, din motive similare, de coliziune cu procesul anterior în
nivelul (N4, T5). În schimb procesul următor ar putea starta în ciclul T4, fără a
produce coliziuni sau hazarduri structurale cum le-am denumit, deci la doi cicli
după startarea procesului curent.

Figura 3.15.1. Structura de procesare pipeline aferentă Tabelului 3.3

Se defineşte vectorul de coliziune al unei structuri pipeline având timpul de


setup de (N+1) cicli, ca fiind un vector binar pe N biţi, astfel: dacă bitul i, i∈ {0,
1, ..., N-1} este 1 logic, atunci procesul următor nu poate fi startat după i cicli de
la startarea procesului curent, iar dacă bitul i este 0 logic, atunci procesul
următor poate fi startat după i cicli, fără a produce coliziuni cu procesul curent.
Se observă, spre exemplu, pentru structura pipeline anterioară, că vectorul de
coliziune este 110000, însemnând deci că procesul următor nu trebuie startat în
momentele (tactele) T2 sau T3, în schimb poate fi startat fără probleme oricând
după aceea.
Se pune problema: cum trebuie gestionată (alimentată) o structură pipeline
dată, caracterizată printr-un anumit vector de coliziune, astfel încât să se obţină
o rată de procesare (procese / ciclu) maximă, evident fără a se produce coliziuni?
Considerând o structură pipeline cu timpul de "setup" de 8 cicli şi având
vectorul de coliziune 1001011, ar trebui procedat ca în figurile următoare
(Figurile 3.16, 3.17):

132
Figura 3.16 Principiul de lansare procese într-o structură pipeline cu hazarduri
structurale

Figura 3.17. Graful de optimizare într-o structură pipeline

Vectorul unei stări Sj, se obţine după relaţia:


Vj = (VC) V (Vi*(m)) unde
V = SAU logic
VC = vector coliziune
Vi*(m) = vectorul Vi deplasat logic la stânga cu (m) poziţii binare

133
Figura 3.18. Tranziția dintr-o stare, în alta
Pentru maximizarea performanţei, se pune problema ca pornind din starea
iniţială a grafului, să se determine un drum închis în graf, cu proprietatea ca
raportul NS / L să fie maximum, unde NS = numărul de stări (procese,
instrucțiuni) al drumului, iar L = lungimea totală a drumului (latența). În cazul
anterior considerat, avem L = 3 + 5 + 3 + 2 = 13 cicli, iar NS = 4 stări. Printr-o
astfel de gestionare a structurii, se evită coliziunile şi se obţine o performanţă
optimă de 4 procese în 13 cicli, adică 0.31 procese / ciclu. De menţionat că o
structură convenţională ar procesa doar 0.125 procese / ciclu. Nu întotdeauna
startarea procesului următor imediat ce acest lucru devine posibil ("greedy
strategy"), duce la o performanţă maximă. Un exemplu în acest sens ar fi o
structură pipeline cu vectorul de coliziune asociat 01011. Este adevărat însă că o
asemenea strategie, de tip greedy, conduce la dezvoltarea unui algoritm mai
simplu.
Este clar că performanţa maximă a unei structuri pipeline (un proces/ciclu)
s-ar obţine numai în ipoteza alimentării ritmice, la fiecare ciclu, cu date de
intrare. În caz contrar, gestiunea structurii se va face pe un drum diferit de cel
optim în graful vectorilor de coliziune. Pe baza unei tehnici elementare atribuită
lui Patel şi Davidson (1976), se arată că prin modificarea tabelei aferente
structurii pipeline în sensul introducerii unor întârzieri, se poate îmbunătăţi
performanţa benzii de asamblare.
În Par. 4.2 al cărții [Vin00] s-a propus dezvoltarea unui model teoretic de
evaluare a performanţei arhitecturilor pipeline cu execuţii multiple, bazat pe
modelarea acestor arhitecturi folosind automate finite de stare (Finite State
Machine). În particular, s-a aplicat acest model în scopul evaluării comparative a
procesoarelor RISC superscalare având busuri şi cache-uri unificate, respectiv
separate, pe spațiile de Instrucțiuni / Date. Recomandăm cititorului parcurgerea
acestui studiu, pentru aprofundarea conceptului de vector de coliziune precum și
a aplicațiilor sale.
Să considerăm acum o structură pipeline bifuncţională, capabilă să execute
două tipuri de procese (instrucțiuni): P1 şi respectiv P2. Aceste procese sunt
descrise prin tabele adecvate, în care se arată ce nivele solicită procesul în

134
fiecare ciclu. Este clar că aceste procese vor fi mai dificil de controlat, în
comparație cu un singur proces. Pentru controlul acestora prin structură, este
necesar a fi determinaţi mai întâi vectorii de coliziune corelați şi anume: VC(P1,
P1) , VC(P1, P2), VC(P2, P1) şi VC(P2, P2), unde prin VC(Pi, Pj) am notat
vectorul de coliziune între procesul curent Pi şi procesul următor Pj. Odată
determinaţi aceşti vectori, în baza tabelelor de descriere aferente celor două
procese, controlul structurii s-ar putea face prin schema de principiu din Figura
3.19.

Figura 3.19. Controlul unei structuri pipeline bifuncţionale

Iniţial, registrul "P1 control" conţine VC(P1, P1), iar registrul "P2 control"
conţine VC(P2, P2). Procesul următor care se doreşte a fi inițiat în structură va
face o cerere de startare către unitatea de control. Cererea va fi acceptată sau
respinsă, după cum bitul cel mai semnificativ al registrului de control este 0
logic sau 1 logic, respectiv. După fiecare iteraţie, registrul de control se va
deplasa logic cu o poziţie la stânga, după care se execută un SAU logic între
conţinutul registrului de control, căile (A) şi (B) respectiv căile de date (C) şi
(D), cu înscrierea rezultatului în registrul de control. Se observă în acest caz că
gestionarea proceselor se face după o strategie de tip "greedy".

3.4.4.2. HAZARDURI DE DATE: DEFINIRE, CLASIFICARE, SOLUŢII


DE EVITARE A EFECTELOR DEFAVORABILE

Aceste hazarduri apar atunci când o instrucţiune depinde de rezultatele unei

135
instrucţiuni anterioare în banda de asamblare (structura pipeline de procesare).
Pot fi la rândul lor clasificate în trei categorii, funcție de ordinea acceselor de
citire respectiv scriere, în cadrul instrucţiunilor.
Considerând instrucţiunile i şi j succesive, hazardul RAW (dependența de
date de tip Read After Write) apare atunci când instrucţiunea j încearcă să
citească o sursă, înainte ca instrucţiunea i să scrie rezultatul în aceasta.
Instrucțiunea i are deci ca destinație o sursă a instrucțiunii j. Apare deosebit de
frecvent în programe și deci, implicit în implementările actuale de procesoare
pipeline. Practic, această dependență caracterizează gradul intrinsec de
secvențialitate a programelor și reprezintă o limitare fundamentală, dată în fond
de legea lui Amdahl. Să considerăm secvenţa de instrucţiuni de mai jos,
procesată într-o structură pipeline pe 5 nivele, ca în figura următoare. Se observă
că data ce urmează a fi incărcată în R5 este disponibilă doar la finele nivelului
MEM aferent instrucţiunii I1, prea târziu pentru procesarea corectă a
instrucţiunii I2, care ar avea nevoie de această dată cel târziu la începutul
nivelului său ALU. Aşadar, pentru o procesare corectă, I2 trebuie stagnată cel
puțin cu un ciclu maşină.

Figura 3.20. Hazard RAW în cazul unei instrucţiuni LOAD


Această stagnare se poate realiza prin software, de exemplu prin inserarea
unei instrucţiuni NOP (No OPeration) între I1 şi I2 (umplerea "load delay slot"-
ului - LDS) sau, și mai bine (!), a unei alte instrucţiuni utile, independentă de
instrucţiunea I1. O altă modalitate de rezolvare constă în stagnarea hardware a
instrucţiunii I2, stagnare determinată de detecţia hazardului RAW de către
unitatea hardware de control. Realizarea întârzierii hardware se poate baza pe
tehnica de "scoreboarding", propusă pentru prima dată de către Seymour Cray
(1964 – sistemul de calcul CDC 6600). Prin această tehnică, se impune ca
fiecare registru din setul de regiştri logici al procesorului să aibă un "bit de scor"
asociat. Dacă bitul de scor este zero, registrul respectiv este disponibil
(conținutul său poate fi accesat), iar dacă bitul este 1, registrul respectiv este

136
ocupat. Dacă pe un anumit nivel al procesării este necesar accesul la un anumit
registru având bitul de scor asociat pe 1, respectivul nivel va fi întârziat,
permiţându-i-se accesul doar când bitul respectiv a fost şters de către procesul
care l-a setat (Figura 3.21a, Figura 3.21.b). De remarcat că ambele soluţii bazate
pe stagnarea fluxului (software - NOP sau hardware - semafoare) au acelaşi
efect defavorabil asupra performanţei, implicând stagnări ale fluxului pipeline.

Figura 3.21a. Detecţie hazard pe baza bitului de scor

Figura 3.21b. Restartarea procesării instrucţiunilor

Există situaţii în care hazardul RAW se rezolvă prin hardware, fără să


cauzeze stagnări ale fluxului de procesare, precum în cazul anterior. Aceste
tehnici de rezolvare se numesc tehnici de forwarding (bypassing) și sunt bazate
pe "pasarea anticipată" a rezultatului instrucţiunii i, nivelului de procesare
aferent instrucţiunii j, care are nevoie de acest rezultat. Spre exemplu, să
considerăm secvenţa de instrucțiuni:

ADD R1, R2, R3 ; R1 <------ ( R2 ) + ( R3 )


SUB R4, R1, R5 ; R4 <------ ( R1 ) - ( R5 )

137
Figura 3.22. Implementarea "forwarding"-ului

Rezultatul ALU aferent primei instrucţiuni (R2 + R3) este memorat în


tampoanele ALU, la finele fazei ALU aferente instrucţiunii ADD. Dacă unitatea
de control va detecta hazardul RAW, va selecta pe parcursul fazei ALU aferente
instrucţiunii SUB, la intrarea A a ALU, prin intermediul multiplexorului
corespunzător, tamponul care conţine rezultatul (R2) + (R3) (în urma fazei ALU
a instrucţiunii ADD), evitând astfel hazardul RAW. Desigur, este necesară
implementarea proceselor de forwarding nu numai de la ieşirile ALU spre
intrări, ci şi din cadrul nivelelor următoare (de ex. nivelul MEM) spre intrările
ALU. Această situaţie corespunde unor instrucţiuni dependente RAW dar
nesuccesive strict în program (adică una imediat după alta). Valoarea "pasată"
uneia din intrările ALU în acest caz, reprezintă rezultatul ALU memorat în
nivelul următor (MEM) în cazul unor instrucţiuni aritmetico- logice sau data
citită din memoria cache de date (sau din memoria principală în cazul unui
miss), în cazul unor instrucţiuni LOAD.
În implementarea controlului mecanismelor de forwarding, pot apărea şi
situaţii conflictuale care trebuie rezolvate pe bază de arbitrare-prioritizare. Ca
exemplu în acest sens, se consideră secvenţa de trei instrucţiuni:
I1: SUB R2,R1,R3
I2: AND R2,R2,R5
I3: ADD R6,R2,R4

138
În acest caz se observă imediat că apar necesare două "pasări anticipate" de
valori: de la I1 la I3 (R2) pe intrarea A a ALU şi respectiv de la I2 la I3 (R2) pe
aceeaşi intrare A a ALU. Este evident că în acest caz trebuie acordată prioritate
nivelului pipeline mai "apropiat" de ALU, deci informaţiei produsă de ieşirile
ALU şi nu celei situate în nivelul următor (MEM aici). Cu alte cuvinte, se preia
pe intrarea A a ALU rezultatul produs de I2 şi nu cel produs de I1. Rezultă deci
că şi astfel de potenţiale situaţii conflictuale trebuie implementate în logica de
control a mecanismelor de forwarding.
Hazardul WAR (dependența Write After Read) poate să apară atunci când
instrucţiunea j scrie o destinaţie, înainte ca aceasta să fie citită pe post de sursă
de către o instrucţiune anterioară, notată i. Poate să apară când într-o structură
pipeline există o fază de citire, posterioară unei faze de scriere. Spre exemplu,
modurile de adresare indirectă cu pre-decrementare pot introduce acest hazard;
de aceea ele nici nu sunt implementate în arhitecturile de tip RISC.

Figura 3.23. Apariţia unui hazard WAR

De precizat că aceste hazarduri WAR (Write After Read), pot apărea şi


datorită execuţiei instrucţiunilor în afara ordinii lor normale, din program
(execuţie Out of Order), după cum se va remarca în continuare. Această
procesare Out of Order este impusă de creşterea performanţei și se poate realiza
atât prin mijloace hardware, cât şi software, legat de optimizarea programelor pe
arhitecturile pipeline. Spre exemplu, dacă instrucțiunea LOAD R1, (R4)200 –
consumatoare de timp, mai ales dacă este cu miss în cache – este urmată de
instrucțiunea ADD R5, R6,R7, într-un procesor cu execuţie Out of Order, a 2-a
instrucțiune se termină înaintea celei dintâi, cu beneficii asupra timpului de
procesare.
Hazardul WAW (dependența de tip Write After Write), apare atunci când
instrucţiunea j scrie un operand înainte ca acesta să fie scris, de către
instrucţiunea anterioară i. Aşadar, în acest caz scrierile s-ar face într-o ordine
inversă. Hazardul WAW poate apărea în structurile care au mai multe nivele de
scriere sau care permit unei instrucţiuni să fie procesată, chiar dacă o

139
instrucţiune anterioară este blocată. Modurile de adresare indirectă cu post-
incrementare pot introduce acest hazard, fapt pentru care ele sunt evitate în
procesoarele RISC. De asemenea, acest hazard poate să apară și în cazul
execuţiei Out of Order a instrucţiunilor care au aceeaşi destinaţie.

Figura 3.24 Apariţia unui unui hazard WAW

Hazardurile de tip WAW sau WAR nu reprezintă hazarduri “tari”, ci mai


degrabă conflicte de nume. Ele pot fi eliminate de către compilator (mai precis
de către o componentă a acestuia, numită scheduler) prin redenumirea resurselor
utilizate de program. De aceea, aceste hazarduri “slabe” se mai numesc
dependenţe de ieşire respectiv antidependenţe.

Exemplu: I1: MULF Ri, Rj, Rk; Ri <--- ( Rj ) * (Rk)


I2: ADD Rj, Rp, Rm; Rj <--- ( Rp) + (Rm)

În exemplul anterior poate să apară hazard WAR, întrucât instrucţiunea I1,


fiind o instrucţiune de coprocesor flotant, se va încheia în execuţie după
instrucțiunea I2, care este o instrucţiune de procesor cu operanzi întregi (faza sa
de execuție, uzual, se încadrează într-un tact). Am presupus deci, în mod realist,
că numărul de nivele aferent structurii pipeline a coprocesorului este mai mare
decât numărul de nivele aferent procesorului. Rezultă deci că instrucţiunile I1, I2
se termină "Out of Order" (I2 înaintea lui I1). Secvenţa reorganizată prin
software, care elimină hazardul WAR este:
MULF Ri, Rj, Rk
ADD Rx, Rp, Rm
.....
MOV Rj, Rx : Rj <- Rx (după Ri <- (Rj) x (Rk))
Prezentăm în continuare un exemplu de reorganizare a unui program în
vederea eliminării hazardurilor de date şi a procesării sale optimale, din punct de
vedere al timpului de procesare:
I0 ADD R3, R1, R2

140
I1 LD R9, A(R7); Loc_Mem(R7)+A R9
NOP
I2 ADD R4, R3, R2
NOP, NOP
I3 ADD R5, R4, R6
I4 LD R4, A(R6)
NOP, NOP
I5 LD R2, A(R4)
S-a considerat că între oricare două instrucțiuni succesive, dependente
RAW, este necesară o întârziere de doi cicli (instrucțiunile NOP). Graful
dependenţelor de date corespunzător acestei secvenţe este următorul (vezi
Figura 3.25):

Figura 3.25. Graful dependenţelor de date asociat secvenţei


Aparent, pe baza grafului de mai sus, secvenţa reorganizată s-ar procesa
astfel:
I0 ADD R3, R1, R2
I1 LD R9, A(R7)
I4 LD R4, A(R6)
I2 ADD R4, R3, R2
NOP, NOP
I3 ADD R5, R4, R6
I5 LD R2, A(R4)
Se observă în acest caz că execuţia Out of Order a instrucţiunilor I4 şi I2
determină o procesare eronată a programului datorită hazardului de tip WAW
prin registrul R4. De asemenea, între instrucţiunile I3 şi I4 există hazard WAR
prin acelaşi registru. Mai precis, în acest caz, instrucțiunea I5 va folosi valoarea
lui R4 setată de I2 și nu cea setată de I4, cum este normal. Acest fapt altereză
semantica secvenței de program. Aşadar, detecţia hazardului WAW între
instrucţiunile I2 şi I4 determină impunerea unei relaţii de precedenţă între
aceste instrucţiuni, adică procesarea lor trebuie să se realizeze în ordinea din

141
program (In Order). În cazul nostru, este necesar deci ca I3, dependentă RAW
de I2, să se execute înaintea instrucţiunii I4. Aceste restricţii au fost evidenţiate
prin linie punctată în graful dependenţelor de date, anterior prezentat. Ţinând
cont de cele de mai sus, pe baza grafului dependenţelor de date, secvenţa
reorganizată corect va fi următoarea:

I0 ADD R3, R1, R2


I1 LD R9, A(R7)
I4 LD R4’, A(R6); redenumire R4= R4’
I2 ADD R4, R3, R2
NOP, NOP
I3 ADD R5,R4,R6
I5 LD R2, A(R4’); R4= R4’

Rata de procesare în acest caz este de (6/8=0,75) instrucțiuni/tact,


performanţă superioară programului iniţial, care avea o rată medie de procesare
de doar (6/11) instrucțiuni/tact, întrucât prin procesarea Out of Order, se evită la
maximum dependenţele de date între instrucţiuni. Hazardul WAR între
instrucțiunile I3 şi I4 prin registrul R4 a putut fi eliminat prin redenumirea
registrului R4 în instrucţiunea I4. De asemenea, relaţii suplimentare de
precedenţă între instrucţiuni impun, într-un mod analog, hazardurile de tip WAR
(în exemplul nostru între I3 şi I4). După cum am mai arătat, hazardurile WAR şi
WAW nu reprezintă conflicte reale, ci doar aşa-zise conflicte de nume, ele
nefiind considerate dependenţe puternice (tari). Considerând un procesor cu mai
mulţi regiştri fizici decât cei logici, precedenţele impuse de aceste hazarduri pot
fi eliminate uşor, prin redenumirea regiştrilor logici cu cei fizici (register
renaming). Principiul constă în existenţa unei liste a regiştrilor activi, adică
folosiţi momentan, şi o alta a celor liberi (adică a celor nefolosiţi momentan).
Fiecare schimbare a conţinutului unui registru logic prin program, se va face
asupra unui registru fizic disponibil în lista regiştrilor liberi şi care va fi trecut în
lista regiştrilor activi. În acest caz secvenţa în discuţie s-ar transforma în
următoarea:
I0: ADD R3a, R1a, R2a
I1: LD R9a, A(R7a)
I2: ADD R4a, R3a, R2a
I3: ADD R5a, R4a, R6a
I4: LD R4b, A(R6a)
I5: LD R2b, A(R4b)

142
În acest caz, în optimizare nu ar mai avea importanţă decât dependenţele
RAW, celelalte fiind eliminate. În baza grafului dependenţelor de date
procesarea optimală ar însemna în acest caz: I0, I1, I4, I2, I3, I5, etc. (am omis
NOP-urile aferente). Să observăm că, deocamdată, am prezentat exclusiv
problema reorganizării aşa numitelor "basic-block"-uri sau unităţi secvenţiale de
program, în cadrul unor procesoare RISC pipeline scalare. Se numeşte basic-
block o secvenţă de instrucţiuni cuprinsă între două instrucţiuni de ramificaţie
(salt, apel etc.), care nu conţine nici o instrucţiune de ramificaţie şi care nu
conţine nici o instrucţiune destinaţie a unei instrucţiuni de ramificaţie.
Optimizarea programelor în basic - block-uri este rezolvată încă din
perioada de pionierat a cercetărilor legate de microprogramare. Din păcate, prin
concatenarea mai multor basic- block-uri optimizate nu se obţine un program
optimizat. Se pune deci problema unei optimizări globale, a întregului program
(vom reveni asupra acestei probleme în capitolul următor).

3.4.4.3. HAZARDURI DE RAMIFICAŢIE (HR): PROBLEME


IMPLICATE ŞI SOLUŢII

Pot fi generate de către instrucţiunile de ramificaţie, numite și instrucțiuni


de salt condiționat (branch). Cauzează pierderi de perfomanţă, în general, mai
importante decât cele cauzate de hazardurile structurale şi cele de date. Efectele
defavorabile ale instrucţiunilor de ramificaţie pot fi reduse prin metode software
(reorganizarea programului sursă), sau prin metode hardware, care determină în
avans dacă saltul se va face sau nu (dynamic branch prediction) şi calculează în
avans noul PC (Program Counter). Diverse statistici arată că instrucţiunile de
salt necondiţionat au o frecvenţă între 2 - 8% din instrucţiunile unui program de
uz general, iar cele de salt condiţionat între 11 - 17%. S-a arătat că salturile
condiţionate simple se fac cu o probabilitate de cca. 50%, loop-urile (salturile
condiționate de la finele unei bucle de program) cu o probabilitate de cca. 90%,
iar majoritatea salturilor orientate pe bit nu se fac.
Considerând o structură pipeline pe 5 nivele şi că la finele nivelului RD
(ID) adresa de salt este disponibilă, efectul defavorabil al unei instrucţiuni de
salt este sugerat în Figura 3.26. Evident că în alte structuri pipeline (super-
pipeline) "branch delay slot"-ul (BDS) poate fi semnificativ mai mare decât un
singur ciclu.

143
Figura 3.26. Hazardul de ramificaţie pe un procesor scalar
O primă soluţie pentru o procesare corectă, ar fi aceea de a dezvolta unitatea
de control hardware în vederea detectării prezenţei saltului şi de a întârzia
procesarea instrucţiunilor următoare cu un număr de cicli egal cu latenţa BDS-
ului, până când adresa de salt devine disponibilă. Soluţia implică, desigur,
reducerea semnificativă a performanţelor, având în vedere frecvența ridicată a
instrucțiunilor de branch în programe. Acelaşi efect l-ar avea şi "umplerea"
BDS-ului de către scheduler cu instrucţiuni NOP, în vederea întârzierii necesare.
În continuare, se vor prezenta succint metodele şi tehnicile care stau la baza
soluţionării prin software a ramificaţiilor de program. Acestea se bazează pe
reorganizarea secvenţei de instrucţiuni maşină, astfel încât efectul defavorabil al
salturilor să fie eliminat sau măcar atenuat. În continuare se va prezenta o astfel
de strategie atribuită lui T. Gross şi J. Hennessy şi dezvoltată încă din perioada
de pionierat a acestor cercetări (1982). Mai întâi, se defineşte o instrucţiune de
salt, situată la adresa b, spre o instrucţiune situată la adresa l, ca fiind un salt
întârziat de ordinul n, dacă instrucţiunile situate la locaţiile de memorie b, b +
1, ....., b + n şi l sunt executate întotdeauna când saltul se face (Taken). O primă
soluţie de optimizare ar fi aceea de mutare a instucţiunii de salt cu n instrucţiuni
"în sus". Acest lucru este posibil doar atunci când niciuna dintre precedentele n
instrucţiuni nu afectează determinarea condiţiilor de salt. În general, se poate
muta instrucţiunea de salt cu doar k instrucţiuni "în sus", unde k reprezintă
numărul maxim de instrucţiuni anterioare care nu afectează condiţiile de salt, k ∈
(0, n]. Când acest lucru se întâmplă, se poate aplica succesiv o altă tehnică, ce
constă în duplicarea primelor (n - k) instrucţiuni plasate începând de la adresa
destinaţie a saltului, imediat după instrucţiunea de salt şi modificarea
corespunzătoare a adresei de salt la acea adresă care urmează imediat după cele
(n - k) instrucţiuni "originale", ca în Figura 3.27.

144
Figura 3.27. Soluţionarea unui hazard de ramificaţie prin duplicarea
instrucţiunilor

În cazul în care saltul condiţionat nu se execută (Not Taken), este necesar ca


niciuna dintre cele (n - k) instrucţiuni adiţionale să nu afecteze execuţia
următoarelor (l + n - k) instrucţiuni. Metoda se recomandă atunci cînd saltul se
face predominant, ceea ce poate fi stabilit în urma colectării unor statistici
obţinute după rulare (profilings). În acest caz se realizează un câştig de
performanţă, în schimb se suplimentează spaţiul de memorare al programului.
De remarcat că aceste microprocesoare nu dețin, în general, registru de flaguri
(indicatori de condiții) şi deci salturile condiţionate nu se fac pe baza acestor
flaguri, ci pe baza unei condiţii specificate explicit în opcode-ul instrucţiunii de
salt.
În continuare se prezintă un exemplu concret de reorganizare, în vederea
eliminării efectului BDS- ului, considerând însă că BDS- ul instrucţiunii de salt
este doar de un singur tact.
a)

Figura 3.28. Soluţionarea optimă


Strategia anterioară îmbunătăţeşte rata de execuţie întotdeauna, indiferent
dacă saltul se face ori nu se face. Dacă această reorganizare nu este posibilă, se
încearcă una dintre următoarele două variante ((b) sau (c)).
b)

145
Figura 3.29. Soluţie utilă când saltul se face preponderent
În acest caz, rata de execuţie creşte doar atunci cînd saltul condiţionat se
face (Taken). Dacă saltul respectiv nu se face (Not Taken), trebuie ca
instrucţiunea introdusă în BDS (SUB R4, R5, R6), să nu provoace execuţia
eronată a ramurii de program respective. Din păcate, un astfel de caz măreşte
zona de cod oricum şi, în plus, necesită timp de execuţie sporit cu o instrucţiune
adiţională, în cazul în care saltul nu se face.
c)

Figura 3.30. Soluţie utilă când saltul nu se face preponderent


Acest ultim caz, creşte performanţa doar atunci când saltul condiţionat nu se
face. Şi aici este necesar ca instrucţiunea introdusă în BDS să nu provoace
execuţia eronată a ramurii de program, în cazul în care saltul se face.
În concluzie, prin toate aceste strategii software se urmăreşte "umplerea"
BDS-ului cu instrucţiuni utile şi care să nu afecteze programul, din punct de
vedere logic. În general, microprocesoarele RISC care deţin un BDS de una-
două instrucţiuni, au posibilitatea - printr-un bit din codul instrucţiunii de salt
condiţionat - să introducă stagnări hardware în cazul salturilor condiţionate sau
să se bazeze pe umplerea BDS-ului cu instrucţiuni NOP sau cu alte instrucţiuni
utile de către reorganizator (scheduler).
În cele ce urmează, se vor prezenta pe larg, într-un mod critic şi pe deplin
actualizat, cele mai performante strategii actuale de gestionare a ramificaţiilor de
program şi anume predicţia dinamică (run-time), prin hardware. Aceste strategii
hardware de predicţie a branch-urilor au la bază un proces de predicţie "run -

146
time" a ramurii de salt condiţionat precum şi determinarea în avans a noului PC.
Ele sunt comune, practic, atât procesoarelor scalare cât şi celor cu execuţii
multiple ale instrucţiunilor, care se vor analiza în detaliu în capitolul următor al
acestei cărți. Cercetări recente insistă pe această problemă, întrucât s-ar elimina
necesitatea reorganizărilor software ale programului sursă şi deci s-ar obţine o
independenţă faţă de maşină.
Necesitatea predicţiei dinamice a intrucțiunilor de salt condiționat, mai ales
în cazul procesoarelor cu execuţii multiple ale instrucţiunilor (VLIW,
superscalare etc.), este imperios necesară. Notând cu BP (Branch Penalty)
numărul mediu de cicli de aşteptare pentru fiecare instrucţiune din program,
introduşi de salturile incorect predicţionate, se poate scrie relaţia:
BP= C (1-Ap) b IR
unde s-au notat prin:
C= Numărul de cicli de penalizare introduşi de un salt predicţionat în mod
eronat
Ap= Acurateţea predicţiei
b= Procentajul instrucţiunilor de salt, din totalul instrucţiunilor, procesate în
program
IR= Rata medie de lansare în execuţie a instrucţiunilor (>1, în cazul
procesoarelor superscalare)
Se observă că BP(Ap=0)=C*b*IR, iar BP(Ap=1)=0 (normal, pentru că
predicţia este ideală aici). Impunând un BP=0.1 şi considerând valorile tipice:
C=5, IR=4, b=22.5%, rezultă ca fiind necesară o acurateţe a predicţiei de peste
97.7% ! Cu alte cuvinte, la o acurateţe de 97.7%, IR=4/1.4=2.8
instrucțiuni/ciclu, faţă de o performanță ideală de IR=4 instr./ciclu, la o predicţie
perfectă (Ap=100%). Relația între acuratețea predicției și performanța
globală a procesorului este una puternic neliniară, ceea ce arată că fiecare
procent câștigat în procesul de predicție este foarte important! Este o
dovadă clară că sunt necesare acurateţi ale predicţiilor foarte apropiate de 100%
pentru a nu se "simţi" efectul defavorabil al ramificaţiilor de program asupra
performanţei procesoarelor avansate.
O metodă consacrată în acest sens o constituie metoda "branch prediction
buffer" (BPB). BPB-ul reprezintă o mică memorie cache, integrată în CPU,
adresată cu cei mai puţin semnificativi biţi ai PC-ului aferent unei instrucţiuni de
salt condiţionat. Cuvântul din BPB este constituit, în principiu, dintr-un singur
bit. Dacă acesta este 1 logic, atunci se prezice că saltul se va face, iar dacă este 0
logic, se prezice că saltul nu se va face. Evident că nu se poate şti în avans dacă

147
predicţia este corectă. Oricum, structura va considera că predicţia este corectă şi
va declanşa aducerea instrucţiunii următoare de pe ramura prezisă. Dacă
predicţia se dovedeşte a fi fost falsă, structura pipeline și bufferul de prefetch
(dar și alte resurse din contextul CPU) se evacuează şi se va iniţia procesarea
celeilale ramuri de program (recovery process). Desigur, în cadrul acestui proces
de refacere a stării corecte a procesorului, se pune problema instrucțiunilor
speculative, care au alterat deja contextul CPU. După cum se va arăta în
continuarea acestui tratat universitar, instrucțiunile speculative nu vor scrie
contextul CPU, ci doar un context de rezervă. Totodată, valoarea bitului de
predicţie din BPB se inversează, implementând astfel o adaptivitate, este drept,
nu prea elaborată, a schemei (se prezice conform comportamentului instanței
anterioare a saltului condiționat.)

Figura 3.31. O buclă tipică de program


BPB cu un singur bit are un dezavantaj care se manifestă cu precădere în
cazul buclelor de program ca cea din figura anterioară, în care saltul se va face
de (N - 1) ori şi, o singură dată, la ieşirea din buclă, nu se va mai face. Bazat pe
tehnica BPB în acest caz vom avea uzual două predicţii false: una la intrarea în
buclă (prima parcurgere) şi alta la ieşirea din buclă (ultima parcurgere a buclei).
Aşadar, în acest caz acurateţea predicţiei va fi de (N - 2) * 100 / N %, iar
saltul se face în proporţie de (N - 1) * 100 / N %. Pentru a elimina acest
dezavantaj, se utilizează doi biţi de predicţie, modificabili conform grafului de
tranziţie de mai jos (numărător saturat sus/jos, cu 4 stări). În acest caz,
acurateţea predicţiei unei bucle care se face de (N - 1) ori va fi de (N - 1) * 100 /
N %. Acest automat de predicţie de tip numărător saturat pe doi biţi
implementează un predictor adaptiv, adică unul care învață, atât din succese
(predicție corectă) cât și din eșecuri (predicție incorectă). Spre exemplu, dacă
predictorul se află în starea curentă “Predicție Da-slab” (01) și dacă ulterior
saltul se va face (succes), automatul va tranzita în starea “Predicție Da-tare”
(00). Dacă însă ulterior saltul nu se va face (eșec), automatul va tranzita în starea

148
“Predicție Nu-tare” (10). Se pune întrebarea, de ce nu va tranzita în acest ultim
caz, spre exemplu, în starea “Predicție Nu-slab” (11)? Ar putea tranzita și așa,
rezultând deci un alt predictor. Așadar, predictorul prezentat nu este unic,
putându-se imagina și alte variante raționale. Numai evaluări statistice, bazate pe
simulări complexe (benchmarking), pot arăta care este cel mai eficient predictor
pentru un anumit set de benchmark-uri. Desigur că ar avea sens și un predictor
analog, cu 8 stări (4 cu predicție DA, 4 cu predicție NU), codificate pe 3 biți, sau
chiar cu mai multe stări. Avantajul mai multor stări constă în faptul că
predictorul înglobează mai multă istorie (locală) a saltului aferent, ceea ce i-ar
permite, în principiu, o acuratețe de predicție mai bună. Pe de altă parte,
adaptarea la schimbări bruște de comportament al saltului condiționat, este mai
dificilă în acest caz (spre exemplu, tranziția din starea DA-tare în starea NU-slab
ar fi mai greoaie, dacă stările intermediare sunt multe.)

Figura 3.32. Automat de predicţie de tip numărător saturat pe doi biţi


Prin urmare, în cazul în care se prezice că branch-ul se va face (Taken),
aducerea noii instrucţiuni se va face de îndată ce conţinutul noului PC este
cunoscut. În cazul unei predicţii incorecte, se evacuează structura pipeline şi se
atacă cealaltă ramură a instrucţiunii de salt. Totodată, biţii de predicţie se
modifică în conformitate cu graful din figură (sau altul, similar), numit şi
numărător saturat (vezi Figura 3.32). Probabilitatea p ca predicţia să fie corectă
pentru o instrucţiune de salt condiționat, este dată de relaţia:
p = p1 * p2 + (1-p2) * p3,
unde am notat prin:
p1, p2 - probabilitatea ca predicţia adresată în BPB să fie corectă şi să se
refere la respectiva instrucţiune de salt;
(1-p2)*p3 - probabilitatea ca predicţia să fie corectă, deşi nu se referă la
instrucţiunea în curs de aducere. (Există, spre exemplu, posibilitatea ca două
instrucţiuni de branch distincte să aibă cei mai puţini semnificativi biţi ai PC-

149
ului identici). Este evident că maximizarea probabilităţii p se obţine prin
maximizarea probabilităţilor p1, p2 (p3 nu depinde de caracteristicile BPB-ului).
Datorită faptului că predictorul propriu-zis este implementat prin automate
finite de stare, mai mult sau mai puţin complexe, aceste scheme au un caracter
dinamic, comportarea lor adaptându-se funcţie de “istoria” saltului respectiv.
Există şi scheme de predicţie numite statice, caracterizate prin faptul că predicţia
saltului pentru o anumită istorie a sa este fixă, bazată în general pe studii
statistice ale comportamentului salturilor. Astfel, spre exemplu, un salt care
anterioarele trei instanţe ale sale s-a făcut efectiv (taken) poate fi predicţionat în
tabele că se va face. Desigur că, cel puţin principial, o schemă dinamică de
predicţie este superioară uneia statice, datorită adaptabilităţii sale și faptului că
beneficiază în procesul de predicție de informații run-time.
O altă problemă delicată constă în faptul că, deşi predicţia poate fi corectă,
de multe ori adresa de salt (noul PC) nu este disponibilă în timp util, adică la
finele fazei de aducere a instrucțiunii - IF. Timpul necesar calculului noului PC
are un efect defavorabil asupra ratei de procesare. Soluţia la această problemă
este dată de metoda de predicţie numită "Branch Target Buffer" (BTB). Printr-
o astfel de schemă adaptivă de predicție dinamică a instrucțiunilor de branch, la
finele fazei de IF se predicționează trei informații importante și anume: dacă
instrucțiunea adusă din memorie este una de branch sau nu (în mod normal acest
fapt ar fi cert doar în faza următoare, cea de decodificare a instrucțiunii) (1),
dacă este branch, se predicționează dacă acesta se va face sau nu (2), iar în cazul
în care se va face, trebuie furnizată și adresa țintă (target address), pentru a ști
de unde să aducă următoarea instrucțiune (3). Un BTB este constituit dintr-un
BPB, care conţine pe lângă biţii de predicţie, noul PC de după instrucţiunea de
salt condiţionat şi eventual alte informaţii utile. Spre exemplu, un cuvânt din
BTB ar trebui să conţină şi adresa de memorie a instrucţiunii ţintă a saltului
(target address). Astfel, ar creşte performanţa, nemaifiind necesar un ciclu de
aducere a acestei instrucţiuni, dar, în schimb, ar creşte și costurile de
implementare. Diferenţa esenţială între memoriile BPB şi BTB constă în faptul
că prima este o memorie operativă, iar a 2-a poate fi și asociativă, ca în figura
următoare. Este normal acest fapt, în fond BTB-ul este un mic cache, integrat
on-chip cu procesorul.

150
Figura 3.33 Structură de predicţie BTB (asociativă)
La începutul fazei IF se declanşează o căutare – operativă (indexare cu PC)
sau asociativă (căutare după conținutul PC) – în BTB, după conţinutul PC-ului
curent. În cazul în care se generează hit în BTB, se obţine în avans PC-ul aferent
instrucţiunii următoare. Mai precis, considerând o structură pipeline pe trei faze
(IF, RD, EX), algoritmul de lucru cu BTB-ul este, în principiu, următorul:
IF) Se trimite PC-ul instrucţiunii ce urmează a fi adusă spre memorie şi spre
BTB. Dacă PC-ul trimis corespunde cu un PC (Tag) din BTB (hit), se trece în
pasul RD2, altfel, se trece în pasul RD1.
RD1) Dacă instrucţiunea adusă este o instrucţiune de branch, se trece în
pasul EX1, altfel se continuă procesarea normală.
RD2) Se trimite PC-ul prezis din BTB spre memoria (cache) de instrucţiuni.
În cazul în care condiţiile de salt sunt satisfăcute, se trece în pasul EX 3, altfel se
trece în pasul EX2.
EX1) Se introduce PC-ul instrucţiunii de salt condiționat (pe post de tag)
precum şi PC-ul prezis în BTB. De obicei, această alocare se face în locaţia cea
mai de demult neaccesată (Least Recently Used - LRU) sau, cu alte cuvinte, în
acea locație din BTB pentru care durata, din momentul ultimei sale accesări (sau
alocări, dacă nu a fost accesată), până la momentul curent, este maximă.
EX2) Predicţia s-a dovedit eronată. Trebuie reluată faza IF de pe cealaltă
ramură, cu penalizările de rigoare datorate evacuării structurilor pipeline și
refacerii contextului CPU (recovery algorithm).
EX3) Predicţia a fost corectă, însă numai dacă şi PC-ul predicţionat este
într-adevăr corect, adică neschimbat. În acest caz, se continuă execuţia normală.
În tabelul următor (Tabelul 3.4) sunt rezumate avantajele şi dezavantajele

151
tehnicii BTB, anterior descrise.

Instr. în Predicţie Realitate Cicli


BTB ? penalizare
Da Da Da 0(Ctt)
Da Da Nu Ctn
Da Nu Nu 0
Da Nu Da Cnt
Nu - Da Ct
Nu - Nu 0
Tabelul 3.4. Penalizarea într-o structură de predicţie tip BTB

În baza celor anterior discutate, rezultă că numărul de cicli de penalizare


(CP) este dat de următoarea relaţie:
CP = PBTB (Ptn*Ctn +Pnt*Cnt) +(1-PBTB )*P*Ct
unde s-a notat:
PBTB - probabilitatea ca instrucţiunea de salt să se afle în BTB;
Ptn - probabilitatea ca saltul să fie prezis că se face şi în realitate nu se va
face;
Pnt - probabilitatea ca saltul să fie prezis că nu se face şi în realitate se va
face;
P - probabilitatea ca respectiva instrucţiune de salt să se facă;
Această relaţie, necesită amendamente serioase pentru a fi utilă şi exactă,
după cum s-a arătat în [Vin00]. În acest caz, rata de procesare a instrucţiunilor ar
fi dată de relaţia:
IR= 1/(1 + Pb*CP), [instr./tact],
unde Pb= probabilitatea ca instrucţiunea curentă sa fie una de ramificaţie.
Un model matematic simplu al acestei tehnici pentru un BTB cu N biţi de
memorare, legat de eficienţa schemei, se referă la maximizarea funcţiei:
s
F = ∑ Pex(i)[P(i)*Ptt(i)*V(i) - (1 - P(i))* Ptn( i )*W(i)]
i=1
s
astfel încât ∑ n( i ) ≤ N,
i=1

unde:
n( i ) - numărul de biţi din BTB alocat instrucţiunii de branch i;
N - numărul total de biţi din BTB;
S - numărul de instrucţiuni branch procesate în cadrul programului;

152
Relativ la expresia funcţiei F avem următorii termeni:
Pex( i ) - probabilitatea ca branch-ul i să se execute în cadrul programului ;
P( i ) - probabilitatea ca branch-ul i să se facă (Taken);
Ptt( i ) - probabilitatea ca branch-ul i să fie prezis că se face şi într-adevăr
se va face;
V( i ) - numărul de cicli economisiţi în cazul unei predicţii corecte a
branch-ului i;
W( i ) - numărul de cicli de penalizare în cazul unei predicţii incorecte a
branch-ului i;
Obs. 1) Ptt( i ) = Ptn( i ) = 0, dacă branch-ul i nu se află în BTB.
Obs. 2) S-a considerat că BTB nu îmbunătăţeşte performanţa pentru o
predicţie corectă de tipul "saltul nu se face" (Pnn( i ) = 0), întrucât în acest caz
structura se comportă în mod implict, la fel ca şi o structură fără BTB. De
asemenea, pentru o predicţie incorectă a faptului că "saltul se face", am
considerat costul acelaşi cu cel pe care l-am avea fără BTB; din acest motiv Pnt(
i ) nu intră în expresia funcţiei.
Obs. 3) Un branch trebuie introdus în BTB, cu prima ocazie când el se va
face (Taken). Un salt care ar fi prezis că nu se va face, nu trebuie introdus în
BTB pentru că nu are potenţialul de a îmbunătăţi performanţa (nu intră în
expresia funcţiei F). Din acest motiv, există strategii care atunci când trebuie
evacuat un branch din BTB îl evacuează pe cel cu potenţialul de performanţă
minim, care nu coincide neapărat cu cel mai puţin folosit (LRU). Astfel, se
construieşte câte o variabilă de tip MPP (Minimum Performance Potential),
implementată în hardware, asociată fiecărui cuvânt din BTB. Evacuarea din
BTB se face pe baza MPP-ului minim. Acesta se calculează ca un produs între
probabilitatea ca un branch din BTB să fie din nou accesat (LRU) şi respectiv
probabilitatea ca saltul să se facă. Probabilitatea din urmă se obţine pe baza unei
istorii locale a respectivului salt (taken / not taken). Minimizarea ambilor factori
duce la minimizarea MPP-ului şi deci la evacuarea respectivului branch din
BTB, pe motiv că potenţialul său de performanţă este minim.
În literatura tehnică de specialitate se arată că prin astfel de scheme se poate
ajunge la predicţii corecte în cca. (80-90)% din cazuri. Există implementări de
mare performanţă în care biţii de predicţie sunt gestionaţi şi funcţie de "istoria"
respectivei instrucţiuni de salt, pe baze statistice (INTEL NEX GEN, TRON,
etc). Prin asemenea implementări creşte probabilitatea de predicţie corectă a
branch-ului. Microprocesorul Intel Pentium I avea un predictor de ramificaţii
bazat pe un BTB cu 256 de intrări, cu mapare directă.

153
În continuare se va prezenta un model analitic general de analiză pentru
arhitecturile cu predicţie de tip BTB, implementate în procesoarele scalare RISC
pipeline şi respectiv în cele superscalare, preluat din cartea autorului [Vin00].
Modelul dezvoltat, aparține chiar autorului acestei cărți. Acest model analitic
abordează problematica unui BTB clasic, integrat într-o arhitectură scalară sau
superscalară. Analiza se bazează pe investigarea următoarelor patru cazuri
posibile: saltul nu se află în BTB şi se va face (Taken), saltul nu se află în BTB
şi nu se va face (Not Taken), saltul se află în BTB şi este predicţionat corect şi
respectiv saltul se află în BTB dar este predicţionat în mod incorect. În
continuare se va prezenta o analiza a fiecăruia din aceste cazuri în parte.
1. Saltul nu se află în BTB
Deoarece instrucţiunea de salt condiționat nu se află în BTB în momentul
aducerii sale din memoria I-Cache, va fi prezisă în mod implicit că nu se va face.
Procesarea va continua secvenţial în acest caz. Aici se disting două subcazuri:
a. Saltul condiționat se va face (P=1)
S-a notat prin P probabilitatea statistică ca instrucţiunea de salt să se facă.
Uzual, pe programe generale, P ia valoarea medie tipică de 0.67. De asemenea,
se fac următoarele notaţii:
N = nivelul de procesare din structura pipeline în care se determină
condiţiile de salt şi adresa efectivă destinaţie a instrucţiunii de salt (target
address). În continuare, se consideră că N > 2.
d = numărul de cicli CPU (tacte) necesari pentru citirea din BTB. În
general, d = 0, căutarea în BTB făcându-se chiar în faza IF.
u = numărul de cicli necesari pentru scriere (actualizare) în BTB. În general
u >= d şi uzual u = 0 sau 1.
i = instrucţiunea următoare instrucţiunii de salt (B), în secvenţa considerată.
B = instrucţiunea de salt curentă
T = instrucţiunea destinaţie la care se va face saltul condiționat (ţintă).
C = numărul de cicli necesari pentru refacerea contextului procesorului în
cazul unei predicţii incorecte (recovery process).
Cu aceste precizări, în acest caz, secvenţa de procesare a instrucţiunilor se
va desfăşura în timp ca în tabelul următor:
Procesarea temporală în cazul în care saltul se face
1. IF B d i u T T+1 T+2
2. B d i u T
N. B d u T
Aşadar, în acest caz, numărul ciclilor de penalizare datorat instrucţiunii de

154
salt condiționat (B) este dat de relaţia :
CP = N - 2 + d + Max (u, C)
Actualizarea, în acest caz, se referă la introducerea saltului în structura
BTB, de îndată ce acesta s-a făcut.
b. Saltul nu se va face (P = 0)
Deoarece saltul nu se va face şi întrucât acesta nu se găseşte în BTB, nu are
sens să fie introdus în structura BTB deoarece introducerea lui nu ar determina
îmbunătăţirea performanţei (oricum, predicția implicită – default prediction –
este că nu se va face). Procesarea temporală a instrucţiunilor s-ar desfăşura ca
mai jos:
1. IF B d i i+1 i+2
2. B d i
N. B d i
În acest caz penalizarea este minimă şi este determinată de căutarea în BTB.
Relaţia de penalizare este următoarea:
CP = d
2. Saltul condiționat se află în BTB.
Aici se vor analiza detaliat cele 4 sub-cazuri posibile, anume:
a. Ptt = 1
Am notat prin Ptt probabilitatea ca instrucţinea de salt curentă să fie
predicţionată că se va face şi, într-adevăr, să se facă. Se disting, din nou, două
sub-cazuri (a1, a2), după cum adresa destinaţie memorată în cuvântul BTB este,
ori nu este, corectă.
a1. Adresa destinaţie din BTB este corectă (Pac = 1).
Se notează cu Pac probabilitatea ca adresa din BTB să fie corectă, deci
nemodificată în raport cu utilizarea ei anterioară. În practică, Pac atinge valori
cuprinse între 0.9 şi 0.99. În acest sub-caz, procesarea este descrisă în tabelul
următor.
Procesarea în cazul în care Pac=1
1. IF B d T u T+1 T+2
2. B d T u
N. B d T u
Şi aici actualizarea (u) semnifică modificarea stării automatului de
predicţie, în conformitate cu acţiunea instrucţiunii de salt. Ciclii de penalizare
sunt daţi de relaţia următoare:
CP = d + u

155
a2. Adresa destinaţie din BTB nu este cea corectă (Pac = 0)
În acest caz, adresa efectivă a instrucţiunii destinaţie memorată în BTB este
diferită de cea actuală (spre exemplu, datorită unei adresări indirecte a
instrucţiunii de salt în care conţinutul regiştrilor respectivi a fost modificat sau a
fenomenului de interferenţă a salturilor în BTB). Procesarea instrucţiunilor se
desfăşoară în acest caz ca în tabelul următor.
Procesarea în cazul în care Pac=0
1. IF B d T u T*
2. B d T u T*
N. B d u T*
S-a notat cu T* instrucţiunea la care se va face saltul condiționat,
corespunzătoare noii adrese modificate. În acest caz penalizarea va fi:
CP = N - 2 + d + Max (u, C)
b. Ptn = 1
S-a notat cu Ptn probabilitatea ca saltul respectiv să fie prezis că se va face
şi, în realitatea faptică, să nu se facă. În acest caz, procesarea temporală a
instrucţiunilor este descrisă prin următorul tabel.
Procesarea în cazul în care Ptn=1
1. IF B d T u i
2. B d T u i
N. B d u i
Desigur că şi în acest caz instrucţiunea va rămâne memorată în BTB, până
la evacuarea sa naturală. Ciclii de penalizarea sunt în acest caz:
CP = N - 2 + d + Max(u, C)
c. Pnn = 1
S-a notat cu Pnn probabilitatea ca saltul respectiv să fie prezis că nu se face
şi într-adevăr să nu se facă. Procesarea este prezentată în tabelul următor:
Procesarea în cazul în care Pnn=1
1. IF B d i u i+1
2. B d i u
N. B d i u
CP = d + u
d. Pnt = 1
Prin Pnt am notat probabilitatea ca saltul să fie prezis că nu se va face şi, în
realitate, se va face.

156
Procesarea în cazul în care Pnt=1
1. IF B d i u T T+1 T+2
2. B d i u T T+1
N. B d u T
În acest caz penalizarea este următoarea:
CP = N - 2 + d + Max(u, C)
Superpoziţionând toţi aceşti cicli de penalizare obţinuţi anterior, obţinem
numărul mediu de tacte de penalizare introduse prin execuţia unei anumite
instrucţiuni de ramificaţie (CPM), astfel:
CPM = (N - 2 + d + Max(u,c)) ((1 - Pbtb)P + Pbtb(Ptt(1 - Pac) + Ptn +
Pnt)) + Pbtb(d + u) (Ptt * Pac + Pnn) + d(1 - Pbtb) (1 - P)
S-a notat cu Pbtb, probabilitatea ca instrucţiunea de salt adusă curent din I-
CACHE, să se afle memorată în BTB. Este clar că scopul principal al proiectării
constă în minimizarea expresiei lui CPM şi deci în maximizarea performanţei
globale. Minimizarea lui CPM se obţine prin minimizarea parametrilor N, d, u,
C, Pnt, Ptn. Este dificilă o soluţie analitică pentru determinarea unui CPM
minim, în condiţiile variaţiilor rezonabile ale diverşilor parametri. În cazul unui
procesor pipeline scalar, rata de procesare (Issue Rate – IR) este dată de relaţia:
IR = 1 / (1 + Pb * CPM),[instr./tact]
unde Pb = probabilitatea ca instrucţiunea curentă să fie o instrucţiune de salt
condiționat.
În cazul unui procesor pipeline superscalar (v. capitolul următor), putem
scrie:
CPI = CPIideal + CPM * Pb
unde CPIideal = numărul mediu de cicli / instrucţiune, dacă numărul ciclilor de
penalizare introduşi de branch-uri este nul şi dacă predicţia branch-urilor se
consideră perfectă. Evident că CPI < 1. Rezultă imediat că în cazul
microprocesoarelor superscalare, rata de procesare (IR) este dată de relaţia:
IR = 1 / CPI
O formulă aproximativă, dar totuși utilă a parametrului CPM, se poate
deriva din cea exactă, anterior obţinută, pe baza unor valori particulare
considerate realiste a unora din parametrii componenţi şi anume:
d = 0, u = 1, C = 2, Pac = 1.
Cu aceste particularizări, de altfel realiste, se obţine:
CPM= N((1-Pbtb)P + Pbtb(1-Ap)) + Pbtb Ap,
unde: Ap=Ptt + Pnn şi reprezintă acurateţea predicţiei.
Devine interesant de precizat performanţa unui procesor superscalar care s-

157
ar obţine în lipsa oricărei predicţii a branch- urilor. În acest caz rezultă, în mod
succesiv, relaţiile:
CPM = P(N - 1) + C
CPI = CPideal + Pb * CPM
IR = 1 / CPI
Se pot particulariza următorii parametri, cu valori considerate realiste, chiar
reprezentative:
N = 3, Pb = 0.317, P = 0.67, Pbtb = 0.98 şi CPIideal = 0.25
În acest caz, performanţa arhitecturii superscalare, în funcţie de gradul de
acurateţe al predicţiei (Ap), este prezentată în graficul următor, pe baza relaţiilor
anterior determinate. Spre comparare, se prezintă o arhitectură superscalară cu
predicţie BTB, faţă de una fără predicţie integrată. De remarcat că pentru Ap =
0.7, rezultă rata medie de procesare IR = 1.31, iar pentru Ap = 1 rezultă
performanța medie de IR = 1.76, adică o creştere a performanţei cu peste 34%.
De altfel de la Ap = 0.9 la Ap = 1.0, performanţa creşte cu 12%, deci
semnificativ. La o acurateţe a predicţiei de 80%, relativ uşor de atins,
performanţa arhitecturii creşte cu 88% faţă de cazul fără predicţie, iar la o
acurateţe de 90%, comună, performanţa creşte cu 106% faţă de cazul fără
predicţie. Ca un alt exemplu de aplicare a relaţiilor analitice obţinute,
considerând aceiaşi parametri aleşi şi, în plus, acurateţea predicției ca fiind Ap =
0.93, obţinem o performanţă realistă, anume: IRreal = 1.62 instr./tact. De
menţionat că în cazul unei predicţii perfecte a branch-urilor Ap=1, s-ar obţine
IRperfect = 1.76 instr./tact, faţă de IRideal = 4 instr./tact şi asta numai datorită
penalizărilor de accesare ale BTB-ului, relativ scăzute, dar şi întârzierii
calculării adresei de salt (N = 3).
1.8
1.6
1.4 predictionat
1.2 nepredictionat
1
IR

0.8
0.6
0.4
0.2
0
0.5 0.6 0.7 0.8 0.9 1
Ap

Figura 3.33b Performanța globală (IR) pentru un CPU fără şi respectiv cu


branch predictor

158
De asemenea, se poate observa că o acurateţe a predicţiei de 93%,
considerată foarte bună în schemele BTB, diminuează performanţa faţă de cazul
unei acurateţi perfecte a predicţiei, cu cca. 9% în condiţiile date. Există deci
potenţial de îmbunătăţire a performanţei arhitecturilor superscalare prin tehnici
de predicţie performante. Încă din 1992 s-au făcut paşi importanţi în acest sens
prin dezvoltarea unor scheme de predicţie adaptive pe două nivele, care ating o
acurateţe de până la 97% măsurat pe benchmark-urile SPEC.
Avantajele acestei metode analitice, faţă de cele clasice, bazate pe simulare,
constau în faptul că metoda este generală şi generează în mod rapid parametrii
legaţi de optimizarea predicţiei (Design Space Exploration), fără să implice
simulări laborioase. Prin particularizări ale valorilor diverşilor parametri, s-a
arătat că metoda furnizează rezultate concordante cu cele publicate în literatura
de specialitate şi obţinute pe bază de simulare.
O problemă dificilă este determinată de instrucţiunile de tip RETURN,
întrucât o aceeaşi instrucţiune, poate avea adrese de revenire diferite, ceea ce va
conduce în mod normal la dese predicţii eronate, pe motivul modificării adresei
eronate în tabela de predicţii. Desigur, problema se pune atât în cazul schemelor
de tip BTB cât şi a celor de tip corelat, care vor fi prezentate în continuare.
Soluţia de principiu, constă în implementarea în hardware a unor aşa zise "stack
- frame"- uri diferite. Acestea vor fi nişte stive interne CPU, care vor conţine
perechi CALL1/ RETURN, CALL2/ RETURN etc., cu toate informaţiile
necesare asocierii lor corecte. Astfel, o instrucţiune CALL (spre ex. CALL2)
poate modifica dinamic, pe durata procesării sale, în tabela de predicţii, adresa
de revenire pentru instrucţiunea RETURN corespunzătoare, evitându-se astfel
situaţiile nedorite, mai sus schiţate. Astfel, când respectiva instrucțiune
RETURN se va aduce din memorie (IF), va avea pregăită în tabela de predicții
adresa de revenire corectă.
În literatura de specialitate, bazat pe testări laborioase, se arată că se obţin
predicţii corecte în cca. 88% din cazuri folosind un bit de predicţie şi respectiv
în cca. 93% din cazuri folosind 16 biţi de predicţie (nu este de mirare, având în
vedere că cca. 70% din salturile condiționate dinamice au un comportament de
tip Taken). Acurateţea predicţiilor creşte asimptotic cu numărul biţilor de
predicţie utilizaţi, adică practic cu "istoria predicţiei". Schema de predicţie pe 4
stări din Figura 3.33 poate fi generalizată uşor la N = 2k stări. Se poate arăta că
există N2N * 2N (stări x ieşiri respectiv) automate distincte de predicţie cu N
stări, deşi multe dintre acestea sunt triviale din punct de vedere al predictiilor
salturilor. În literatura de specialitate Dr. Ravi Nair arată într-un mod elegant, pe

159
bază teoretică dar şi de simulare, că schema din Figura 3.32 este cvasi-optimală
în mulţimea acestor automate de predicţie. În acord cu literatura de specialitate,
mărirea numărului N de stări al automatului de predicţie pe k biţi nu conduce la
creşteri semnificative ale performanţei. După cum vom arăta în continuare, prin
scheme de predicţie corelată a salturilor se pot obţine performanţe superioare.
Schemele de predicţie anterior prezentate se bazau pe comportarea recentă a
unei instrucţiuni de salt condiționat, de aici predicţionându-se comportarea
viitoare a acelei instrucţiuni de salt. Este posibilă îmbunătăţirea acurateţii
predicţiei dacă predictorul dinamic se va baza pe comportarea recentă a altor
instrucţiuni de salt, întrucât frecvent aceste instrucţiuni pot avea o comportare
corelată în cadrul programului. Altfel spus, saltul curent poate depinde în
comportamentul său, de comportamentul anterioarelor salturi condiționate
(corelație). Schemele bazate pe această observaţie se numesc scheme de
predicţie corelată pe două niveluri (Two Level Adaptive Branch Predictors) şi
au fost introduse pentru prima dată în 1991/1992, în mod independent, de către
cercetătorii americani Yeh şi Patt şi respectiv de Pan et al. Să considerăm pentru
o primă exemplificare a acestei idei o secvenţă de program C, extrasă din
benchmark-ul Eqntott din cadrul grupului de benchmark-uri SPECint '92:
(b1) if (x = = 2)
x = 0;
(b2) if (y = = 2)
y = 0;
(b3) if (x ! = y) {
Se observă imediat că în acest caz, dacă salturile b1 şi b2 nu se vor face,
atunci saltul b3 se va face în mod sigur (pentru că x = y = 0). Aşadar saltul b3 nu
depinde de comportamentul său anterior, ci de comportamentul anterior al
salturilor b1 şi b2, fiind deci corelat cu acestea. Evident că în acest caz
schemele de predicţie anterior prezentate nu vor da randament satisfăcător, ele
neținând cont de aceste posibile corelații între branch-uri distincte. Dacă două
branch-uri sunt corelate, cunoscând comportarea primului, se poate anticipa
comportarea celui de al doilea, ca în exemplul de mai jos:
if (cond1)
....
if (cond1 AND cond2)
Se poate observa că funcţia condiţională a celui de al doilea salt este
dependentă de cea a primului. Astfel, dacă prima ramificaţie nu se va face,
atunci se va şti sigur ca nici cea de a doua nu se va face. Dacă însă prima

160
ramificaţie se va face, atunci cea de a doua va depinde exclusiv de valoarea
logică de adevăr a condiţiei "cond2". Aşadar, în mod cert, aceste două
ramificaţii sunt corelate, chiar dacă comportarea celui de al doilea salt nu
depinde exclusiv de comportarea primului. Să considerăm acum pentru analiză o
secvenţă de program C simplificată, împreună cu secvenţa obţinută în urma
compilării (s-a presupus că variabila x este asignată registrului R1).
if (x = = 0) (b1) BNEZ R1, L1
x = 1; ADD R1, R0, #1
if (x = = 1) L1: SUB R3, R1, #1
(b2) BNEZ R3, L2
Se poate observa că dacă saltul condiţionat b1 nu se va face, atunci nici b2
nu se va face, cele două salturi fiind deci corelate. Vom particulariza secvenţa
anterioară, considerând iteraţii succesive ale acesteia, pe parcursul cărora x
variază, spre exemplu alternativ, între valorile 0 şi 5. Un BPB clasic, iniţializat
pe predicţie NU, având un singur bit de predicţie, s-ar comporta ca în Tabelul
3.5. Aşadar o astfel de schemă ar predicţiona în acest caz, întotdeauna greşit!

Tabelul 3.5. Modul de predicţionare al unui BPB clasic

Să analizăm acum comportarea unui predictor corelat având un singur bit de


corelaţie (se corelează deci doar cu instrucţiunea de salt anterior executată) şi un
singur bit de predicţie. Acesta se mai numeşte şi predictor corelat de tip (1, 1).
Acest predictor va avea doi biţi de predicţie pentru fiecare instrucţiune de salt
condiționat: primul bit predicţionează dacă instrucţiunea de salt actuală se va
face sau nu, în cazul în care instrucţiunea anterior executată nu s-a făcut, iar al
doilea, analog, în cazul în care instrucţiunea de salt anterior executată s-a făcut.
Există deci următoarele 4 posibilităţi (v. Tabelul 3.6).

161
Biţi Predicţie dacă Predicţie dacă
predicţie precedentul salt nu s-a precedentul salt s-a
făcut făcut
NU / NU NU NU
NU / DA NU DA
DA / NU DA NU
DA / DA DA DA
Tabelul 3.6. Semnificaţia biţilor de predicţie pentru o schemă corelată

Ca şi în cazul BPB-ului clasic cu un singur bit de predicție, în cazul unei


predicţii care se dovedeşte a fi eronată, bitul de predicţie indicat se va
complementa. Comportarea predictorului (1,1) pe secvenţa anterioară de
program este prezentată în continuare (s-a considerat că biţii de predicţie asociaţi
salturilor b1 şi b2 sunt iniţializaţi pe NU / NU).

Tabelul 3.7. Modul de predicţionare al unei scheme corelate

După cum se observă în Tabelul 3.7, singurele două predicţii incorecte apar
atunci când x = 5, în prima iteraţie. În rest, predicţiile vor fi întotdeauna corecte,
schema comportându-se deci foarte bine, spre deosebire de schema BPB clasică.
În cazul general, un predictor corelat de tip (m,n) utilizează comportarea
precedentelor m instrucţiuni de salt executate, alegând deci o anumită predicţie
de tip Da sau Nu din cele 2m posibile, unde n reprezintă numărul biţilor utilizaţi
în predicţia fiecărui salt. În cazul unei bucle, o înregistrare a istoriei saltului sub
forma 011111111 (m=9) conduce în mod cert la o predicţie corectă a saltului
('0', adică nu se va face pentru că după 8 iteraţii consecutive, în a noua iteraţie,
se va ieşi din buclă), ceea ce printr-un predictor clasic de tip BTB este mai
dificil de predicţionat, după cum deja am arătat; de asemenea, o comportare
alternativă a unui salt este simplu de predicţionat printr-o schemă corelată; în

162
schimb printr-o schemă clasică este foarte dificil. Aşadar schemele corelate sunt
eficiente atunci când predicţia depinde şi de un anumit pattern al istoriei saltului
de predicţionat, corelaţia fiind, în acest caz particular, reprezentată de istoria pe
m biţi (Taken/Not Taken) chiar a acelui salt şi nu cu istoria anterioarelor m
salturi.
Un alt avantaj al acestor scheme este dat de simplitatea implementării
hardware, cu puţin mai complexă decât cea a unui BPB clasic. Aceasta se
bazează pe simpla observaţie că "istoria" celor mai recent executate m salturi din
program, poate fi memorată într-un registru binar de deplasare pe m ranguri
(registru de istorie globală sau de corelație). Aşadar, adresarea cuvântului de
predicţie format din n biţi şi situat într-o tabelă de predicţii, se poate face foarte
simplu prin concatenarea c.m.p.s. biţi ai PC-ului instrucţiunii de salt curente cu
acest registru de deplasare, în adresarea BPB-ului de predicţie. Ca şi în cazul
BPB-ului clasic, un anumit cuvânt de predicţie poate corespunde mai multor
salturi condiționate. Există deci în implementare două nivele: un registru de
istorie (globală în acest caz), al cărui conţinut concatenat cu PC- ul c.m.p.s. al
instrucţiunii de salt pointează la un cuvânt din tabela de predicţii (aceasta
conţine biţii de predicţie, adresa destinaţie etc.) Yeh și Patt, în articolul lor
originar, nu fac concatenarea PC - registru de istorie şi, în consecinţă, obţin
rezultate mai puțin satisfăcătoare, tocmai datorită interferenţelor diverselor
salturi condiționate la aceeaşi locaţie din tabela de predicţii, lucru constatat şi
eliminat prin simulări proprii ale “Centrului de cercetare în arhitecturi avansate
de procesare a informației” (CCAAPI), condus de autorul acestei cărți, în cadrul
Universității “Lucian Blaga” din Sibiu (v. http://acaps.ulbsibiu.ro/), după cum s-
a arătat în [Vin00].
Pan și coautorii (PAN S.T., SO K., RAHMEH J.T. - Improving the
Accuracy of Dynamic Branch Prediction Using Branch Correlation, ASPLOS V
Conference, Boston, October, 1992) analizează calitativ şi cantitativ, într-un
mod foarte atent, rolul informaţiei de corelaţie, pe exemple concrete extrase din
benchmark-urile SPECint '92. Se arată că, bazat pe predictoare de tip
numărătoare saturate pe doi biţi, schemele corelate (5-8 biţi de corelaţie utilizaţi)
ating acurateţi ale predicţiilor de până la 11% în plus, faţă de cele clasice.
De remarcat că un BPB clasic reprezintă un predictor de tip (0,n), unde n
este numărul biţilor de predicţie utilizaţi. Numărul total de biţi utilizaţi în
implementarea unui predictor corelat de tip (m,n) este:
N = 2m * n * NI,
unde NI reprezintă numărul de intrări al BPB-ului utilizat.

163
Există prezentate în literatura tehnică de specialitate mai multe
implementări de scheme de predicţie a ramificaţiilor, prima implementare
comercială a unei astfel de scheme făcându-se în microprocesorul Intel Pentium
Pro. Astfel, de exemplu, implementarea tipică a unui predictor corelat de tip
GAg (Global History Register, Global Prediction History Table) este prezentată
în Figura 3.34. Tabela de predicţii PHT (Prediction History Table) este adresată
cu un index, rezultat din concatenarea a două informaţii ortogonale: PClow (i
biţi), semnificând gradul de localizare al saltului în memorie, respectiv registrul
de istorie globală a saltului respectiv (HR- Global History Register, pe k biţi),
semnificând "contextul" dinamic în care se situează saltul în program. Având în
vedere că, de obicei, PClow este multiplu de 4, se recomandă eliminarea
ultimilor săi 2 biți (00). Astfel, tabela PHT va fi utilizată complet și nu doar o
locație din 4, cum ar fi situația dacă această eliminare nu s-ar face. De fapt,
introducerea PClow în adresarea tabelei, precum şi introducerea tag-urilor în
PHT, aparţin autorului acestei lucrări [Vin00]. Ambele contribuţii s-au făcut cu
scopul eliminării interferenţelor branch-urilor în tabela de predicţie. Adresarea
PHT exclusiv cu HR (registrul de istorie globală), ducea la serioase interferenţe
(mai multe salturi puteau accesa aceelaşi automat de predicţie din PHT), cu
influenţe evident defavorabile asupra performanţelor. Desigur, tabela PHT, ca
orice cache, poate avea diferite grade de asociativitate. Un cuvânt din această
tabelă are un format similar cu cel al cuvântului dintr-un BTB. Se arată în
literatura de specialitate că se poate evita concatenarea HR şi PClow în
adresarea PHT, cu rezultate foarte bune, printr-o funcţie de dispersie de tip SAU
EXCLUSIV între acestea (hashing), care să adreseze tabela PHT cu un index de
lungime mai mică. Evident, această compresie a indexului de adresare are o
influenţă benefică asupra capacităţii tabelei PHT.
În scopul reducerii interferenţelor diverselor salturi în tabela de predicţii, se
prezintă o schemă numită PAg - Per Address History Table, Global PHT, a
cărei structură este oarecum asemănătoare cu cea a schemei GAg. Componenta
HR*(k) a introdus-o autorul acestei lucrări, având semnificaţia componentei HR
de la varianta GAg, adică un registru global al istoriei salturilor precedente, care
memorează comportarea ultimelor k salturi. Fără această componentă, credem că
schema PAg şi-ar pierde din capacitatea de adaptare la contextul programului, în
sensul în care schema GAg o face. În opinia autorului acestei lucrări, Yeh şi Patt
renunţă într-un mod curios şi eronat la informaţia de corelaţie globală (HRg), în
trecerea de la schemele de tip GAg la cele de tip PAg, în favoarea exclusivă a
informaţiei de corelaţie (istorie) locală (HRl). În schimb, componenta din

164
structura History Table, conţine "istoria locală" (taken/ not taken) a saltului
curent, cel care trebuie predicţionat. După cum se va arăta mai departe,
performanţa schemei PAg este superioară celei obţinute printr-o schemă de tip
GAg, cu tributul de rigoare plătit complexităţii hardware, de mărime
exponențială.

Figura 3.34. Structură de predicţie de tip GAg


Următoarea schemă de predicţie (de tip PAg) predicţionează pe baza a trei
informaţii cvasi-ortogonale, toate disponibile pe chiar timpul fazei IF: istoria
HRg a anterioarelor salturi corelate (taken / not taken), istoria saltului curent
HRl şi PC-ul acestui salt. O astfel de schemă, ca și următoarea, fac parte din
clasa de scheme numite Two Level Adaptive Branch Predictors, datorită faptului
că, mai întâi se adresează tabela de istorie, iar apoi, pe baza contextului memorat
în această tabelă, se adresează predictorul din tabela de predicții. Dacă adresarea
tabelei PHT s-ar face în schema PAg cu HR concatenat cu PClow(i), atunci,
practic, fiecare branch ar avea propria sa tabelă PHT, rezultând deci o schemă şi
mai complexă, numită PAp (Per Address History Table, Per Address PHT), a
cărei schemă de principiu este prezentată mai jos (Figura 3.36). Complexitatea
acestei scheme o face practic neimplementabilă în siliciu la ora actuală, fiind
doar un model utilizat în cercetare, prin simulări software, pentru evaluarea
performanțelor. Se dezvoltă variațiuni mai simple ale acestei scheme, bazate în
principal pe hashing-ul (PC, HRg, HRl), în scopul reducerii capacităților
tabelelor de predicție.

165
Figura 3.35. Structură de predicţie de tip PAg

Figura 3.36. Structură de predicţie de tip PAp


După rezolvarea branch-ului curent, noua predicție a acestuia poate fi
transferată din PPHT în locația corespunzătoare din PBHT. Astfel, la următoarea
procesare a branch-ului, se evită dubla indexare, cu efecte pozitive asupra
timing-ului schemelor adaptive pe două niveluri. Desigur, este posibil ca o parte
dintre branch-urile memorate în registrul HR, să nu se afle în corelaţie cu
branch-ul curent, ceea ce implică o serie de dezavantaje. În astfel de cazuri,
pattern-urile din HR pot pointa, în mod inutil, la intrări diferite în tabela de
predicţii, fără beneficii asupra performanţei predicţiei, dimpotrivă chiar,
separându-se astfel situaţii care nu trebuie separate. Mai mult, aceste situaţii pot

166
conduce la un timp de "umplere" a structurilor de predicţie mai îndelungat
(implicând cold misses), cu implicaţii defavorabile asupra performanţei.
În opinia autorului, o critică valabilă pentru toate schemele corelate
constă în faptul că informaţia de corelaţie globală (HRg) este deseori
insuficientă în predicţie. Mai precis, spre exemplu, în predicţia saltului curent
notat b4, să considerăm că se dispune de conţinutul lui HRg = 101 şi respectiv
HRl = 01. De asemenea, să considerăm că cele 3 salturi anterioare celui curent şi
al căror comportare (taken / not taken) este dată de conţinutul HRg (101 aici), au
fost b1, b2 şi b3. Într-o următoare instanţă a apariţiei saltului b4, în exact acelaşi
context dinamic al conţinuturilor HRg şi HRl ca cel precedent, se va apela
acelaşi automat de predicţie accesat de anterioara apariţie a lui b4. Această
acțiune poate fi total ineficientă, având în vedere că nu există nicio garanţie a
faptului că şi de această dată, cele trei salturi anterioare lui b4 au fost b1, b2 şi
b3, exact ca în cazul precedent. Prin urmare HRg nu poate "prinde" întreg
contextul de apariţie al unui anumit salt condiționat (b4 aici). Acest lucru l-am
demonstrat pe bază de statistici efectuate pe trace-urile benchmark-urilor
Stanford HSA, arătând că există salturi care în acelaşi context (HRg, HRl) au
comportări “aberante” (haotice, cvasi-aleatoare), adică de exemplu în 56% din
cazuri s-au făcut, iar în celelalte 44% din cazuri, nu s-au făcut, într-un mod
amestecat. Prin urmare, aceste salturi sunt practic impredictibile, din motivul că
"acelaşi context", nu este în realitate acelaşi! Soluţia, în opinia autorului acestei
cărți, demonstrată de altfel la nivel de simulare software prin cercetări ale
autorului şi ale colaboratorilor săi, ar consta în asocierea fiecărui bit din HRg cu
PC-ul aferent saltului respectiv şi accesarea predicţiei pe baza acestei informaţii
mai complexe, conținând și informația de cale a salturilor anterioare (path), nu
doar istoria acestora. Astfel, am putea avea siguranţa că la contexte diferite de
apariţie a unui anumit salt, se vor apela automate diferite de predicţie, asociate
corect contextelor. Astfel s-ar reduce, din păcate nu semnificativ (!), din efectele
unui fenomen de interferenţă a predicţiilor sesizat de autor sub denumirea de
“unbiased branches” (branch-uri nepolarizate într-un context dinamic dat,
precum în exemplul precedent) [Vin08, Vin08b]. Compararea acestor noi
scheme de predicţie trebuie făcută cu scheme clasice având aceeaşi complexitate
structurală. Desigur, comprimarea acestui complex de informaţie (HRg cu PC-
urile aferente) este posibilă şi chiar necesară, având în vedere necesitatea unor
complexități și costuri rezonabile ale acestor scheme. Ea se poate realiza prin
utilizarea unor funcţii de dispersie simple (de ex. tip SAU- EXCLUSIV).
Oricum, această observaţie simplă poate conduce la îmbunătăţiri substanţiale

167
ale acurateţii de predicţie, comparativ cu oricare dintre schemele clasice
prezentate. Beneficiile unei asemenea idei novatoare pot fi preliminate cantitativ
prin determinarea în cadrul unui anumit trace (urmă de program dinamic, v.
Glosarul) a direcţiei (taken / not taken), pentru fiecare instanţă a unui anumit
salt, apărut "în aceleaşi context" dat HRg, HRl. Procentaje (mult) diferite de
100% pentru direcţia predilectă a saltului, nu vor arăta decât necesitatea
implementării acestei idei, după cum am mai arătat.

Problema salturilor condiţionate impredictibile

În continuare vom face o paranteză, în cadrul acestui paragraf mai special,


utilă doar pentru cei interesați de comportamentul entropic al unor salturi
condiționate, practic impredictibile, și înțelegerea acestui comportament pe baze
mai riguroase, care țin de teoria informației. Prezentarea se face prin preluarea
unor pasaje din lucrarea noastră [Vin08], revizuite și augmentate aici prin câteva
explicații suplimentare.
După cum am arătat până la acest punct al prezentării, pentru fiecare
branch dinamic (în curs de procesare), predicţia se face pe baza unei informaţii
binare de context, ataşate respectivului branch şi considerate relevante pentru
comportamentul său (istoria locală a branch-ului, memorată pe un anumit număr
de biţi, istoria globală – informația de corelație globală, cu alte branch-uri
anterior procesate, calea de program pe care s-a ajuns la saltul condiţionat curent
– branch’s path etc.) Ce s-a observat statistic? Că anumite branch-uri dinamice,
într-un anumit context binar de apariţie, au un comportament nepolarizat, fiind
atât Taken cât şi Not Taken în proporţii semnificative (unbiased branches le-am
numit în [Vin07, Vin08, Vin08b] și în alte lucrări, adică branch-uri cu un
comportament – Taken/Not Taken – nepolarizat într-un anumit context dinamic
de apariție).
Mai precis, se consideră contextul de apariţie al unui branch dinamic ca
fiind reprezentat pe p biţi. Fiecare branch static are asociate k contexte dinamice
de apariţie ( k ≤ 2 p ). Indexul de polarizare al unui branch apărut într-un context
dat se defineşte ca fiind:
 f 0 , f 0 ≥ 0.5
P( S i ) = Max( f 0 , f 1 ) = 
 f 1 , f 0 < 0.5
unde:

168
• S = {S1 , S 2 , ..., S k } = mulţimea contextelor distincte, apărute pentru toate
instanţele branch-ului;
• k = numărul de contexte distincte, k ≤ 2 p ;
T NT
• f0 = , f1 = , NT = numărul de instanţe Not_Taken
T + NT T + NT
corespunzătoare contextului Si, T = numărul de instanţe Taken
corespunzătoare contextului Si, (∀) i = 1, 2, ..., k şi evident f 0 + f1 = 1 ;
• P(Si)∈ [0.5,1] , semnificând pentru minimul 0.5, o nepolarizare maximă a
contextului S i (fully unbiased).
În plus, aceste salturi condiţionate au şi un comportament haotic, entropic,
adică în contextul dat sunt amestecate (Taken/Not_Taken) într-un mod ce pare a
fi relativ aleator. În consecinţă, aceste branch-uri sunt practic impredictibile
(comportamentul lor nu poate fi învăţat). Cum am abordat problema înţelegerii
şi predicţiei lor [Vin07, Vin08b]?
Prima idee de rezolvare a fost aceea de a mări lungimea (istoria)
contextului binar asociat fiecărui branch. Dacă la un context pe 32 de biţi, cca.
17% din branch-uri erau unbiased şi impredictibile prin algoritmii şi schemele
utilizate, la unul pe 64 de biţi, acest procentaj a scăzut la cca. 4%, ceea ce
reprezintă totuşi mult. De precizat că un context de predicţie de 64 de biţi nu
este fezabil din punct de vedere practic, implicând complexităţi exponenţiale ale
predictoarelor aferente, deci problema este în realitate şi mai deranjantă. Totuşi,
după cum de altfel am intuit, o istorie mai lungă le-a micşorat „entropia”. Aceste
branch-uri de tip unbiased au repercursiuni negative asupra performanţei
globale a microprocesoarelor. Am arătat că cele mai sofisticate predictoare ale
momentului, unele preluate direct din simulatoarele software publicate în cadrul
Campionatului mondial de branch prediction organizat de compania Intel în
anul 2004, obţin acurateţi foarte scăzute de predicţie pentru aceste branch-uri, în
jur de 70% [Vin08, Vin08b] (în timp ce un salt "normal" polarizat este prezis cu
acurateţi de 97%-99%). Predictoarele utilizate erau hibride, fiind compuse în
general din seturi de predictoare Markov de diferite ordine (Prediction by
Partial Matching) şi din predictoare dinamice neuronale.
A 2-a idee de rezolvare a problemei a fost aceea de a căuta alte informaţii
de context, relevante, care să le reduca entropia (haosul) şi deci să devină, astfel,
mai predictibile. Cu alte cuvinte, am căutat acele informaţii relevante prin
prisma cărora branch-urile unbiased să devină mai polarizate şi deci, mai
predictibile. Aşadar, în cazul acestor branch-uri nepolarizate, comportamentul
lor memorat ca o secvenţă de ‘0’ (Not_Taken) şi de ‘1’ (Taken), este

169
impredictibil din punct de vedere al nevoilor noastre inginereşti. De ce oare? În
fond, ele sunt generate prin compilarea unor programe cu acţiuni deterministe,
nicidecum aleatoare. Or fi aceste branch-uri "aleatoare", sau doar relativ
impredictibile prin structurile şi informaţiile de context utilizate? În continuare
se prezintă cîteva reflecţii asupra aleatorismului, cu scopul practic de a sugera
câteva metrici care să caracterizeze o secvenţă (binară) aleatoare. Aceste metrici
ar putea justifica din punct de vedere teoretic mai bine aleatorismul
(impredictibilitatea) acestor salturi condiţionate.
Aşadar întrebarea mai generală, din punct de vedere științific, care se
pune este: în fond, ce înseamnă aleator? În particular, ce înseamnă un şir
(infinit) binar aleator? De multe ori, aleatorul se confundă cu impredictibilul.
Totuşi, care ar fi diferenţele specifice între aceste două noţiuni? Impredictibilă
este o secvenţă de simboluri care nu poate fi prezisă suficient de exact printr-o
metodă (algoritm) de predicţie. Se poate da o definiţie matematică intrinsecă,
ontologică, a unui şir aleator de simboluri? (deci care să nu facă referire la
metodele de predicţie, ci doar la proprietăţi intrinseci ale şirului respectiv.) Poate
genera rularea unui program determinist secvenţe "aleatoare", sau acest fapt
trebuie respins apriori? (Un şir aleator însă, ar putea conţine subşiruri
deterministe sau măcar corelate stohastic, deci predictibile.) Mai general, există
oare fenomene cu adevărat aleatoare în lumea fizicii macroscopice, guvernată de
legi şi de modele matematice deterministe? Dar în lumea ştiinţei şi ingineriei
calculatoarelor? Nu cumva aleatorul şi impredictibilul apar în această lume
macroscopică, doar ca efect al lipsei de informaţii relevante ale observatorului
sau a dificultăţilor acestuia de a gestiona multitudinea informaţiilor? (Este, spre
exemplu, incomparabil mai comodă previzionarea rezultatului aruncării unei
monede, prin modele stohastice, decât prin calcule deterministe, care ar fi
deosebit de complexe.) Cu alte cuvinte, modelele stohastice nu sunt oarecum
artificiale, justificabile doar ca instrumente comode (și relativ eficiente) de
contracarare, sau de ținere sub un oarecare control, a incertitudinii? O informaţie
relevantă asupra generatorului de secvenţe haotice (în general, greu sau chiar
imposibil de găsit), nu le-ar reduce „devălmăşia”, astfel încât aceste secvenţe să
devină (mai) predictibile? Așadar, problema esențială în acest caz este aceea a
reprezentării adecvate a acestor salturi, într-un spațiu ortogonal cu mai multe
dimensiuni sau cu alte dimensiuni, în vederea predictibilității lor. O asemenea
reprezentare ar conduce și la predictibilitatea lor. Dar care sunt oare
caracteristicile, trăsăturile relevante (features) pentru o astfel de reprezentare?
Dacă este aşa, oare merită să încercăm să le predicţionăm cu algoritmi mai

170
puternici, de genul modelelor adaptive Markov cu legături ascunse, Hidden
Markov Models - HMM [Vin07, Vin08b]? Această ipoteză se bazează pe faptul
că aceste modele stohastice cu legături ascunse, care consideră că stările
modelului Markov observabil sunt generate de un alt model Markov, ascuns de
astă dată, ar putea compensa necunoaşterea acelor informaţii relevante pentru
predicţie, prin chiar procesul stohastic ascuns, pe post de generator al stărilor
observabile (proces generator cu o semantică neinteligibilă pentru observator).
Prin utilizarea acestor modele HMM, “aleatorul” n-ar mai constitui, poate, o
fatalitate ireductibilă, impredictibilă, din cauza modelului stohastic ascuns, care
îl generează pe cel observabil. Astfel, acurateţea predicţiei unui şir de simboluri
printr-un astfel de model puternic, n-ar putea constitui oare un grad de
aleatorism intrinsec al secvenţei, din punct de vedere practic? Cu cât acuratețea
predicției șirului respectiv prin HMM este mai mare, cu atât gradul de
aleatorism al secvenței este mai mic și invers. Răspunsul este așadar pozitiv și a
fost dat de noi în lucrarea [Vin08b], unde am arătat că gradul de aleatorism al
unui șir de simboluri este dat și de rata de compresie a acelui șir (prin algoritmi
de compresie Huffman sau Gzip), dar și de măsura entropiei sale informaționale.
Notă. Un model stohastic de tip Hidden Markov Model (HMM), notat
sintetic ( A, B, π ) , se definește astfel:

1. N – numărul de stări ascunse, cu mulțimea de stări ascunse S = {S0, S1, …,


SN-1} și qt starea ascunsă la momentul discret de timp t. În procesul de
predicție, N va fi variat pentru a obține o valoare optimală (v. în continuare).
2. M – numărul de stări observabile, cu mulțimea de stări observabile V = {V0,
V1, …, VM-1} și Ot starea observabilă de la momentul t.
3. A = {aij} – probabilitatea de tranziție între starea ascunsă actuală Si și starea
ascunsă viitoare Sj, unde aij = P[qt +1 = S j qt = S i ], 0 ≤ i, j ≤ N − 1 . Practic, A
este o matrice de probabilități de tranziție între stările ascunse.
4. B = {bj(k)} – probabilitatea ca în starea ascunsă actuală Sj să se genereze
starea observabilă Vk, unde
b j (k ) = P[Ot = Vk qt = S j ], 0 ≤ j ≤ N − 1, 0 ≤ k ≤ M − 1 . Practic, B este o
matrice de probabilități de legătură între stările ascunse și cele observabile, la
momentul actual t.
5. π = {πi} – probabilitățile inițiale ale stărilor ascunse, unde
π i = P[q1 = S i ], 0 ≤ i ≤ N − 1

Inițializarea modelului HMM se face astfel:

171
• Probabilitățile de tranziție între stările ascunse A(NXN) = {aij} sunt
inițializate cvasi-aleator, fiecare în jurul valorii 1/N, astfel încât suma pe
fiecare rând să fie 1 (condiție de echilibru).
• Probabilitățile stărilor observabile B(NXM) = {bj(k)} sunt inițializate cvasi-
aleator, fiecare în jurul valorii 1/M, astfel încât suma pe fiecare rând să fie 1
(condiție de echilibru).
• Probabilitățile inițiale atașate stărilor ascunse π(1XN) = {πi} sunt setate în
jurul valorii 1/N fiecare, astfel încât suma lor să fie 1.

În procesul de predicție parametrii modelului ( A, B, π ) sunt modificați astfel


încât să se maximizeze probabilitatea secvenței de stări observabile de până la
momentul curent, P(O λ ) , prin metode iterative de gradient [Vin07].
Să considerăm acum, spre exemplu, secvenţele binare pe 23 de biţi:

• 01010101010101010101010
• 01101010000010011110011

Prima secvenţă apare ca fiind una periodică, iar a 2-a, pare a fi una
aleatoare, deşi ambele au aceeaşi probabilitate de apariţie, anume 1/223. De ce
oare a 2-a secvenţă pare aleatoare, iar prima, nu? Nu cumva este aceasta doar o
impresie subiectivă, ţinând de structura vizual-cognitivă a omului? Mai ales că şi
cea de a 2-a secvenţă este una deterministă, reprezentând primele zecimale ale
numărului iraţional netranscendent 2 ! Existând deci un algoritm care o
generează, iar știind acest fapt, desigur că secvenţa nu mai este percepută ca
fiind aleatoare. Dacă însă algoritmul generator ne este ascuns, secvenţa ne poate
apărea ca fiind aleatoare. Este clar deci că nu ne putem baza prea mult pe intuiţia
noastră de a deosebi aleatorul, de determinist (non-aleator).
Considerăm acum un şir binar finit (secvenţă) X1X2 ... Xk, probabilitatea
de apariţie a lui ‘0’ fiind P(0)=a, iar cea de apariţie a lui ‘1’ fiind P(1)=b, unde
a+b=1. Fără specificarea acestor probabilităţi, definirea noţiunii de secvenţă
binară aleatoare pare să nu aibă sens. Dacă numărul de apariţii ale lui ‘0’ în şir
este m (implicit numărul de apariţii ale lui ‘1’ este k-m), atunci probabilitatea de
apariţie a secvenţei X1X2 ... Xk este:

k
P(X1X2 ... Xk)= ∏ P( Xi) =ambk-m
i =1

172
În particular dacă a=b=1/2 atunci P(X1X2 ... Xk)=1/2k. Totuşi, prin prisma
acestor consideraţii, o anumită secvenţă ar fi la fel de aleatoare ca oricare alta,
fapt aflat în contradicţie cu intuiţia noastră subiectivă despre aleator, după cum
am mai arătat.
O măsură a aleatorismului unei secvenţe S de simboluri, un simbol
oarecare aparţinând mulţimii X={X1X2 ... Xk}, s-ar putea baza chiar pe entropia
informaţională discretă a secvenţei S (de la Claude Shannon, cel care a definit-
k
o), anume E(S) = - ∑ P( Xi) log P( Xi) ≥ 0 , cu un maxim (log k) în cazul
i =1

simbolurilor echiprobabile în S. Evident, dacă logaritmul se scrie în baza 2, E(S)


reprezintă numărul minim de biţi care pot codifica simbolurile secvenţei (log2 k).
k
Dacă ∑ P( Xi) = 1 ,
i =1
atunci entropia informațională este o măsură a cantității de

informație dintr-o variabilă aleatoare. Cu alte cuvinte, entropia informațională


măsoară gradul de dezordine dintr-o mulțime de exemple.
Cum lim x log x = 0 (după un mic artificiu de calcul se aplică regula lui
x − − >0 , x > 0

l’Hospital. Demonstrați rezultatul!), în formula entropiei s-a considerat 0log0=0,


deși, din punct de vedere matematic, aceasta este o nedeterminare. Se definește
și noțiunea fertilă de câștig informațional în raport cu un anumit atribut (A), ca
Card ( Sv)
fiind Gain( S , A) = E ( S ) − ∑
v∈Values ( A)
E ( Sv)
Card ( S )
∈ [0, log2 k ], unde:

Values (A) – mulțimea valorilor posibile pentru atributul A


Sv – submulțimea din S pentru care A are valoarea v.
Card (S), Card (Sv) – cardinalele celor două mulțimi
Gain (S,A) – reducerea de entropie – prin entropii specific E(Sv) – cauzată
de gruparea eșantioanelor în acord cu un anumit atribut, numit A. Rezultă
imediat că Gain( S , A) ∈ [0, E ( S )] . Pentru fiecare atribut se calculează câștigul
informational obținut, dacă mulțimea S este împărțită utilizând acel atribut. Într-
un proces de selecție a trăsăturilor (feature selection) ne convin acele atribute
care maximizează Gain (S,A), deci care nu afectează prea mult valoarea
entropiei globale E(S). Aceste atribute trebuie păstrate, pe când la celelalte,
având valori mici ale câștigului informațional, se poate renunța. Acest fapt este
echivalent cu minimizarea entropiei specifice atributului A. Cu alte cuvinte,
putem renunța la atributele cu entropie specifică mare, pentru că acestea reduc
semnificativ entropia E(S).

173
Totuşi, măsura entropiei nu este suficientă pentru a defini gradul de
aleatorism al unui șir de simboluri, întrucât se pot găsi secvenţe cu entropie
ridicată, dar care să fie generate în mod determinist. În [Vin07], relativ la o
secvenţă binară S, s-a definit indicatorul numit grad de amestecare, astfel:
0, nt = 0

D( S ) =  nt
 2 ⋅ Min('0' , '1' ) , nt > 0

unde:
• nt = numărul de tranziţii binare (0 1 sau 1 0) din secvenţa S;
• Min(‘0’,’1’) reprezintă minimul dintre numărul de apariţii al lui ‘0’
respectiv al lui ‘1’ în secvenţa S. Deci 2 ⋅ Min('0' , '1' ) = numărul maxim de
tranziţii binare posibile într-o secvenţă de lungimea lui S.
Se observă imediat că D(S) ∈ [0,1] , fiind maxim în cazul unor amestecări
maximale (50%) ale simbolurilor ‘0’ şi ‘1’ în secvenţă. Credem că o măsură
inginerească (empirică) acceptabilă de definire a gradului de aleatorism aferent
unei secvenţe binare S, ar putea fi dată de produsul D(S)*E(S) ∈ [0, log2 k].
Desigur că în locul entropiei informaționale discrete E(S) s-ar putea utiliza şi
alte metrici, precum energia informaţională Gini-Onicescu, definită ca
k
En(S)= ∑ P2(Xi) ∈ [1/k, 1], cu minimul atins în cazul simbolurilor
i =1

echiprobabile în S, sau sinergia informaţională, definită ca Sinerg(S) = -log


En(S).
Acad. prof. Solomon Marcus a arătat că întrebările despre semnificaţia
conceptului de şir aleator îi framântă pe matematicieni de peste 400 de ani şi a
făcut o scurtă prezentare a problemei [Vin08]. Astfel, în secolul trecut (1909) s-
au propus diferite modele matematice ale aleatorului, primul find cel propus de
către matematicianul francez Emile Borel, sub forma de şir normal (el a avut în
vedere secvenţe descriind numere reale, dar chestiunea se extinde la şiruri
infinite arbitrare de simboluri, pe un anumit alfabet finit). Aleatorul, după Borel,
ar reveni la faptul că, pentru orice n, blocurile de n termeni din şir au aceeaşi
probabilitate de apariţie în şir. În cazul şirurilor pe un alfabet de m simboluri
respectiv al şirurilor binare, această probabilitate este egală cu 1/mn
respectiv1/2n. Borel a arătat că aproape toate numerele reale (cu excepția unei
infinități numărabile), scrise în reprezentare zecimală infinită, sunt aleatoare,
conform definiţiei sale. Acest fapt este justificabil şi pe criterii de calculabilitate
- Turing. Totuşi, remarcăm că această definiţie a lui Borel nu este constructivă
(efectivă), în sensul permiterii generării de şiruri aleatoare concrete. Orice

174
algoritm de concatenare a pattern-urilor de lungime dată, conduce la negarea
caracterului aleator al şirului. Rezultă deci că se pot construi şiruri Borel-
normale care nu sunt şi aleatorii.
Au urmat alte modele matematice, mai restrictive, bazate pe
complexitatea descrierii unei secvenţe de simboluri. Matematicianul A. N.
Kolmogorov (cel care a axiomatizat teoria probabilităţilor în 1933) şi G. Chaitin
au definit, în anii '60 ai secolului trecut, aleatorismul unei secvenţe finite de
simboluri, cu referire la lungimea celui mai scurt program de calculator
(algoritm) care descrie secvenţa respectivă. Dacă lungimea acestui program este
(sensibil) egală cu lungimea secvenţei însăşi, atunci secvenţa este considerată
aleatoare (deci nu există un program generator, mai scurt). Într-o formulare
echivalentă, un şir aleator nu poate fi comprimat. Deci, din punct de vedere
practic, rata de compresie a unei secvenţe de simboluri ar putea constitui o
măsură, empirică desigur, a aleatorismului acelei secvenţe. Totuşi, un corolar al
acestei definiţii ar conduce la ideea inacceptabilă că majoritatea şirurilor mai
scurte decât lungimea programului minim de generare a lor, sunt aleatoare.
Evident că lungimea de reprezentare a celui mai scurt program (algoritm) care
poate genera un anumit şir de simboluri x, numită şi complexitate Kolmogorov
K(x) sau entropie algoritmică, depinde de limbajul formal de descriere
considerat. Ideea de esenţă este că unei complexităţi reduse i se asociază
ordinea, în timp ce uneia ridicate, i se asociază aleatorul (haosul). În general,
matematicienii au preferat să abordeze problema considerând mulţimea tuturor
şirurilor binare infinite, în locul unor secvenţe finite.
Între conceptul de aleator şi cel de algoritm, respectiv de calculabilitate,
există o strânsă dependenţă. Un şir de simboluri generat printr-un algoritm
(funcţie calculabilă Turing) nu este aleator, şirul fiind predictibil prin însuşi
algoritmul care îl generează (pe post de predictor). Pentru adâncirea acestei
intuiţii este necesară reamintirea conceptului fertil de maşină Turing, descris în
1936 de către matematicianul britanic Alan Mathison Turing.

Mașina Turing a apărut din cauza unei celebre provocări matematice, puse
de marele matematician german David Hilbert la începutul secolului XX (a 10-a
problemă deschisă, dintr-o listă celebră a lui Hilbert). Aceasta întreba dacă
există un algoritm care să decidă dacă o teoremă (afirmație matematică) este
demonstrabilă sau nu, în cadrul unui sistem axiomatic dat, considerând regulile
logicii clasice (Entscheidungsproblem – problema decidabilității; mai precis, dar
mai particular, formularea lui Hilbert cerea un algoritm, care să decidă dacă o
ecuație diofantică de tip polinomial, având coeficienți întregi, are sau nu are o

175
soluție din mulțimea numerelor întregi.) Această problemă a necesitat mai întâi
definirea noțiunii de algoritm, pe care Turing a făcut-o sub forma mașinii
virtuale, care azi îi poartă numele. Alonzo Church a dat un răspuns negativ la
această problemă în anul 1936. Imediat după această ispravă științifică, printr-un
articol publicat în anul 1937, Turing a arătat și el acest fapt, pe baza conceptului
de mașină Turing introdus de el și care, a demonstrat că este echivalent cu
abordarea lui Church (calculul lambda). Problema a fost rezolvată definitiv, într-
un mod pe deplin riguros, de Matiyasevich, abia în anul 1970.
O maşină Turing este constituită dintr-o bandă infinită de celule egale şi
dintr-un cap de citire/scriere pe post de automat finit. Fiecare celulă conţine un
simbol s ∈ S={0,1,blank}. Secvenţa de intrare x aparţine mulţimii tuturor
secvenţelor binare finite (FB – Finite Binary) şi poate codifica, în diverse
moduri, datele de intrare. Capul de citire-scriere citeşte o celulă (simbol) în
fiecare pas. Acest cap poate fi într-o stare aparţinând mulţimii Q={q0q1q2 … qf},
unde q0 este starea iniţială (start), iar qf este cea finală (stop). Funcţie de
simbolul s(t) citit curent şi de starea curentă q(t), capul de citire-scriere scrie un
nou simbol s(t+1) în celula curentă, se mută o poziţie la stânga (L, Left) sau la
dreapta (R, Right) şi tranzitează într-o nouă stare q(t+1). Aşadar fiecare pas este
caracterizat de 5-uplul TMt(x)={q(t), s(t); s(t+1), q(t+1), m} unde m∈ M={L,R}.
TMt(x) se mai numeşte şi instrucţiune, secvenţa tuturor instrucţiunilor
constituind programul executat de maşină. Aşadar, din punct de vedere formal, o
maşină Turing este o funcţie f:QxS QxSxM. Mulţimea TM a tuturor maşinilor
Turing este una numărabilă1. Acest fapt se justifică imediat, întrucât se poate
trece de la mulţimea maşinilor Turing cu k instrucţiuni (TMk) la a celor cu (k+1)
instrucţiuni (TMk+1) ∀k =3,4,5..., rezultând deci că mulţimea TMk este
numărabilă. Evident că în cadrul unei mulţimi TMk există un număr finit de
maşini Turing, deci mulţimea TM este numărabilă. Dacă o maşină Turing
ajunge în starea finală (converge), calculul se opreşte. (Altfel, calculul diverge,
continuând în mod nedeterminat.) În urma acestui calcul, pentru ∀x ∈ FB se
generează secvenţa de ieşire TM(x). Prin urmare, fiecare maşină Turing
defineşte o funcţie parţială2 a mulţimii FB pe ea însăşi, semnificând faptul că, în
fond, calculul (algoritmul) este echivalent cu o procesare de simboluri. Spre
exemplu, se poate construi o maşină Turing care să adune, să înmulţească etc.
oricare M numere întregi, fiecare codificat binar pe N biţi. Aceste operaţii nu
sunt altceva decât funcţiile parţiale amintite. Un rezultat important constă în
demonstrarea faptului că există o infinitate de aşa numite maşini Turing
1
O mulţime este numărabilă dacă poate fi pusă în corespondenţă bijectivă cu mulţimea numerelor naturale N.
2
Dacă funcţia este definită pe mulţimea tuturor şirurilor binare, se zice totală

176
universale (TU). O astfel de maşină TU poate simula orice maşină Turing, dacă i
se codifică la intrare setul de instrucţiuni al maşinii simulate TM urmat de
intrarea x. Evident, la ieşirea TU se va genera TM(x) [Vin08].
Orice şir binar din FB poate reprezenta un număr natural prin intermediul
unei funcţii de codificare c:FB N. Conform lui Sergio Volchan (citat în
lucrarea noastră [Vin08]), o funcţie parţială f:N N este „Turing-calculabilă”
dacă ∀n ∈ N, ∃ x ∈ FB cu n=c(x) pentru care maşina se opreşte, generând la final
codificarea binară a lui n prin funcţia c, adică f(n)=c(TM(x)). Cum mulţimea TM
este numărabilă, rezultă că mulţimea funcţiilor parţiale Turing-calculabile este şi
ea numărabilă. Această mulţime aparţine mulţimii tuturor funcţiilor parţiale
f:N N. Rezultă că funcţiile calculabile sunt „infinit-puţine” (cardinal alef 0),
din punctul de vedere al teoriei infiniţilor actuali, iniţiată de Georg Cantor.
În acest moment merită amintită cunoscuta teză (practic o conjectură) a
lui Church-Turing, care, în esenţă, afirmă că pentru orice algoritm (procedură
care se termină într-un număr finit de paşi), există o maşină Turing echivalentă.
Noţiunea de algoritm, în sens clasic, impune ca acesta să poată fi implementat
pe o maşină Turing. Cu alte cuvinte, dacă un calculator poate implementa un
algoritm (o funcţie intuitiv calculabilă), acesta poate fi implementat şi de o
anumită maşină Turing. Reciproca nu este însă adevarată. Un algoritm
implementat de o maşină Turing nu este în mod sigur implementabil pe un
calculator real, cel puţin datorită resurselor infinite de memorie ale maşinii
abstracte. Această teză a adâncit înţelegerea noţiunii de algoritm de-a lungul
timpului.
Există probleme care nu pot fi rezolvate de către nicio mașină Turing,
deci, de către niciun algoritm. Un exemplu ar putea fi cel al conjecturii lui
Christian Goldbach (1690-1764), care afirmă că orice număr par (natural) mai
mare ca doi, poate fi scris ca sumă a două numere prime (ex. 108=37+71; v.
problema 46 de la finalul acestei cărți). Nu s-a putut încă dovedi această
afirmație, deși ea pare a fi adevărată, în urma unor testări laborioase, prin
programe de calculator dedicate, până la numere foarte mari. Nu se cunoaște
niciun algoritm care să demonstreze această conjectură. Un alt tip de probleme
grele sunt cele numite NP-hard (intractable), care nu admit soluții rezonabile din
punct de vedere al complexității (admit doar soluții de complexitate
exponențială). Un exemplu în acest sens este dat de celebra problemă a
comisului voiajor (travelling salesman problem): fie o listă de orașe cu
distanțele între oricare două orașe, cunoscute. Problema cere ruta de lungime
minimă care vizitează fiecare oraș exact o singură dată și revine în orașul de

177
origine. Complexitatea acestei probleme NP-hard (non-deterministic polynomial
time) este de natură combinațională. O altă problemă, mai simplă (admițând
soluții de complexitate polinomială) este cea a determinării unui circuit Euler
într-un graf și a fost formulată pentru prima dată de marele matematician
Leonhard Euler (1707-1783), probabil cel mai prolific matematician al tuturor
timpurilor. Problema cere determinarea unui drum închis într-un graf neorientat
care să conțină fiecare arc al grafului exact o singură dată. (Reamintim că se
numește graf neorientat o pereche ordonată de mulțimi notată G=(N, M), unde
N este o mulțime finită nevidă, ale cărei elemente se numesc noduri, iar M este o
mulțime de perechi neordonate de elemente din N, ale cărei elemente se numesc
muchii sau arce.) Evident că, un algoritm de tip brut force (căutare exhaustivă)
va genera o listă a tuturor permutărilor de arce posibile, adică N! posibilități și
va verifica pentru fiecare, dacă este sau nu este un circuit! Desigur că multe
dintre aceste permutări nu vor reprezenta un circuit Euler, nici măcar drumuri
posibile. Euler a demonstrat că un graf deține un circuit Euler dacă și numai
dacă în fiecare nod converge un număr par de arce. Intuitiv, acest rezultat afirmă
că orice nod de intrare are un nod de ieșire corespunzător, deci nodurile de
intrare-ieșire sunt perechi [Hay98].
Revenind la definiţia unui şir binar aleator, ar trebui ca un asemenea şir să
nu fie generabil de un algoritm. Cu alte cuvinte, secvenţa de biţi nu trebuie să fie
generabilă printr-o funcţie Turing-calculabilă. Acest fapt ar fi echivalent cu a
cere ca secvenţa să fie generată printr-o funcţie care nu este Turing-calculabilă.
Asociind secvenţele aleatoare cu funcţiile parţiale care nu sunt Turing-
calculabile (şi care le-ar putea genera), rezultă că aceste secvenţe aleatoare sunt
nenumărabile (de puterea continuului), deci majoritare pe mulţimea tuturor
funcţiilor parţiale f:N N. Aparent paradoxal şi oarecum ironic, deşi sunt
infinit-majoritare, nu se poate specifica niciuna! Din păcate, nici această
definiţie a secvenţelor aleatoare, bazată pe conceptul de calculabilitate Turing,
nu are o utilitate practică clară, de natură inginerească, pentru că defineşte
secvenţele aleatoare în mod abstract, fără să le genereze efectiv. Pe de altă parte,
generarea acestora ar fi în contradicţie cu însuși aleatorismul lor!
Nu există încă o paradigmă universală satisfăcătoare pentru şirurile
aleatoare de simboluri, problema fiind de actualitate şi de interes pentru multe
categorii de specialişti, nu doar pentru matematicieni, informaticieni, fizicieni,
electroniști etc. Definirea şi înţelegerea aleatorului sunt, deloc surprinzător,
legate strâns de noţiuni precum cele de calculabilitate, entropie informaţională,
algoritmi, teoria complexităţii, teoria infiniţilor actuali a lui Cantor etc. Relativ

178
la problema salturilor condiţionate impredictibile, s-au propus cu succes
următoarele idei practice de caracterizare a acestora din punct de vedere al
gradului lor de aleatorism [Vin08, Vin08b, Vin07]:

• Acurateţea predicţiei unui şir de simboluri printr-un predictor HMM ar


putea defini un anumit grad de aleatorism al secvenţei, din punct de
vedere practic.
• O măsură inginerească acceptabilă de definire a gradului de aleatorism
aferent unei secvenţe binare S, bazată pe entropia discretă E(S), ar putea fi
dată de produsul D(S)E(S).
• Rata de compresie a unei secvenţe de simboluri, obţinută prin algoritmi
cunoscuţi de compresii fără pierderi (spre exemplu, Huffman, Gzip), ar
putea constitui o altă măsură a aleatorismului secvenţei. În cazul
secvenţelor binare generate de comportamentul (Taken / Not Taken)
salturile dificil predictibile, rata de compresie a acestora trebuie să fie mai
mică decât în cazul celorlalte branch-uri.
• Complexitatea Kolmogorov a secvenţei de program maşină care generează
salturile condiţionate impredictibile ar putea constitui o altă metrică care
să le caracterizeze aleatorismul. Astfel, complexitatea acestora ar trebui să
fie mai mare decât a celorlalte salturi condiţionate (adică a celor
polarizate).

O altă cauză a unor comportamente "aberante" ale ramificaţiilor de program


(comportamente diferite în contexte identice) poate fi cauzată de anumite
pattern-uri mai "defavorabile" ale comportării respectivului salt. Astfel, spre
exemplu, un "loop" (salt condiționat situat la finele unei bucle de program) care
se face de 99 de ori şi, o dată, la ieșirea din buclă, nu se face, este practic
imposibil de a fi predicţionat corect 100% pe un context, normal, de genul HRg
=111 respectiv HRl=11. De ce ? Pur şi simplu pentru că nu se ştie dacă acel 111
conţinut în HRg este sau nu este cel de dinaintea "catastrofei" (a momentului în
care se iese din buclă). Ce informaţii ar mai fi necesare atunci "prinderii" unor
astfel de comportări imprevizibile. Fără îndoială acele PC-uri asociate fiecărui
bit din HRg vor fi inutile în acest caz, în schimb ar putea fi utilă aici memorarea
contorului de iteraţii undeva în tabelele de predicţie. Această ipoteză ar putea fi
confirmată prin simulări specifice.
O comparare echitabilă între schemele de predicţie clasice şi cele corelate
trebuie să impună acelaşi număr de biţi utilizaţi în implementarea celor două

179
scheme de comparat. Astfel, de exemplu, se compară un predictor (0,2) de
capacitate 4k cu predictor (2,2) de capacitate 1k. Acurateţea predicţiilor schemei
corelate este, în mod clar, mai bună. Simulările s-au făcut pe procesorul DLX,
bazat pe 10 benchmark-uri SPECint 92. Schema corelată a obţinut predicţii
corecte în 82%-100% din cazuri. Mai mult, predictorul (2,2) obţine rezultate
superioare în comparaţie cu un predictor de tip BPB având un număr infinit de
locaţii [Hen11].
Ceva mai recent, având în vedere complexitatea tot mai mare a acestor
predictoare, cu implicaţii defavorabile asupra timpului de căutare în structurile
aferente, se vehiculează ideea unor predictoare hibride, constând în mai multe
predictoare relativ simple, asociate diferitelor tipuri de salturi, în mod optimal.
Aceste predictoare, care se completează reciproc în momentul rulării
programelor (din punct de vedere al abilităților de predicție), se activează în mod
dinamic, funcţie de tipul saltului care este în curs de predicţionat. O strategie
rezonabilă ar fi ca să predicţioneze predictorul având cel mai mare grad de
încredere, la momentul dat. Această soluţie pare a fi cea care ar putea depăşi
performanţa complicatelor predictoare corelate pe două nivele.
O altă problemă dificilă în predicţia branch-urilor o constituie salturile
(JMP) / apelurile (CALL) codificate în moduri de adresare indirecte prin
registru, a căror acurateţe a predicţiei este deosebit de scăzută prin schemele
anterior prezentate (cca. 50%). Problema salturilor indirecte - de tip JMP (R5)
sau CALL (R5) - este de mare actualitate, cu precădere în contextul programelor
obiectuale, legat mai ales de implementarea polimorfismelor. Şi în filosofia
programării procedurale apar apeluri indirecte (ex. apel indirect la funcţii). În
acest caz, adresele de început ale diferitelor obiecte vizate sunt înscrise dinamic
în registrul de indirectare al saltului care implementează polimorfismul. Practic,
aici problema predicţiei direcţiei saltului este înlocuită cu una mult mai dificilă,
anume cu aceea a predicţiei valorii adresei acestuia. Chang propune o structură
de predicţie numită "target cache", special dedicată salturilor indirecte. În acest
caz, predicţia adresei de salt nu se mai face pe baza ultimei adrese ţintă a saltului
indirect, ca în schemele de predicţie clasice, ci pe baza alegerii uneia din
ultimele adrese ţintă ale respectivului salt, memorate în structură. Aşadar, în
acest caz, structura de predicţie memorează pe parcursul execuţiei programului
pentru fiecare salt indirect ultimele N adrese ţintă, exploatând o potențială
vecinătate a valorilor acestora, din punct de vedere statistic (v. Cap. 4).
Predicţia se va face deci în acest caz pe baza următoarelor informaţii: PC-ul
saltului, istoria acestuia, precum şi ultimele N adrese ţintă înregistrate. Structura

180
de principiu a target cache-ului e prezentată în figura următoare,anume Figura
3.37. O linie din acest cache conţine ultimele N adrese ţintă ale saltului
împreună cu tag-ul aferent.

Figura 3.37. Predicţia adresei în cazul salturilor indirecte


Informaţia de "istorie" provine din două surse: istoria saltului indirect sau a
anterioarelor salturi şi respectiv ultimele N adrese ţintă, înscrise în linia
corespunzătoare din cache. Aceste două surse de informaţie binară sunt
prelucrate prin intermediul unei funcţii de dispersie (în general de tip SAU
EXCLUSIV), rezultând indexul de adresare în cache precum şi tag-ul aferent.
După ce adresa ţintă a saltului devine efectiv cunoscută, se va introduce în linia
corespunzătoare din cache. Schema acţionează oarecum "în mod disperat",
mizând pe faptul că la acelaşi context dinamic de apariţie a unui salt indirect se
va asocia o aceeaşi adresă ţintă. O alternativă, inspirată din paradigma mai
generală a predicției dinamice a valorilor instrucțiunilor [Vin07], ar fi ca fiecare
adresă target să aibă asociat un contor de încredere (incrementat la predicție
corectă, decrementat la predicție eronată). Predicția curentă ar urma să aleagă
adresa având contorul asociat cu cea mai mare valoare, dacă aceasta este mai
mare decât un prag prestabilit (altfel, predicția nu se va mai face și se procesează
nespeculativ). Prin astfel de scheme, măsurat pe benchmark-urile SPECint '95,
acurateţea predicţiei salturilor indirecte creşte şi, ca urmare, câştigul global
asupra timpului de execuţie este de cca. 4.3% - 9%.
O altă idee în predicţia branch-urilor, din păcate mai puţin mediatizată şi
înţeleasă, în opinia autorului acestei cărți, a fost lansată în 1996 de către Dr.
Trevor Mudge şi constă în predicţia pe bază de lanţuri stohastice Markov,
utilizând algoritmul PPM (Prediction by Partial Matching), folosit pe scară
largă de altfel şi în procesarea (compresia) de imagini, recunoaşterea automată a

181
vorbirii (speech recognition) etc. Un predictor Markov de ordinul k
predicţionează bitul următor al unei secvențe binare pe baza celor k biţi
precedenţi. Dintr-un punct de vedere mai general, predicția markoviană este
descrisă în continuare.

Principiul predicției bazate pe un proces Markov

Să considerăm un proces discret cu N stări, notate S1, S2,…,SN. La un


moment dat (t), procesul tranzitează din starea curentă Si(t), în starea următoare
Sj(t+1), conform matricii de tranziție având probabilitățile aij, i,j∈ {1,2,...,N}.
Într-un proces Markov de ordinul k, k ∈ {1,2,...,N}, starea următoare depinde de
anterioarele k stări. Cu alte cuvinte, putem scrie sintetic că există următoarele
probabilități condiționate:

P[Sj(t+1) | Si(t), Sl(t-1),…,Sm(t-k+1)], unde i, j, l, m∈ {1,2,…,N}.

Pentru un predictor Markov de ordinul 1, probabilitățile condiționate


aferente sunt următoarele:

ai,j= P[Sj(t) | Si(t-1)], 1≤i,j≤N;


Notații: p(Si)=pi, p = p1 , p 2 ,..., p N și A = aij , i, j ∈ 1, N

Se pot scrie următoarele identități:

p = p ⋅ A (matricial) şi
N

∑a
j =1
ij = 1 (Relația de echilibru sau de normalizare)

Considerând un proces Markov de ordinul întâi, aflat în starea curentă


S i (t ) , următoarea stare prezisă de acesta este S l (t + 1) dacă Max{ai , j } t = ai ,l ,
unde i, j, l ∈ {1,2,..., N }.
De remarcat faptul că probabilitățile aij vor fi modificate după fiecare
ciclu, conform tranziției reale. Astfel, dacă predicția corectă a fost ( S l ) , atunci
ai,l va crește, iar toate celelalte probabilități de tranziție vor descrește
corespunzător; în caz contrar (starea următoare va fi S m , m≠l) ai ,m va crește, iar

182
celelalte probabilități de tranziție vor descrește corespunzător [Vin07]. Așadar
un asemenea model predictiv este unul adaptiv, care învață atât din succese (spre
deosebire de perceptron – v. în continuare!) cât și din eșecuri.
În esenţă, pentru predicţia branch-urilor, se caută pattern-ul memorat în
registrul de istorie globală HRg, pe k biţi, într-un şir binar mai lung decât k, al
istoriei salturilor anterioare. Dacă acest pattern este găsit în şirul respectiv cel
puţin o dată, predicţia se va face corespunzător, pe baza unei statistici care
determină de câte ori acest pattern a fost urmat în şir de 0 logic (not taken) şi
respectiv de câte ori a fost urmat de 1 logic (taken), pe baza argumentului
maxim. Dacă însă pattern-ul din HRg nu a fost găsit în şirul de istorie, se
construieşte un nou pattern, mai scurt, prin eludarea ultimului bit din HRg şi
algoritmul se reia, prin căutarea acestui nou pattern, ş.a.m.d., până la găsirea
unui anumit pattern în şirul de istorie binară. Acesta este de fapt algoritmul
numit Prediction by Partial Matching (PPM). Evident că algoritmul PPM poate
fi îmbunătățit, prin implementarea unui prag (threshold). Astfel, predicția se va
face numai dacă argumentul maxim depășește valoarea acestui prag prestabilit.
Se arată că deşi complexitatea implementării acestei noi scheme în hardware
creşte de cca. două ori faţă de o schemă corelată, eficienţa sa - la acelaşi buget al
implementării - este clar superioară. Nu sunt însă de acord cu autorii, care, fără
să o demonstreze, susţin că acest predictor reprezintă limita superioară a
predictibilităţii ramificaţiilor.
În fine, o altă soluţie, mai agresivă decât cele precedente, constă în aducerea
instrucţiunilor din cadrul ambelor ramuri ale branch-ului, în structuri pipeline
paralele de procesare a instrucțiunilor (multiple instructions streams). Când
condiţia de salt este ulterior determinată, una din ramuri se va abandona. Totuşi,
necesitatea predicţiei apare şi în acest caz, datorită faptului că în anumite cazuri
(salturi indirecte, reveniri din subrutine etc.), adresa de salt este necunoscută la
finele fazei IF şi deci ar trebui predicţionată în vederea procesării eficiente.
Chiar și în cazul unor salturi în mod de adresare direct, adresa țintă trebuie
memorată într-o structură de date implementată în hardware, pentru a putea fi
anticipată încă din faza de IF a acestui salt. Apoi, chiar cunoscute ambele
posibile adrese, apare dificultatea adresării memoriei şi aducerii blocurilor de
instrucţiuni de la aceste două adrese distincte, în mod simultan, din I-Cache. O
altă dificultate majoră constă în posibilitatea ca pe cele două ramuri de program
procesate simultan să se găsească alte instrucțiuni de ramficație. Desigur că în
acest caz sunt necesare redundanţe ale resurselor hardware (cache-uri, unităţi de
execuţie, busuri, logică de control etc.) precum şi complicaţii în logica de

183
control. Dacă pe o ramură a programului există, spre exemplu, o instrucţiune de
tip STORE, procesarea acestei ramuri trebuie oprită, întrucât există posibilitatea
unei alterări ireparabile a unei locaţii de memorie. Această soluţie implică
creşteri serioase ale costurilor, dar se pare că ar fi singura capabilă să se apropie
oricât de mult faţă de idealul predicţiei absolut corecte. În cazul
microprocesoarelor, aceste mecanisme de prefetch al ambelor ramuri, nu se
aplică în prezent, în principal datorită lărgimii de bandă limitate între
microprocesor şi memorie. Tehnica s-a întâlnit în cazul supercomputerelor
anilor '90 (ex. IBM-3033).
Aceste tehnici de predicţie hardware a branch-urilor, datorită complexităţii
lor, nu sunt implementate în mod uzual în microprocesoarele RISC scalare
simple (de tip microcontroler), întrucât se preferă tehnicile software de
"umplere" a BDS-ului (limitat în general la o instrucţiune) cu instrucţiuni utile,
în general anterioare celei de salt, lucru posibil în cca. 70%-80% din cazuri. În
schimb, predicţia hardware este implementată în cazul unor procesoare
superscalare, unde datorită BDS-ului de câteva instrucţiuni, umplerea lui cu
instrucţiuni anterioare independente devine, practic, imposibilă.
În [Vin01] am dezvoltat o abordare alternativă, care, în principiu, nu ar
mai avea nevoie de predicția branch-urilor. S-a propus, în locul predicției
branch-urilor, calcularea în avans a condiției și adresei de salt (de îndată ce
valorile operanzilor din condiția acestuia devin disponible), astfel încât în
momentul în care acesta se va aduce din memorie, condiția și adresa de salt să
fie deja disponibile într-o structură de date, implementate în hardware (SDB –
Structură de Date pentru Branch-uri). Pentru exemplificarea ideii, se consideră
secvența de instrucțiuni (MIPS – I):

ADD R9, R5, R7; //R9<-(R5) + (R7)


...
BNE R9, R8, offset; //if (R9!=R8) PC<-(PC) + offset

Instrucțiunea ADD ar putea modifica în avans valoarea lui R9 din SDB,


de îndată ce această valoare va fi înscrisă în registru. După această modificare
efectuată de instrucțiunea ADD, algoritmul SDB va re-estima valoarea condiției
de salt a branch-ului BNE R9, R8, offset, astfel încât, în momentul în care acesta
se va aduce din memorie, SDB să furnizeze în mod anticipat toate datele
necesare execuției sale (valoarea condiției de salt, a adresei țintă), fără întârzieri.
Problema majoră constă în faptul că, în general, distanța între instrucțiunea care

184
modifică valoarea condiției de salt (ADD în cazul exemplificat aici) și
instrucțiunea de salt condiționat este prea mică. Astfel, pre-calcularea adesea nu
este încheiată înainte de aducerea instrucțiuniii de branch din memorie. Detalii
asupra ideii de pre-calculare a condiției de salt sunt prezentate în articolul
VINȚAN L., SBERA M., FLOREA A. – Pre-computed Branch Prediction, Acta
Universitatis Cibiniensis, Technical Series. Computer Science and Automatic
Control, pg. 91-100, vol. XLIII, ISSN 1221-4949, Editura Universității “L.
Blaga” din Sibiu, 2001 [Vin01].

Fundamentele predicției neuronale a branch-urilor

În continuare se prezintă bazele teoretice și practice ale predicției


neuronale a instrucțiunilor de salt condiționat (branch), implementabile în
cadrul unor microarhitecturi pipeline superscalare de procesare a instrucțiunilor
(microprocesoare superscalare), pe baza articolului autorului: L. N. VINŢAN -
Dynamic Neural Branch Prediction Fundamentals, Buletinul AGIR, an XXI,
no. 1, pp. 64-71, ISSN-L 1224-7928, Editura AGIR, București, ianuarie –
martie, 2016, cu minore modificări și adaptări. Utilizarea unor metode ale
inteligenței artificiale și învățării automate în proiectarea microprocesoarelor
moderne poate contribui semnificativ la creșterea performanțelor acestora,
printr-un comportament inteligent, senzitiv la contextul dinamic de procesare.
Ideea de predictor neuronal de branch-uri, publicată de autor pentru prima dată
într-o lucrare publicată în anul 1999 (cu 57 de citări independente până în anul
2015), a contribuit la această tendință actuală, importantă, în arhitectura
sistemelor de calcul. Adaptarea unor asemenea metode teoretice relativ rafinate
la restricțiile implementărilor hardware constituie o adevărată artă, care
combină cunoștințele teoretice și practice, cu intuiția și talentul cercetătorului.
După cum am mai precizat în această lucrare, modelul generic de
predictor de branch-uri - utilizat în microprocesoarele actuale - este prezentat în
Figura 3.37.1. și este cunoscut în literatura de specialitate sub denumirea de
Two-Level Adaptive Branch Prediction (TLABP). A fost dezvoltat, în principal,
de grupul profesorului Yale Patt, de la Universitatea din Michigan, SUA (în
anul 1992). PCL reprezintă biții mai puțin semnificativi ai registrului PC, GHR
– informația de corelație (istorie) globală, LHR - informația de istorie locală a
branch-ului curent, FSM (Finite State Machine) – predictorul propriu zis,
implementat sub forma unui automat finit, PHT (Prediction History Table) –
tabela istoriei predicțiilor, PT (Prediction Table) – tabela de predictoare (FSM-

185
uri). După ce instrucțiunea curentă de branch a fost executată, structurile GHR,
LHR și FSM se actualizează corespunzător compartamentului instrucțiunii
(Taken/Not Taken). Complexitatea unei asemenea structuri predictive este
exponențială, implicând tabele de 2n respectiv 2m intrări, unde n și m sunt
lungimile indecșilor de adresare în cele două tabele (PHT și PT). Pentru
reducerea acestei complexități, uneori conținutul regiștrilor GHR și LHR este
comprimat prin funcții de tip SAU EXCLUSIV (hashing).

Figura 3.37.1. Predictor adaptiv, generic, pe două niveluri

Predicția dinamică a instrucțiunilor de branch (salt condiționat) prin


metode neuronale, ca o alternativă la modelele clasice, de tip Two Level
Adaptive Branch Prediction, a fost inițiată chiar de autorul acestei cărți în
lucrarea sa - Vinţan L., Towards a High Performance Neural Branch Predictor,
International Joint Conference on Neural Networks, Washington DC, USA, 10-
16 July, 1999. Ulterior, ideea originară a fost continuată într-un mod mai amplu
și în lucrarea [Ega03]. În acest articol (1999) se arată că metode neuronale de
învățare supervizată, precum Learning Vector Quantization (datorată lui T.
Kohonen) sau Back-Propagation în structuri neuronale de tipul Multi-Layer
Perceptron, pot fi deosebit de eficiente în predicția dinamică a instrucțiunilor
de salt condiționat. S-a demonstrat că utilizarea rețelelor neuronale în procesele
predictive pot identifica noi corelații, care pot fi exploatate de predictor. Un
avantaj important al predicției neuronale față de cea clasică (TLABP) constă în
faptul că se pot exploata istorii globale/locale (corelații) mai adânci, la

186
complexități liniare și nu exponențiale. În plus, învățarea supervizată a rețelelor
neuronale este una care garantează matematic convergența, după cum se va
arăta în continuare, spre deosebire de învățarea predictorului FSM, care este
una empirică (bazată, în fond, pe bun simț – common sense). Spre exemplu, în
cazul predictorului FSM nu există o demonstrație matematică care să justifice
optimalitatea tranziției din starea “Taken slab” (a 2-a stare T), în starea „Not
Taken slab” (a 2-a stare NT), în cazul în care branch-ul s-a dovedit a fi Not
Taken (tranziție nt - not taken). Cineva ar putea întreba de ce nu se tranzitează
în starea „Not Taken tare” în acest caz. Justificarea ar putea fi doar de natură
empiric-statistică, bazată pe benchmarking.
Ideea inițială a predicției neuronale a branch-urilor a fost ulterior
dezvoltată în mod semnificativ de mulți alți cercetători, ajungând să fie
implementată de compania INTEL într-un simulator al procesorului Itanium.
Ideea predicției neuronale a fost ulterior (2016/2017) implementată in
microprocesoarele comerciale Samsung Exynos M1 Processor (quadcore, ISA
ARM v8.0, 64/32 bits) – v.
https://translate.google.ro/translate?hl=ro&sl=ru&tl=en&u=https%3A%2F%2F
geektimes.ru%2Fpost%2F279738%2F (citare L. Vintan), AMD Ryzen (8 cores,
16-thread chip) – v. http://www.amd.com/en-us/press-releases/Pages/amd-
takes-computing-2016dec13.aspx. Se remarcă, în special, contribuțiile
importante în acest sens ale lui Dr. Daniel Jimenez (SUA) și Dr. Andre Seznec
(Franța), care au demonstrat că implementarea în microprocesoarele moderne
ale unor predictoare de branch-uri de tip perceptron este fezabilă. Cel mai
simplu perceptron (un neuron artificial) este prezentat în figura următoare.

X0=1 w0
X1 w1
r
Σ
w2 O (x ) Y
X2

wn
Xn
Figura 3.37.2. Structura unui perceptron simplu

X = {x0 , x1 ,..., xn }, reprezintă vectorul de intrare.

W = {w0 , w1 ,..., wn }, reprezintă vectorul ponderilor atașate sinapselor (legăturilor


între intrări și celula de tip neuron).

187
n
O( X ) = W ⋅ X = w0 + ∑ wk ⋅ xk , reprezintă ieșirea neuronului artificial (hiper-planul
k =1

de decizie este definit de ecuația W ⋅ X = 0 ).


+ 1 if O( X ) > 0
Y = , reprezintă semnul ieșirii perceptronului.
− 1 if O( X ) ≤ 0

În mod frecvent, pentru a obține valoarea ieșirii Y se folosește o funcție


1
de activare sigmoidală, precum Y= −
є [0,1] sau variații, precum Y=
−O ( X )
1+ e

1 − e −O ( X )

є [-1,1]. Perceptronul poate fi folosit ca un clasificator/predictor binar
1 + e −O ( X )
(branch Taken = +1 sau branch Not Taken = -1). Desigur că acest perceptron
simplu poate să clasifice corect doar exemple (intrări) separabile liniar printr-un
hiper-plan, în spațiul de reprezentare ( X ). Spre exemplu, o funcție logică de tip
SAU EXCLUSIV (XOR) nu poate fi reprezentată de acest perceptron simplu.
În continuare, schițăm o demonstrație elementară a acestui fapt, considerând un
perceptron cu trei intrări (X0=1, bias), cu ieșirea O(X1,X2) = W0 + W1*X1 +
− −
W2*X2 = W * X , unde X1, X2 sunt intrările, iar W0, W1 și W2 sunt ponderile
respective, care aparțin mulțimii numerelor reale (R). Considerând funcția
logică Y=X1 XOR X2, tabelul următor arată această imposibilitate.

X1 X2 Y=X1 XOR X2
0 0 0=W0+W1*0+W2*0,
deci W0=0
0 1 1=0+W1*0+W2*1, deci
W2=1
1 0 1=0+W1*1+1*0, deci
W1=1
1 1 0=0+1*1+1*1 0=2
(contradicție!)

Așadar, un perceptron simplu nu poate reprezenta o funcție logică de tip


XOR sau, mai general, o funcție inseparabilă printr-un hiper-plan (ieșirile
− −
pozitive de cele negative), pentru că nu există un hiper-plan W * X = 0 care să
poată separa exemplele pozitive de cele negative, în spațiul de reprezentare al
acestora. Particularizând la exemplul considerat, nu există un plan în spațiul

188
euclidian 3-dimensional, dat de ecuația W0+W1*X1+W2*X2 = O(X1, X2),
care să poată separa ieșirile pozitive de cele negative ale funcției XOR.
Problema principală constă în elaborarea unui algoritm de învățare supervizată
pentru perceptronul simplu, care să poată învăța corect o mulțime de exemple
liniar separabile (pozitive și negative), notată cu D. Pentru fiecare exemplu
d ∈ D este necesar să-i cunoaștem ieșirea corespunzătoare, notată cu td. Dacă
n
Od = w0 + ∑ wk x dk este ieșirea reală, o măsură convenabilă a erorii de învățare ar
k =1

1
putea fi eroarea globală medie pătratică E ( w ) = ∑ (t d − Od ) 2 >0 (constanta ½
2 d∈D
este ne-esențială, fiind introdusă exclusiv din motive de eleganță a calculului,
după cum se va putea remarca în continuare). Reprezentarea grafică a acestei
funcții E (w ) este o hiper-suprafață cuadratică (parabolică mai precis), cu un
r
singur minimum (evident global). Desigur că vectorul de ponderi w pentru care
se obține acest minimum global va clasifica cel mai bine un anumit exemplu
X dk , k=0,1,..,n. Vom considera acum un caz particular simplu, în care setul de
antrenament D conține doar două exemple, unul pozitiv (x1) iar altul negativ
(x2). Notăm ieșirile corespunzătoare cu t1 și respectiv t2. În acest caz se poate
scrie:

O(x1) = w0 + w1x1 and O(x2) = w0 + w1x2. Funcția de eroare corespunzătoare (E)


este:

E: R2 R, E(w0,w1) = 0.5[(t1 – w0 – w1x1)2 + (t2 – w0 – w1x2)2].

Se arată, prin calcul elementar, că valoarea minimă a acestei funcții este:

Emin = 0.25[t1 – t2 + w1(x2 – x1)]2,

care se obține dacă identitatea 2w0 + w1(x1+ x2) = t1 + t2 este îndeplinită.


Concret, dacă se consideră exemplul pozitiv ca fiind (x1, t1) = (1, 1), iar cel
negativ ca fiind (x2, t2) = (-1,-1), rezultă următoarea expresie a funcției de
eroare:

189
E(w0,w1) = 0.5[(1 – w0 – w1)2 + (–1 – w0 + w1)2] = w02+ (w1-1)2.

În acest caz, minimul global unic se obține pentru w0 = 0 și w1 = 1 și este


egal cu Emin = 0. De fapt, ecuația precedentă reprezintă un paraboloid eliptic,
având ecuația echivalentă z = x2 + y2, unde z = E(w0,w1), x= w0 și y= w1-1. (O
x2 y2 z
formă mai generală pentru paraboloid este dată de expresia 2 + 2 = ;
a b c
substituind în această expresie x cu mx+n, y cu py+q și z cu rz+s, se poate
obține o formă și mai generală). Utilizând programul Wolfram Alpha am
obținut pentru această funcție reprezentarea grafică din Figura 3.37.3. (cu
notațiile w0 = x și w1 = y).

Figura 3.37.3. Reprezentare 3D a funcției de eroare

Gradientul erorii E (w ) se scrie:

190
 ∂E ∂E ∂E  n ∂E
∇E ( w ) =  , ,..., =∑ i k , unde ik sunt versorii celor n axe
 ∂w0 ∂w1 ∂wn  k =0 ∂wk
ortogonale. Evident că produsul scalar între oricare doi versori este 0. Este
cunoscut că − ∇E (w ) reprezintă direcția și sensul de descreștere locală
(infinitezimală) a funcției de eroare, în punctul considerat. Acest fapt este
justificat de semnificația derivatei unei funcții de variabilă reală, f(x). Vectorul
f’(x) i arată sensul de creștere al funcției f(x), pe axa OX. Justificarea este
imediată, având în vedere definiția derivatei într-un punct x0, anume
f ( x) − f ( x0 )
lim . Așadar derivata funcției f în punctul x0 înmulțită cu versorul i
x − > x0 x − x0
de pe axa OX este un vector, care arată sensul de creștere - pe axa OX - al
funcției f, în punctul x0. Este clar că gradientul, care este o generalizare a
derivatei într-un spațiu ortogonal n-dimensional, arată sensul de creștere al
funcției respective. Rezultă că o regulă rațională de învățare ar putea fi una de
tipul W ← W + ∆W , unde ∆W = −α∇E (W ) , α = pasul de învățare (în general, un
număr pozitiv subunitar). Această regulă este echivalentă cu setul de (n+1)
formule de mai jos, anume:

∂E
wk ← wk − α , (∀)k = 0,1,..., n
∂wk

Cu alte cuvinte, printr-o astfel de actualizare a ponderilor, fiecare valoare


a ponderii ( wk ) este modificată pe axa proprie așa încât această modificare să
conducă la descreșterea valorii funcției de eroare. Procesul de învățare se va
repeta în mod iterativ, până în momentul în care se atinge minimumul funcției
de eroare. Continuând calculele rezultă:

∂E ∂ 1 2 ∂ (t − W ⋅ X )
=  ∑ (t d − Od )  = ∑ (t d − Od ) d = − ∑ xdk ⋅ (t d − Od )
∂wk ∂wk  2 d∈D  d∈D ∂wk d ∈D

În final, regula de învățare supervizată se poate exprima sub forma:

191
wk ← wk + α ∑ (t d − Od ) xdk , (∀)k = 0,1,..., n ; este numită regula gradientului
d ∈D

descendent sau regula delta. Uneori, în loc să fie considerat un număr pozitiv
subunitar, α este considerat sub forma unei funcții pozitive descrescătoare în
timp (odată cu numărul de iterații), α(t). Bazat pe aceste calcule simple, rezultă
exprimat în pseudo-cod algoritmul următor [Mit97]:

Algoritmul gradientului descendent


Initialize each Wk to (pseudo)random values ∈ − ,  (recomandare)
2 2
 n n
r
Until E ( w) < T (threshold ), DO:
Initialize each ∆Wk = 0
For each learning pair (xd, td), from training examples, DO:
Compute the corresponding output Od
For each Wk, DO:
∆wk ← ∆wk + α (t d − Od ) x dk
For each wk, DO:
wk = wk + ∆wk

În continuare, în loc să considerăm suma erorilor pentru toate exemplele


din setul de antrenament, putem considera câte o eroare locală distinctă, pentru
fiecare exemplu, anume E d ( w) = (t d − Od )2 . Determinând perceptronul să
r 1
2
învețe setul de exemple într-o ordine cvasi-aleatoare și minimizând fiecare
eroare locală, se obține o aproximare rezonabilă a algoritmului de învățare
precedent. Desigur, acest algoritm nu mai are rigoarea celui precedent, care
minimiza eroarea globală și nu fiecare eroare locală în parte, ca acesta
(minimizarea fiecărei erori locale la un moment dat, nu garantează minimizarea
erorii globale la finalul procesului de învățare). În schimb, timpul de învățare
poate fi mai scurt pentru acest algoritm. Se obține astfel algoritmul următor
[Mit97]:

Algoritmul stohastic de învățare de tip gradient descendent

Initialize each wk randomly to − ,+ 


2 2
(recomandare)
 n n
r
Until the termination condition is met ( E d ( w) < T or Od > T , etc.) , DO:

192
For each learning pair (xd, td), DO:
Compute the output Od
For each wk, do:
wk ← wk + α (t d − Od ) x dk

Deoarece sumează erorile pentru toate exemplele, algoritmul de gradient


descendent standard este mare consumator de timp, dar este deseori utilizat cu
valori mai mari ale pasului de învățare decât varianta sa stohastică. Dacă,
pentru structuri de perceptroane mai complexe (cu multiple celule) E(W ) are
multiple minime locale, algoritmul stohastic ar putea evita eșuarea într-un
asemenea minimum, întrucât folosește multiple gradiente locale ∇E d (W ) pentru
a ghida căutarea. Dacă se consideră ieșirea perceptronului ca fiind
O ( X ) = sgn(W ⋅ X ) în loc de O ( X ) = W ⋅ X , precedenta regulă de învățare devine:

wk ← wk + α (t − o )xk , (∀)k = 0,1,..., n

Dacă exemplul de antrenare este corect clasificat (t=o), nu se face nicio


actualizare a ponderilor, semnificând faptul că perceptronul simplu nu învață
din succese (spre deosebire de predictorul FSM). Să considerăm acum o= -1 și t
= +1. În acest caz toate ponderile wk având xk pozitiv (ca și t) sunt incrementate,
iar celelalte ponderi sunt decrementate. În mod similar, dacă o = +1 și t = -1,
toate wk având xk negativ (exact ca semnul lui t) vor fi incrementate, iar
celelalte ponderi vor fi decrementate. Așadar, regula de învățare este una
simplă și intuitivă.
Să trecem acum la predictorul de branch-uri de tip perceptron simplu.
Regula de predicție este foarte simplă, consecință a celor deja prezentate. Dacă
valoarea ieșirii sale este pozitivă, branch-ul este predicționat ca Taken, altfel, ca
Not Taken. Acest proces predictiv se face în cadrul fazei de feed-forward, iar
învățarea se face în faza backward. Referitor la algoritmul de învățare al
predictorului, regula este una intuitivă. Dacă sgn t = sgn x k atunci wk este
incrementată; altfel, este decrementată. Bazat pe această observație cercetătorul
american D. Jimenez a propus următorul algoritm de învățare:

Algoritmul de învățare simplificat al predictorului

193
If sign(o) ≠ t or |O|<T
for k=0 to n do
wk = wk + txk (incrementare dacă sgn t=sgn xk)
(decrementare, în caz contrar)
endfor
endif

Pentru a se facilita implementarea algoritmului în hardware, s-a propus


varianta următoare:

Algoritmul de învățare implementat în hardware


For each bit do in parallel (paralelismul hardware-ului este natural!)
if t=xk then
wk = wk + 1
else
wk = w k – 1
end if

În cazul predictorului de branch-uri de tip perceptron vectorul de intrare


X reprezintă vectorul uzual de intrare, exact ca și în schemele clasice de
predicție de tip TLABP. Cum t și xk pot lua valorile +1 sau -1, ponderea wk este
incrementată când t și xk au același semn (corelație pozitivă), respectiv este
decrementată în caz contrar (corelație negativă). Figura 3.37.4. prezintă întregul
proces de predicție și învățare. Pașii de procesare sunt următorii:

1. Biții mai puțin semnificativi ai registrului PC indexează, pe timpul fazei


de aducere a instrucțiunii din memorie de către microprocesor, tabela de
predicție, care conține vectorii de ponderi aferenți instrucțiunilor de branch.
2. Vectorul de ponderi asociat instrucțiunii de branch aduse este adus din
tabela de predicție în registrul P.
3. Se calculează ieșirea O, ca produs scalar între vectorul P și biții
registrului de istorie locală/globală (informația de corelație a branch-ului).
4. Se startează procesul de predicție a branchului (Taken dacă O > 0, Not
Taken dacă O ≤ 0).
5. După ce se află comportamentul real al acestui branch (Taken / Not
Taken), ponderile din registrul P se actualizeză corespunzător
(incrementare/decrementare).

194
6. Registrul P actualizat se scrie în locația corespunzătoare din tabela de
predicție (PT).

PC
PT

Table
index of
perceptrons

Branch Outcome
Update
P Weights

History Reg. Compute Training


O

Sign O

Prediction
Figura 3.37.4. Schema bloc a predictorului perceptron

Dr. D. Jimenez a propus câteva simplificări ingenioase în vederea


adaptării algoritmului de predicție / învățare la restricțiile hardware, precum și
în vederea reducerii timing-ului. Câteva dintre aceste simplificări sunt
următoarele:

• Ponderile perceptronului sunt reprezentate în cod binar complementar


față de doi, ca octeți întregi cu semn.
• Cum informația de istorie este codificată utilizând valorile 1 și -1, în
calculul ieșirii perceptronului se evită înmulțirea. Ieșirea se calculează practic
ca suma unor octeți cu semn, reprezentați în cod complementar.
• Reprezentarea unui octet negativ în complement față de 2, se
aproximează prin reprezentarea acestuia în complement față de 1 (octetul -wk se
aproximează în suma respectivă cu wk negat). Ca și consecință, complexitatea
hardware a predictorului este liniară și nu exponențială, ca în cazul schemelor
clasice de tip TLABP. Cu alte cuvinte, dacă istoria crește cu un bit, este nevoie
doar de un sumator binar în plus, operând la nivelul a doi octeți.

195
• Sumatoarele binare utilizate pentru predicție au timpul de sumare
suficient de mic pentru a îndeplini cerințele de timp real.

Se arată că acuratețea de predicție crește semnificativ odată cu creșterea


contextului de istorie de la 16 la 66 de biți. O istorie de 66 de biți ar fi imposibil
de utilizat în cazul schemelor TLABP, din cauza capacităților prohibite ale
tabelelor de predicție (266 intrări!) S-a arătat, pe baza unor laborioase simulări
în software, că aceste predictoare de tip perceptron obțin acurateți de predicție
superioare predictoarelor clasice, chiar dacă la o latență ceva mai ridicată a
predicției.
Evident că nu există un predictor ideal. Predictoarele neuronale și cele de
tip TLABP sunt complementare. În general, cele neuronale obțin acurateți mai
bune ale predicțiilor, dar au un timing mai ridicat.
Perceptronul predicționează cu mare acuratețe branch-urile liniar
separabile. Notăm cu Hn cei mai recenți n biți de context ai unui anumit branch
(istorie globală, locală etc.) Pentru un branch static B, există o funcție booleană
fB(Hn) care clasifică cel mai bine comportamentul saltului condiționat B. Dacă
funcția fB(Hn) este separabilă liniar (printr-un hiperplan), atunci și saltul B este
liniar separabil. Din punct de vedere teoretic, schemele de tipul Two Level
Adaptive Branch Predictors pot învăța și branch-uri care nu sunt separabile
liniar, pentru că fiecare pattern al contextului Hn indexează propriul predictor
FSM. Jimenez a arătat că cele mai dificil predictibile branch-uri sunt cele care
nu sunt liniar separabile. Performanța predictorului crește dacă istoria lungimii
crește, chiar și în cazul branch-urilor care nu sunt separabile liniar.
În cadrul aceleiași clase de predictoare există diverse instanțe, unele mai
bune în anumite situații, altele superioare în alte situații ale procesării. Prin
urmare, ideea predicției hibride apare ca naturală. În esență, aceasta înseamnă
ca mai multe predictoare de diverse tipuri, complementare din punct de vedere
al performanțelor implicate, să lucreze împreună. Desigur, acestea vor fi
dirijate de un meta-predictor (MP), care va decide, în fiecare moment, în care
dintre predictoarele componente să aibă încredere. Această decizie se poate
baza pe informația de confidență (încredere), atașată fiecărui predictor. Această
informație (Confid1 și Confid2) poate fi reprezentată ca un număr binar pe k
biți, care reprezintă comportamentul predictorului respectiv, pe parcursul
ultimelor sale k instanțe de procesare (1 – predicție corectă, 0 – predicție
incorectă). Desigur că metapredictorul poate fi neadaptiv (de gen voter) sau

196
adaptiv (care învață). Schemele următoare arată modul de implementare a unor
asemenea abordări hibride.

Figura 3.37.5. Predictor hibrid (neuronal - NBP și TLABP)

Figura 3.37.6. Schemă generică a unui meta-predictor

197
3.4.5. PROBLEMA EXCEPŢIILOR ÎN PROCESOARELE RISC

La sesizarea unui eveniment de excepţie (întrerupere externă sau


derută/deviere internă) se vor inhiba toate procesele de scriere, atât pentru
instrucţiunea care a provocat excepţia, cât şi pentru următoarele aflate în bandă
(pipeline). Această acțiune previne orice modificare a contextului procesorului
care ar putea fi cauzată de continuarea procesării speculative a acestor
instrucţiuni. În principiu, după terminarea procesării instrucţiunii anterioare celei
care a provocat excepţia, se intră în protocolul de tratare, în cadrul căruia se
salvează intern (într-un registru special dedicat al CPU) sau extern (memoria
stivă), PC-ul instrucţiunii care a provocat excepţia, precum şi contextul
procesorului (spre exemplu, registrul de stare). În particular, în cazul în care
instrucţiunea care a provocat excepţia se află într-un BDS de ordinul n şi saltul
se face, atunci trebuie reluate cele n instrucţiuni BDS, precum şi instrucţiunea la
care se face saltul. În acest caz trebuie salvate (n + 1) PC-uri pentru că în general
adresele instrucţiunilor din BDS şi respectiv adresa instrucţiunii la care se face
saltul, nu sunt contigue.
Dacă în cazul unei excepţii structura pipeline poate fi oprită, astfel încât
instrucţiunile anterioare celei care a provocat excepţia să poată fi complet
executate şi respectiva instrucţiune, împreună cu cele ulterioare ei, să poată fi
reluate în condiţii deterministe, se zice că avem o excepţie precisă. În caz
contrar, excepţia se numește imprecisă. Mai jos, se prezintă un exemplu de
posibilă excepţie imprecisă:
DIVF F0, F2, F4
ADDF F6, F6, F8
SUBF F10, F10, F14
În acest caz instrucţiunile se vor termina Out of Order, adică ADDF şi
SUBF se vor termina înaintea instrucţiunii DIVF, fiind independente de aceasta
(înmulțirile și împărțirile sunt mari consumatoare de timp întrucât, necesită mai
multe deplasări la stânga respectiv dreapta și adunări respectiv scăderi, pentru
numere întregi - v. spre ex. cursul disponibil online la
https://www.cs.utah.edu/~rajeev/cs3810/slides/3810-08.pdf sau lucrarea
autorului disponibilă online la
https://www.researchgate.net/publication/284492951_Using_a_Multibit_Techni
que_in_Multiply_Algorithm). Să presupunem că instrucţiunea DIVF a
determinat o derută aritmetică (spre exemplu o împărțire la zero) într-un
moment în care ADDF şi SUBF s-au încheiat. Această situaţie implică o

198
excepţie imprecisă, întrucât reluarea instrucţiunii DIVF se va face cu conţinutul
regiştrilor F6 şi F10 alterat. Mai mult, pot exista intrucțiuni anterioare celei de
împărțire, a căror procesare, de asemenea, nu s-a încheiat. Salvarea în stivă a
registrelor CPU nu are sens, într-un asemenea caz. Aceste situaţii sunt evident
nedorite, iar dacă apar, trebuie eliminate. Relativ la excepţiile imprecise, în
literatură se precizează următoarele posibilităţi principale de soluţionare:
a) Contextul CPU în care scrierile rezultatelor instrucțiunilor se fac out of
order (un fel de “ciornă” a CPU, care va fi tratată ulterior sub denumirea de
buffer de reordonare) să fie dublat printr-un aşa-numit "history-file" (spre
exemplu, setul de regiștri logici), care să păstreze toate resursele modelului de
programare. În acest "history-file" se înscriu noile rezultate, la finele terminării
"normale" (pur secvenţiale, in order) a instrucţiunilor (faza commit). În cazul
apariţiei unei excepţii imprecise, contextul procesorului se va încărca din acest
context de rezervă (ex. CYBER 180 / 990). Există şi alte variaţiuni pe această
idee.
b) Prin această a 2-a soluţie de principiu, nu se permite terminarea unei
instrucţiuni în bandă (pipeline), până când toate instrucţiunile anterioare (din
punct de vedere al ordinii secvențiale) nu se vor fi terminat, fără să cauzeze o
excepţie. Astfel, se garantează că dacă a apărut o excepţie în cadrul unei
instrucţiuni, nicio instrucţiune ulterioară acesteia nu s-a încheiat şi, totodată,
instrucţiunile anterioare ei s-au încheiat normal. Soluţia implică întârzieri ale
procesării (spre exemplu, microprocesoarele MIPS R 2000 / 3000).
O altă problemă o constituie excepţiile simultane. Dacă luăm în
considerare o procesare pe 5 niveluri, în cadrul fiecărui nivel pot apărea
următoarele excepţii:
IF - derută accesare pagină memorie, acces la un cuvânt nealiniat etc.
RD - cod ilegal de instrucţiune
EX - diverse derute aritmetice (overflow, împărțire la zero etc.)
MEM - ca şi la IF
WB – spre exemplu, acces la resurse privilegiate în modul de lucru user.
Rezultă imediat posibilitatea apariţiei simultane a două sau mai multe
evenimente de excepţie. Să considerăm spre exemplificare secvenţa de
instrucţiuni din Figura 3.38, în cadrul căreia apar simultan două excepţii:

199
Figura 3.38. Excepţie simultană

Soluţia ar consta în tratarea prioritară a derutei instrucţiunii LOAD, după


care se va relua această instrucţiune. Apoi, va apărea deruta de depăşire aferentă
instrucţiunii ADD, care va fi şi ea tratată.

Figura 3.39. Excepţii în ordine inversă

Un caz mai dificil este acela în care excepţiile apar Out of Order, ca în
exemplul din Figura 3.39. În acest caz ar fi posibile două soluţii de principiu:
1) Să existe un flag (indicator) de stare excepţie, aferent fiecărei instrucţiuni
şi care să fie testat la intrarea în nivelul WB. Dacă există setată vreo excepţie, se
va trece la protocolul de tratare. Astfel, se garantează că toate excepţiile din
cadrul unei anumite instrucţiuni vor fi văzute înaintea excepţiilor apărute pe
parcursul unei instrucţiuni următoare.
2) Se bazează pe tratarea excepţiei de îndată ce aceasta a apărut.
La sesizarea derutei din cadrul instrucţiunii (i + 1) se vor inhiba
instrucţiunile (i - 2), (i - 1), i, (i + 1) şi prin protocolul de tratare se va relua
instrucţiunea (i - 2). Apoi se va sesiza deruta din cadrul instrucţiunii i, urmând
ca după tratarea ei instrucţiunea i să se reia. Evident că deruta aferentă nivelului
IF din cadrul instrucţiunii (i + 1) a fost anterior eliminată şi deci nu va mai
apărea. Menţionăm că, în general, majoritatea microprocesoarelor RISC
avansate deţin suficiente resurse hardware interne care să le permită, în cazul
apariţiei unei excepţii, salvarea internă a contextului CPU. Evident că limitarea
resurselor interne nu implică limitarea posibilităţii de imbricare a excepţiilor. În
asemenea cazuri se va folosi memoria stivă. Ca şi procesoarele CISC,

200
procesoarele RISC deţin regiştri de stare excepţie, regiştri care conţin descrierea
evenimentului de excepţie curent, regiştri care memorează adresa virtuală care a
cauzat o excepţie etc.

3.4.6. AMBIGUITATEA REFERINŢELOR LA MEMORIE

Dependenţele cauzate de variabilele aflate în memorie reprezintă o altă


frână în calea obţinerii performanţei. Pentru exemplificare, să considerăm
secvenţa de două instrucțiuni cu referire la memorie:

ST 4 ( Ri ), R1
LD R2, 8 ( Rj )

După cum deja am arătat, există motive ca instrucţiunea LD să se execute


înaintea instrucţiunii ST, din motive de eficienţă a execuţiei (mascare latenţă –
Load Delay Slot, beneficii legate de procesarea out of order etc.) Acest lucru
este posibil numai dacă cele două adrese de memorie implicate sunt întotdeauna
diferite. Este evident că dacă la un anumit moment ele sunt identice, semantica
secvenţei se modifică inacceptabil printr-o execuție out of order. În general,
această problemă se rezolvă static, de către compilator, atunci când acest lucru
este posibil. O componentă a acestuia ("disambiguating routine") compară cele
două adrese de memorie şi returnează una dintre următoarele trei posibilităţi:
a) adrese întotdeauna distincte;
b) adrese întotdeauna identice;
c) cel puţin două adrese identice sau nu se poate determina.
Aşadar, doar în primul caz putem fi siguri că execuţia anterioară a
instrucţiunii LD faţă de instrucţiunea ST (sau simultană în cazul unui procesor
MEM - Maşină cu Execuţie Multiplă, vezi Capitolul următor) îmbunătăţeşte
performanţa, fără a cauza alterarea semantică a programului. Din păcate, nu se
poate decide întotdeauna acest lucru în momentul compilării.
Dezambiguizarea statică dă rezultate bune în cazul unor adresări liniare şi
predictibile ale memoriei (spre exemplu, accesări de tablouri, matrici etc.) Ea
presupune rezolvarea unor ecuaţii diofantice mai mult sau mai puţin complexe,
similare cu cele necesare vectorizării buclelor de program (v. Capitolul
următor). Prin urmare, un reorganizator de program bazat pe dezambiguizarea
statică, va fi deosebit de conservator în acţiunile sale. Dacă această comparare a
adreselor de memorie se face pe parcursul procesării programului prin hardware,

201
se zice că avem o dezambiguizare dinamică. Aceasta este mai performantă decât
cea statică (în timpul execuției se poate discrimina între adresele de memorie),
dar necesită, evident, resurse hardware suplimentare şi deci costuri sporite.
Pentru a pune în evidenţă performanţa superioară a variantei dinamice, să
considerăm secvenţa de program:

for i = 1 to 100 do
a[ 2i ]=....
y = f(..., a[i+4], ...)
end

Într-un singur caz din cele 100 posibile (i = 4), cele două referinţe (adrese)
la memorie a[2i] respectiv a[i + 4] sunt identice. Aşadar, o dezambiguizare
statică va fi conservatoare, nepermiţând optimizarea buclei, deşi doar in 1% din
cazuri acest lucru este posibil (a[2i] = a[i + 4] i=4, unic). Pentru rezolvarea
situaţiei pe această cale este necesară scoaterea din buclă a dependenţelor
datorate alias-urilor. O variantă dinamică însă, va putea exploata mai eficient
acest fapt.
Exemplu de dezambiguizare statică, efectuată cu ajutorul compilatorului:

1. 2.
for i=1 to 100 a[2]=x[1];
a[2i]=x[i]; y[1]=a[2]+5;
y[i]=a[i+1]+5; for i=2 to 100
a[2i]=x[i];
y[i]=a[i+1]+5;

ET: ET:
… …
ST Rj, a[2i]; // a[2i]=x[i] (x[i] este in Rj)
LD Rk, a[i+1]; // y[i]=a[i+1] LD Rk, a[i+1]; // y[i]=a[i+1]
NOP; //Load Delay Slot! ST Rj, a[2i]; // a[2i]=x[i];
ADD Rk, Rk, #5; // Rk=a[i+1]+5 ADD Rk, Rk, #5;
ST Rk, y[i]; // y[i]=a[i+1]+5 ST Rk, y[i]
... ...
LOOP ET LOOP ET

202
Pe un procesor superscalar:

ET:

LD Rk, a[i+1]; // y[i]=a[i+1], ST Rj, a[2i]; // a[2i]=x[i]; LD/ST IN
PARALEL!
NOP; // LDS
ADD Rk, Rk, #5; // Rk=a[i+1]+5
ST Rk, y[i];
...
LOOP ET;

După cum se poate remarca în acest exemplu, scoaterea în afara buclei a


primei iterații (i=1), permite execuția out of order a instrucțiunilor LD și ST din
cadrul buclei, cu mari avantaje asupra timpului de rulare. În cazul buclei inițiale,
acest fapt nu este posibil. În consecință, aceasta se procesează neoptimal,
datorită instrucțiunii NOP care umple LDS-ul. Pe un procesor superscalar sau
VLIW acest lucru este şi mai avantajos, întrucât cele două operaţii din buclă se
vor putea realiza simultan. Se consideră că progresele în soluționarea acestei
probleme pot duce la creşteri semnificative de performanţă în domeniul
paralelismului la nivelul instrucţiunilor.

3.4.7. EXECUŢIA CONDIŢIONATĂ ŞI SPECULATIVĂ

Execuţia condiţionată (predicativă) se bazează pe implementarea în


Instruction Set Architecture a unor aşa numite instrucţiuni cu execuții
condiţionate. O instrucţiune condiţiontă se va executa dacă o variabilă de
condiţie, inclusă în corpul instrucţiunii, îndeplineşte condiţia dorită. În caz
contrar, instrucţiunea respectivă nu va avea nici un efect (NOP). Variabila de
condiţie poate fi memorată într-un registru general al procesorului sau în anumiți
regiştri, special dedicaţi acestui scop, numiţi regiştri booleeni (pe un singur bit).
Astfel, spre exemplu, instrucţiunea CMOVZ R1, R2, R3 mută (R2) în R1 dacă
(R3) = 0. Instrucţiunea TB5 FB3 ADD R1, R2, R3 execută adunarea numai dacă
variabilele booleene B5 şi B3 sunt '1' respectiv '0'. În caz contrar, instrucţiunea
este inefectivă. Desigur că variabilele booleene necesită biţi suplimentari în
corpul instrucţiunii.

203
Execuţia condiţionată a instrucţiunilor este deosebit de utilă în eliminarea
salturilor condiţionate dintr-un program, simplificând programul şi transformând
deci hazardurile de ramificaţie în hazarduri de date. Să considerăm spre
exemplificare o construcţie if-then-else ca mai jos:

if (R8<1) LT B6, R8, #1; if R8<1, B6<---1


R1 = R2 + R3, BF B6, Adr1; Dacă B6=0 salt la Adr1
else ADD R1, R2, R3
R1 = R5 - R7; BRA Adr2 ; salt la Adr2
R10 = R1 + R11; Adr1: SUB R1, R5, R7
Adr2: ADD R10, R1, R11

Prin rescrierea acestei secvenţe de program utilizând instrucţiuni


condiţionate se elimină cele două instrucţiuni de ramificaţie, obţinându-se
următoarea secvenţă mai simplă şi mai eficientă de program:

LT B6, R8, #1
TB6 ADD R1, R2, R3
FB6 SUB R1, R5, R7
ADD R10, R1, R11

Este clar că timpul de execuţie pentru această secvenţă de program este mai
mic decât cel aferent secvenţei anterioare, pe lîngă faptul că secvența de
program este mai simplă (doar 4 instrucțiuni, dintre care două exclusive mutual
în execuție). Se arată că astfel de transformări implementate în compilatoare
reduc cu cca. 25-30% instrucţiunile de salt condiţionat dintr-un program.
Această execuţie condiţionată a instrucţiunilor facilitează execuţia speculativă.
Codul situat după un salt condiţionat în program şi executat înainte de stabilirea
condiţiei şi adresei de salt, cu ajutorul instrucţiunilor condiţionate, se numeşte
cod cu execuţie speculativă, operaţia respectivă asupra codului numindu-se
predicare. Predicarea reprezintă o tehnică de procesare care - utilizând
instrucţiuni cu execuţie condiţionată - urmăreşte execuţia paralelă, prin
speculaţie, a unor instrucţiuni şi reducerea numărului de ramificaţii din program,
ambele benefice pentru minimizarea timpului de execuţie al programului. Acest
mod de execuţie a instrucţiunilor poate fi deosebit de util în optimizarea
execuţiei unui program.
Prezentăm în continuare o secvenţă de cod iniţială şi care apoi este

204
transformată de către scheduler, în vederea optimizării execuţiei, prin speculaţia
unei instrucţiuni.

SUB R1, R2, R3 SUB R1, R2, R3


LT B8, R1, #10 LT B8, R1, #10
BT B8, Adr FB8 ADD R7, R8, R1; speculativă
ADD R7, R8, R1 BT B8, Adr
SUB R10, R7, R4 SUB R10, R7, R4

În secvența transformată (din partea dreaptă), instrucțiunea ADD R7,


R8, R1 a migrat într-un basic block anterior (percolation), cu beneficii asupra
timpului de execuție. Execuţia speculativă a instrucţiunii ADD putea fi realizată
şi în lipsa variabilelor de gardă booleene, dar atunci putea fi necesară
redenumirea registrului R7 (dacă acesta ar fi în viaţă pe ramura pe care saltul se
face). Orice instrucţiune - cu excepţia celor de tip STORE - poate fi executată
speculativ. O posibilă strategie de a permite instrucţiuni STORE speculative
constă în introducerea unui Data Write Buffer (DWB), în vederea degrevării
procesorului de procesarea scrierilor efective în memorie. Memorarea se va face
întâi aici şi abia când condiţia de salt va fi cunoscută se va înscrie data și în
memorie. Pe lângă avantajele legate de eliminarea salturilor, facilizarea
execuţiei speculative, predicării etc., execuţia condiţionată a instrucțiunilor-
mașină are şi câteva dezavantaje, dintre care amintim următoarele:
- instrucţiunile condiţionate anulate (NOP) necesită totuşi un timp de
execuţie. În cazul speculaţiei, în aceste condiţii performanţa în execuţie scade.
- dacă variabila de condiţie este evaluată prea târziu, utilitatea instrucţiunii
condiţionate va fi micşorată.
- promovarea unei instrucţiuni peste mai multe ramificaţii condiţionate în
vederea execuţiei speculative necesită gardări multiple și deci, un timing mărit în
execuție.
- instrucţiunile condiţionate pot determina scăderea frecvenţei de tact a
microprocesorului.
Având în vedere cele de mai sus, utilitatea execuţiei condiţionate este încă
discutată. MIPS, POWER-PC, SUN-SPARC, DEC ALPHA deţin doar o
instrucţiune de tip MOVE condiţionată, în timp ce alte microarhitecturi, precum
HEWLET PACKARD PA, HARP, HSA etc., permit execuţia condiţionată a
majorităţii instrucţiunilor maşină. La ora actuală există încă puţine evaluări
cantitative care să stabilească avantajele/dezavantajele acestei idei, într-un mod

205
clar, convingător.

Figura 3.40. Instrucțiune mutată speculativ prin două gardări

De subliniat acum, la finalul acestui capitol destinat microprocesoarelor


pipeline RISC scalare, că există mai multe simulatoare software extrem de utile
studiului acestora. Recomandăm în acest sens în special simulatoarele SPIM
(CPU MIPS) și DLX, utilizate de noi în lucrările practice de laborator din cadrul
studiilor de licență, în domeniul științei și ingineriei calculatoarelor [Flo03]. Prin
intermediul unor asemenea simulatoare se poate aprofunda modul de procesare a
instrucțiunilor prin intermediul structurilor pipeline de procesare, limbajul de
asamblare aferent etc.

206
4. PROCESOARE CU EXECUŢII MULTIPLE ALE
INSTRUCŢIUNILOR. MULTIPROCESOARE

4.1. CONSIDERAŢII GENERALE. PROCESOARE SUPERSCALARE ŞI


VLIW (EPIC)

Acest capitol are la bază, în principal, părți ale unei lucrări anterioare scrise
și publicate de autor sub forma unei monografii tehnico-științifice [Vin00], cu
revizuiri și adăugiri semnificative în această versiune, în speranța că, sub această
formă nouă, de tratat universitar, va fi mai ușor asimilabil de către studenții și
specialiștii interesați. Au fost utilizate și alte lucrări ale autorului [Vin00b,
Vin02, Vin07] etc., citate corespunzător, cu rafinări și dezvoltări importante în
versiunea unitară prezentată aici.
Un deziderat ambiţios în dezvoltarea microprocesoarelor este acela de se
atinge rate medii de procesare de mai multe instrucţiuni per tact (ciclu de
procesare). Procesoarele care iniţiază execuţia mai multor operaţii (instrucțiuni)
independente, simultan intr-un ciclu (sau tact), se numesc procesoare cu execuţii
multiple ale instrucţiunilor (Multiple Instruction Issue Processors). Un astfel de
procesor aduce din cache-ul de instrucţiuni una sau mai multe instrucţiuni
simultan şi le distribuie spre execuţie în mod dinamic sau static (prin
reorganizatorul de program – software scheduler), multiplelor unităţi funcționale
de execuţie.
Principiul acestor procesoare paralele, numite şi "maşini cu execuţie
multiplă" (MEM), constă în existenţa mai multor unităţi de execuţie paralele,
care pot avea latenţe diferite. Aceste unități pot fi la rîndul lor pipeline-izate,
pentru reducerea latențelor de procesare. Pentru a facilita procesarea acestor
instrucţiuni, acestea sunt codificate pe un singur cuvânt de 32 sau 64 de biţi
uzual, pe modelul RISC anterior prezentat. Dacă decodificarea instrucţiunilor,
detecţia dependenţelor de date dintre ele, rutarea şi lansarea lor în execuţie, din
bufferul de prefetch sau din fereastra de instrucțiuni curentă înspre unităţile
funcţionale, se fac prin hardware, în mod dinamic (run-time), aceste procesoare
MEM se mai numesc şi superscalare.

207
Pot exista mai multe unităţi funcţionale distincte (în general neomogene),
dedicate, spre exemplu, diverselor tipuri de instrucţiuni de tip întreg sau flotant
(sau chiar vectorial). Aşadar execuţiile instrucţiunilor întregi, se suprapun cu
execuţiile instrucţiunilor flotante (FP - Flotant Point). În cazul procesoarelor
MEM, paralelismul temporal, determinat de procesarea pipeline a
instrucțiunilor, se suprapune cu un paralelism spaţial, determinat de existenţa
mai multor unităţi funcționale de execuţie. În general, structura pipeline a co-
procesorului (virgulă mobilă, vectorial etc.) are mai multe nivele decât structura
pipeline a procesorului care operează pe întregi, ceea ce implică probleme de
sincronizare mai dificile decât în cazul procesoarelor pipeline scalare. Acelaşi
lucru este valabil şi între diferite alte tipuri de instrucţiuni având latenţe de
execuţie diferite. Caracteristic deci procesoarelor superscalare este faptul că
dependenţele de date între instrucţiuni se rezolvă prin hardware, în momentul
decodificării și execuției instrucţiunilor. Astfel, la procesoarele superscalare cu
execuții out of order ale instrucțiunilor, dependențele de date de tip WAR și
WAW se rezolvă prin redenumirea dinamică a registrelor implicate, după cum
se va arăta, în mod concret, în continuare. Modelul ideal de procesare
superscalară, în cazul unui procesor care poate aduce şi decodifica două
instrucţiuni simultan, este prezentat în Figura 4.1. După cum se poate remarca,
spre deosebire de procesoarele (pipeline) scalare, care execută la un moment dat
maximum o instrucțiune (uneori niciuna, din cauza stagnărilor impuse de
hazarduri), cele superscalare pot avea într-o anumită fază de procesare mai
multe instrucțiuni independente simultan. Așadar, bariera de performanță a
procesoarelor pipeline scalare, de o instrucțiune/ciclu, poate fi depășită în cazul
celor superscalare.
Este evident că în cazul superscalar complexitatea logicii de control este
mult mai ridicată decât în cazul pipeline scalar, întrucât detecţia şi sincronizările
între structurile pipeline de execuţie, cu latenţe diferite şi care lucrează în
paralel, devin mult mai dificile. Spre exemplu, un procesor superscalar RISC
având posibilitatea aducerii şi execuţiei a "n" instrucţiuni maşină simultan,
necesită teoretic 2C 2n =2n(n-1)/2=n(n-1) unităţi de detecţie a hazardurilor de date
între aceste instrucţiuni (comparatoare digitale), ceea ce conduce la o
complexitate ridicată a logicii de control (justificați!)

208
Figura 4.1. Modelul execuţiei superscalare
S-ar putea deci considera aceste procesoare MEM ca fiind arhitecturi de tip
MIMD (Multiple Instructions Multiple Data) conform taxonomiei lui Michael
Flynn. De remarcat, totuşi, că în această categorie sunt introduse cu precădere
sistemele multiprocesor sau alte tipuri de sisteme paralele sau chiar distribuite,
care exploatează paralelismul la nivelul mai multor fire de execuție aferente unei
aplicații (coarse grain parallelism), arhitecturile RISC ca şi cele de tip MEM
exploatând paralelismul instrucţiunilor la nivelul aceleiaşi aplicaţii sau fir de
execuție (fine grain parallelism). O exprimare în jargonul de specialitate ar
putea afirma că primele exploatează Thread/Task Level Parallelism (paralelism
la nivelul firelor de execuție sau chiar al task-urilor independente), în timp ce
arhitecturile MEM exploatează Instruction Level Parallelism (paralelism la
nivelul instrucțiunilor). Desigur că - din punctul de vedere al acestei taxonomii
simpliste - arhitecturile pipeline scalare (RISC), ar fi încadrabile în clasa SISD
(Single Instruction Single Data), fiind deci incluse în aceeaşi categorie cu
procesoarele cele mai convenţionale (secvenţiale), ceea ce implică o slăbiciune a
acestei sumare taxonomii.
În Figura 4.2 se prezintă o structură tipică de procesor superscalar care
deţine practic două module care lucrează în paralel: un procesor universal (de uz
general) şi un procesor destinat operaţiilor în virgulă mobilă. Ambele module
deţin unităţi de execuţie proprii, având latenţe diferite. La anumite
microprocesoare superscalare regiştrii CPU sunt diferiţi de regiştrii FP, pentru a
se reduce hazardurile structurale dar și cele de date (în schimb, apar creşteri
serioase ale costurilor şi dificultăţi tehnologice) iar la altele (spre ex. Motorola
88100), regiştrii CPU sunt identici cu cei ai co-procesorului. De exemplu, pentru
eliminarea hazardurilor structurale, multe dintre aceste microprocesoare nu deţin
"clasicul" registru al indicatorilor de condiţie. În consecință, salturile
condiţionate se realizează prin compararea pe o anumită condiţie, a două dintre
registrele codificate în instrucţiune. Hazardurile structurale la resursele hardware

209
interne se elimină prin multiplicarea acestora şi sincronizarea adecvată a
proceselor.

Figura 4.2. Structură de procesor superscalar pipeline


De remarcat că procesoarele superscalare, determină apropierea ratei de
execuţie la una sau, în cazul în care se pot și aduce mai multe instrucţiuni
simultan, la mai multe instrucţiuni per ciclu. Dificultăţile de sincronizare sporite,
se rezolvă prin utilizarea unor tehnici hardware bazate pe "scoreboarding",
deosebit de sofisticate și complexe. Majoritatea microprocesoarelor RISC
actuale de uz general sunt de tip superscalar (conţin cel puţin un coprocesor
integrat în chip). Un procesor superscalar care aduce din cache-ul de instrucţiuni
mai multe instrucţiuni primitive (tip RISC) simultan, având 6 unități de execuție,
poate mări rata medie de procesare la 1.2 - 2.3 instrucțiuni/ciclu, măsurat pe o
mare diversitate de benchmark-uri, la nivelul realizărilor practice între anii
1995-1998. Exemple remarcabile de microprocesoare superscalare comerciale
de tip RISC, sunt: INTEL 960 CA, SUN SuperSPARC, MPC 601, 603, 620
(POWER PC) etc. Microprocesoarele Intel Pentium (diferite versiuni), AMD K6
etc., sunt practic procesoare având model de programare CISC, dar execuţie
hardware superscalară a instrucțiunilor, de tip out of order. În acest caz
instrucțiunile CISC ale modelului de programare (spre exemplu, compatibile cu
setul de instrucțiuni x86) sunt “sparte” run-time în micro-instrucțiuni de tip
RISC (RISC like OPerations – ROPs) procesate de un procesor RISC pipeline
superscalar. Practic, mașina CISC (compatibilă Intel x86 spre exemplu) este o
mașină virtuală în acest caz, fiind emulată de un procesor hardware RISC, mai
rapid.
Procesoarele VLIW (Very Long Instruction Word) reprezintă procesoare
care se bazează pe aducerea, în cadrul unei instrucţiuni multiple, a mai multor
instrucţiuni RISC independente de date, pe care le distribuie spre procesare

210
unităţilor de execuţie. Aşadar, rata de execuţie ideală la acest model, este de mai
multe instrucţiuni/ciclu. Pentru a face acest model viabil, sunt necesare
instrumente software de exploatare a paralelismului programului, bazate pe
gruparea instrucţiunilor simple independente şi deci executabile în paralel, în
instrucţiuni multiple. Arhitecturile VLIW sunt tot de tip MEM. Principiul VLIW
este sugerat în Figura 4.3:

Figura 4.3. Decodificarea şi alocarea instrucţiunilor într-un procesor VLIW


În cadrul acestui model, se încearcă prin transformări ale programului
(scheduling static), ca instrucţiunile RISC primitive din cadrul unei instrucţiuni
multiple să fie independente de date şi deci să se evite hazardurile de date între
ele, a căror gestionare ar fi deosebit de dificilă în acest caz. Evident că
redenumirea resurselor în vederea eliminării dependențelor de date de tip WAR
sau WAW se face în mod static, prin componenta compilatorului numită
scheduler. Latențele instrucțiunilor sunt controlate tot de către scheduler.
Performanţa procesoarelor VLIW este esenţialmente determinată de programele
de compilare şi reorganizare care trebuie să fie deosebit de "inteligente". De
aceea, acest model de arhitectură se mai numeşte uneori şi EPIC (Explicitly
Parallel Instruction Computing – termen introdus în premieră de compania
Intel, după știința autorului).
Prin urmare, în cazul modelului de procesor VLIW, compilatorul trebuie să
înglobeze mai multe instrucţiuni RISC primitive independente în cadrul unei
instrucţiuni multiple (așadar o exploatare statică a paralelismului la nivel de
instrucțiuni), în timp ce în cazul modelului superscalar, rezolvarea
dependenţelor între instrucţiuni se face prin hardware, în mod dinamic, începând
cu momentul decodificării acestor instrucţiuni. De asemenea, poziţia
instrucţiunilor primitive într-o instrucţiune multiplă determină alocarea (rutarea)
statică a acestor instrucţiuni primitive la unităţile de execuţie, spre deosebire de
modelul superscalar, unde alocarea se face dinamic, prin control hardware.
Acest model de procesor nu mai necesită sincronizări şi comunicaţii de date
suplimentare între instrucţiunile primitive după momentul decodificării lor, fiind

211
astfel mai simplu din punct de vedere hardware decât modelul superscalar. Un
model sugestiv al principiului de procesare VLIW este prezentat în Figura 4.4.

Figura 4.4. Principiul de procesare VLIW


Pentru exemplificarea principiului de procesare MEM - VLIW, să
considerăm secvenţa de program de mai jos:

LOOP: LD F0, 0(R1)


ADD F4, F0, F2
SD 0(R1), F4
SUB R1, R1, #8
BNEZ R1, LOOP

Se va analiza în continuare cum ar trebui reorganizată şi procesată secvenţa


de program anterioară pentru a fi executată pe un procesor VLIW care poate
aduce maximum 5 instrucţiuni primitive simultan şi deţine 5 unităţi de execuţie
distincte şi anume: două unităţi LOAD / STORE (MEM1, MEM2), două unităţi
de coprocesor flotant (FPP1, FPP2) şi o unitate de procesare a instrucţiunilor
întregi şi respectiv a branch-urilor. S-a considerat că toate instrucțiunile au
latența de un tact, cu excepția celor de LD care au latența de două tacte. Așa
cum este scrisă, secvența este procesată puternic sub-optimal de procesorul
VLIW, datorită secvențialității intrinseci din buclă. Mai precis, adunarea din
buclă (ADD) este dependentă RAW de încărcarea din memorie (LD) prin
registrul F0, iar memorarea (SD) este dependentă RAW, prin registrul F4, de
adunare (ADD). Mai mult, saltul condiționat (BNEZ) este dependent RAW prin
registrul R1 de instrucțiunea de scădere (SUB). Așadar, bucla este intrinsec
secvențială, unitățile multiple de execuție fiind “neputincioase” (adică
neutilizabile în mod semnificativ) în această formă de scriere a programului.

MEM 1 MEM 2 FPP 1 FPP 2 CPU/BRANCH


1 Loop: LD LD F6, -
F0, 0(R1) 8(R1)

212
2 LD F10, - LD F14, -
16(R1) 24(R1)
3 LD F18, - LD F22, - ADD F4, ADD F8,
32(R1) 40(R1) F0, F2 F6, F2
4 LD F26, - LD F30, - ADD F12, ADD F16,
48(R1) 56(R1) F10, F2 F14, F2
5 ADD F20, ADD F24,
F18, F2 F22, F2
6 SD 0(R1), SD -8(R1), ADD F28, ADD F32,
F4 F8 F26, F2 F30, F2
7 SD -16(R1), SD -24(R1),
F12 F16
8 SD -32(R1), SD -40(R1), SUB R1, R1, #64
F20 F24
9 SD -48(R1), SD -56(R1), BNEZ R1, Loop
F28 F32
10 NOP

Tabelul 4.1. Execuţia instrucţiunilor pe un procesor VLIW cu 5 unităţi


de execuţie specializate

De remarcat faptul că după reorganizare (v. tabelul anterior) se obține o


rată medie de procesare de 2.7 instrucţiuni / ciclu, prin faptul că se execută
practic 8 iterații în 10 cicli. Altfel exprimat, o iterație din bucla de program
anterioară se execută în doar 1.25 cicli (10 cicli / 8 iterații). De remarcat, printre
altele, o redenumire a regiştrilor prin compilator, absolut necesară acestei
procesări paralele agresive. Posibilităţile hard / soft aferente unei asemenea
procesări vor fi prezentate succint în continuare. Este clar că performanţa
procesoarelor MEM este esenţial determinată de programele de compilare şi
reorganizare care trebuie să fie deosebit de "inteligente". Cercetări realizate în
comun la Universitatea Stanford din SUA şi compania DEC (Digital Equipment
Corporation) pe procesoare VLIW cu 4 instrucţiuni simultane, au arătat că în
aplicaţii reale se ajunge la execuţia a maximum 2 - 3 instrucţiuni / ciclu, prin
aportul semnificativ al unor compilatoare optimizate. Deşi relativ rare, există
realizări comerciale de computere VLIW cu software de optimizare de oarecare
succes pe piaţă: IBM RS / 6000 ( 4 instrucţiuni / ciclu , teoretic), INTEL 860
(maxim 2 instrucţiuni / ciclu), APOLLO DN 10000 etc. Aceste realizări sunt
disponibile comercial începând cu anul 1991, deşi cercetările au fost iniţiate
începând din 1983, prin cercetările Dr. Joshua Fisher (ulterior laureat al

213
Premiului Eckert-Mauchly, decernat de IEEE și ACM). Firma Intel a anunţat că
modelul de procesor având numele de cod Merced (Itanium Architecture-64),
care a fost lansat pe piață în anii 1999 - 2000, a fost realizat pe principii VLIW
(EPIC). Având în vedere că în cadrul acestor arhitecturi compilatorul este
puternic senzitiv la orice modificare hardware, personal văd o legătură hardware
- software semnificativ mai pronunţată decât la procesoarele superscalare
clasice. Necesităţile de "upgrade" hardware - software, cred, de asemenea, că
sunt mai imperioase prin această filosofie EPIC, necesitând deci, mai mult decât
până acum din partea utilizatorilor, serioase şi continue investiţii financiare,
corespunzător noilor modele hardware. IA-64 (Intel Architecture) a fost prima
arhitectură Intel pe 64 de biţi care a înglobat două caracteristici esenţiale,
descrise deja în Capitolul 2 al lucrării: execuţia condiţionată prin variabile de
gardă booleene a instrucţiunilor (“execuţie predicativă”) şi respectiv execuţia
speculativă a instrucţiunilor – cu beneficii asupra mascării latenţei unor
instrucţiuni mari consumatoare de timp şi deci asupra vitezei de procesare.
Arhitectura se bazează pe explicitarea paralelismului instrucţiunilor la nivelul
compilatorului, într-un mod similar cu cel din arhitecturile VLIW. Compania
Intel susţine că programele optimizate pe o anumită maşină IA-64 funcţionează
fără probleme pe oricare altă viitoare maşină din aceeași clasă, întrucât latenţele
unităţilor de execuţie, ca şi numărul acestora, sunt invizibile pentru
optimizatorul de cod. Aceasta se realizează însă prin interconectarea totală a
unităţilor de execuţie care se sincronizează prin tehnici de tip “scoreboarding”.
Rezultă deci că un program obiect portat de la o versiune mai veche de procesor
la alta mai nouă, chiar dacă va funcţiona totuşi corect, se va procesa mai lent
decât dacă ar fi optimizat special pentru noua variantă de procesor. Oricum,
succesul comercial al procesoarelor VLIW nu este în domeniul sistemelor de
calcul de uz general, ci în domeniul sistemelor dedicate (incorporate, embedded
systems). Motivul este simplu: deseori în aceste cazuri, compatibilitatea de sus
în jos nu mai este necesară. În plus, arhitecturile VLIW se pretează la structura
aplicațiilor dedicate din categoria telecom (voce), procesare de imagini, digital
video, compresii de date, automotive etc. [Fis05]
Dificultăţile principale ale modelului VLIW sunt următoarele:
- Paralelismul limitat al aplicaţiei, ceea ce determină ca unităţile de
execuţie să nu fie ocupate permanent, fapt valabil de altfel şi la modelul
superscalar.
- Incompatibilitate software cu modele succesive şi compatibile de
procesoare, care nu pot avea în general un model VLIW identic datorită faptului

214
că paralelismul la nivelul instrucţiunilor depinde de latenţele operaţiilor
procesorului scalar, de numărul unităţilor funcţionale şi de alte caracteristici
hardware ale acestuia.
- Dificultăţi deosebite în reorganizarea aplicaţiei (scheduling) în vederea
determinării unor instrucţiuni primitive independente sau cu un grad scăzut de
dependenţe.
- Creşterea complexităţii hardware şi a costurilor, ca urmare a resurselor
multiplicate, căilor de informaţie "lăţite" etc.
- Creşterea necesităţilor de memorare ale programelor, datorită
reorganizărilor software şi "împachetării" instrucţiunilor primitive RISC în
cadrul unor instrucţiuni multiple, care necesită introducerea unor instrucţiuni
NOP (atunci când nu există instrucţiuni de un anumit tip disponibile spre a fi
asamblate într-o instrucţiune multiplă).
În esenţă, prin aceste modele MEM se încearcă exploatarea paralelismului
din programe secvenţiale prin excelenţă, de unde şi limitarea principală a acestui
domeniu de "low level parallelism". Actualmente, datorită faptului că aceste
procesoare sunt mult mai ieftine decât procesoarele vectoriale (super-
procesoare), şi totodată foarte performante, se pune problema determinării unor
clase largi de aplicaţii în care modelele superscalar, superpipeline şi VLIW să se
comporte mai bine, sau comparabil, cu modelul vectorial (Single Instruction
Multiple Data). Se poate arăta relativ simplu, că din punct de vedere teoretic,
performanţa unui procesor superscalar având N unităţi funcţionale, fiecare cu o
structură pipeline pe M nivele, este echivalentă cu cea a unui procesor scalar
superpipeline, cu o structură pipeline pe M*N nivele. Asocierea unei arhitecturi
optimale unei anumite clase de aplicaţii, este o problemă dificilă. Performanţa
procesoarelor scalare superpipeline, superscalare de tip in order şi VLIW este în
strânsă legătură cu progresele compilatoarelor specifice acestor structuri,
compilatoare care trebuie să extragă cât mai mult din paralelismul existent la
nivelul instrucţiunilor programului. De remarcat că modelele superscalar şi
VLIW nu sunt exclusive, în implementările reale se întâlnesc adesea procesoare
hibride, în încercarea de a se optimiza raportul performanţă / preţ. După cum se
va vedea, spre exemplu tehnicile software de optimizare sunt comune ambelor
variante de procesoare. Aceste modele arhitecturale de procesoare paralele sunt
considerate a face parte din punct de vedere arhitectural, din generaţia a III-a de
microprocesoare, adică cea a anilor 1990 - 2000.

Studiu de caz. Microprocesorul Intel Itanium [Vin00b]

215
În continuare, se prezintă sub forma unui studiu de caz, arhitectura primului
microprocesor Intel pe 64 de biți, pe baza lucrării noastre [Vin00b], cu minore
revizuiri. Arhitectura Intel IA-64 pe 64 de biţi (având inițial numele de cod
Merced) a fost proiectată de către compania Intel în colaborare cu cercetătorii de
la Hewlett Packard, dar şi cu anumite grupuri de cercetare academice, precum
cel de la Universitatea din Illinois (compilatorul IMPACT, dezvoltat de
profesorul Wen Mei Hwu, fost doctorand al lui Yale Patt), ca reprezentând un
pas (r)evoluţionar pe linia viitoarelor microprocesoare (de după 1999)
comerciale de uz general, prin exploatarea agresivă a paralelismului la nivel de
instrucţiuni, printr-o sinergie hardware-software numită EPIC (“Explicitly
Paralell Instruction Computing”). Prin sinergie se înțelege faptul că
interacțiunea algoritmilor utilizați genereaza efecte mai bune decât
superpoziția efectelor individuale ale acestor algoritmi (așadar, în cazul
sistemelor sinergice, principiul superpoziției nu este respectat). S-a dorit, fără
mare succes comercial însă, ca IA-64 să înlocuiască deja clasicele standarde
Pentium de microprocesoare, cu unul nou. În vederea extragerii unui grad ILP
(“Instruction Level Parallelism”) maximal, microprocesorul IA-64 (Intel
Architecture) - ce va fi cunoscut, sub prima formă comercială implementată în
tehnologie de 0.18 microni la 800 MHz, sub numele de Itanium - include
caracteristici arhitecturale moderne, precum: execuţie speculativă şi predicativă
a instrucţiunilor, seturi extinse de regiştri interni (inclusiv pentru procese de
renaming), predictor avansat de branch-uri, optimizatoare de cod etc. Adresarea
memoriei se face pe 64 biţi, într-un spaţiu virtual enorm. Desigur că IA-64
respectă o compatibilitate binară perfectă cu arhitecturile Intel pe 32 biţi
anterioare (Pentium, Pentium Pro, Pentium II, Pentium III, Pentium IV etc.),
putându-se deci rula actualele aplicaţii software pe 32 biţi pe mai noile
platforme de operare pe 64 biţi. De altfel, arhitectura IA-64 permite atât
implementarea unor sisteme de operare pe 32 biţi în modurile de lucru protejat,
real şi virtual 8086 (Pentium) cât si a unor sisteme de operare pe 64 biţi, care
însă pot rula mai vechile aplicaţii pe 32 de biţi, în oricare dintre cele trei moduri
de lucru amintite. Aşadar, în orice moment, procesorul acesta poate rula atât
instrucţiunile Pentium ISA-32 (ISA - ”Instruction Set Arhitecture”) cât şi setul
nou ISA-64. Există implementate două instrucţiuni de salt necondiţionat
dedicate (având mnemonicile jmpe şi br.ia), care dau controlul programului spre
o instrucţiune ISA-64 respectiv ISA-32 şi totodată modifică în mod
corespunzător setul curent de instrucţiuni utilizate (ISA). În continuare, se vor
prezenta succint şi fatalmente incomplet, doar câteva dintre caracteristicile

216
arhitecturale novatoare ale arhitecturii IA-64, unele implementate în premieră în
sfera comercială
(altfel cunoscute şi investigate de mult timp în mediile de cercetare, în special în
cele academice).
Arhitectura generală: câteva aspecte
Pe scurt, arhitectura regiştrilor program ai procesorului Itanium ar fi
următoarea:
128 de regiştri generali pe 64 biţi (32 globali şi 96 locali, utilizabili în
ferestre, de către diversele programe). În modul de lucru pe 32 biţi IA-32
(compatibil Pentium), parte sau porţiuni din aceşti regiştri generali
îndeplinesc rolul cunoscuţilor regiştri ai acestei arhitecturi. Astfel, spre
exemplu, registrul GR8 devine acumulatorul extins EAX al arhitecturii Intel
x86, iar registrul GR17(15:0) devine registrul selector CS (Code Segment) din
cadrul modului de lucru protejat IA-32.
128 de regiştri FPP (Flotant Point Processor) pe 82 de biţi (primii 32 sunt
generali, ceilalţi sunt dedicaţi redenumirii statice, în special în vederea
accelerării procesării buclelor de program).
64 de regiştri de gardă (regiştri predicat), pe un bit, utilizaţi pentru execuţia
condiţionată a instrucţiunilor (vezi în continuare).
8 regiştri destinaţi instrucţiunilor de branch (salt condiționat), pe 64 de biţi.
Sunt utilizaţi pentru a specifica adresele destinaţie în cazul salturilor indirecte
(inclusiv cele de tip Call/Return).
un numărător de adrese program numit – clasic în tehnologia Intel –
Instruction Pointer (IP).
un registru numit CFM (“Current Frame Marker”) care descrie starea
ferestrei curente de regiştri locali (mărime fereastră, număr regiştri locali/de
ieşire, adrese de bază pentru regiştrii utilizaţi la redenumire).
un număr de aşa-numiţi regiştri de aplicaţie, incluzând regiştrii de date,
dedicaţi unor scopuri speciale precum şi regiştrii de control ai aplicaţiilor.

IA-64 procesează 6 tipuri distincte de instrucţiuni (întregi ALU, întregi


non-ALU, cu referire la memorie, flotante, ramificaţii şi extinse). Există
implementate 4 unităţi de execuţie şi anume:

unitatea numită I, dedicată instrucţiunilor întregi şi extinse


unitatea M, dedicată instrucţiunilor cu referire la memorie dar şi unor
instrucţiuni de tip întreg ALU

217
unitatea F dedicată instrucţiunilor în virgulă mobilă (FPP)
unitatea B (Branch) dedicată instrucţiunilor de ramificaţie program

Instrucţiunile IA-64 sunt procesate pipeline-izat în 4 faze distincte şi


anume:
faza de aducere a instrucţiunii din cache-ul de instrucţiuni sau din memoria
principală
faza de citire stare arhitecturală, dacă este necesară
faza de execuţie propriu-zisă a instrucţiunii
faza de actualizare a contextului arhitectural, dacă este necesară (“update”)

Astfel, IA-64 permite execuţia mai multor instrucţiuni – maşină


independente, în mod simultan, fapt facilitat prin implementarea unor unităţi de
execuţie multiple, seturi multiple de regiştri generali, optimizator de cod agresiv
(scheduler integrat în compilatoare şi care grupează instrucţiunile independente
din programul obiect în grupuri de instrucţiuni primitive multiple) etc.
Arhitectura permite transferuri de informaţie între compilator şi procesorul
hardware, în scopul minimizării efectelor defavorabile aferente miss – predicţiei
ramificaţiilor, miss-urilor în cache, instrucţiunilor Load/Store etc. Astfel, spre
exemplu, compilatorul poate transmite prin anumite câmpuri binare din codul
instrucţiunii, informaţii deosebit de utile legate de predicţia branch-urilor
(strategie de predicţie statică – probabil în vederea optimizării statice globale a
programelor obiect sau dinamică – prin predictorul hardware etc.)
Schedulerul “împachetează” câte trei instrucţiuni primitive independente
într-o aşa numită instrucţiune multiplă (“bundle”) după binecunoscutul principiu
care stă la baza arhitecturilor VLIW (“Very Long Instruction Word”). Formatul
unei asemenea instrucţiuni multiple este prezentat mai jos:

Slot instr. 1 Slot instr. Slot instr. 3 Template


2
41 biţi 41 biţi 41 biţi 5 biţi

Câmpul numit “Template” are două roluri distincte:


• codifică unitatea de execuţie aferentă fiecărei instrucţiuni primitive din
instrucţiunea multiplă, realizând astfel o rutare statică a instrucţiunilor spre
unităţile hardware de execuţie, cu beneficii majore asupra reducerii
complexităţii hardware a procesorului.

218
• indică aşa-numite “stopuri arhitecturale”; un atfel de “stop” aferent unui slot
informează structura hardware asupra faptului că una sau mai multe
instrucţiuni anterioare sunt dependente de date faţă de una sau mai multe
instrucţiuni situate după cea marcată cu indicatorul “stop”. Prin urmare, logica
de control va opri fluxul de instrucţiuni pe slotul respectiv până la rezolvarea
dependenţei în cauză. Cu alte cuvinte, structura hardware este asigurată, de
către schedulerul software (reorganizatorul - optimizator), că toate
instrucţiunile primitive cuprinse între două astfel de “stopuri” succesive sunt
independente (RAW – Read After Write sau WAW – Write After Write) şi,
prin urmare, ar putea fi procesate în paralel dacă resursele hardware
disponibile o permit. Se rezolvă astfel, în mod elegant, o redundanţă
operaţională caracteristică, din păcate, multor procesoare superscalare actuale,
prin reluarea de către hardware a stabilirii dependenţelor de date, operaţie
efectuată anterior de către schedulerul software. Iată deci esenţa conceptului
de “paralelism explicit” (EPIC) care caracterizează arhitectura IA-64, ca fiind
un hibrid interesant între o structură superscalară simplificată şi respectiv una
de tip VLIW.
Execuţia speculativă a instrucţiunilor
O altă caracteristică deosebit de modernă implementată în arhitectura IA-64
(microprocesorul Itanium) constă în execuţia speculativă a instrucţiunilor. Există
două tipuri de speculaţii: de control (când o instrucţiune aflată în program după
un salt condiţionat se execută înaintea acestuia, cu influenţă benefică asupra
timpului de execuţie) şi respectiv de date (când o instrucţiune tip “load” – de
citire din memorie - situată în program după o instrucţiune de tip “store” – de
scriere în memoria de date - se execută înaintea acesteia, cu beneficii importante
asupra timpului de execuţie, datorate mascării latenţei instrucţiunii de încărcare).
Pentru a exemplifica în mod concret o speculaţie de control pe IA-64, se
consideră următoarea secvenţă de program:

if (a > b)
load (ld_adr1,target 1)
else
load (ld_adr2,target 2)

Schedulerul va rescrie această secvenţă utilizând instrucţiuni “load” de tip


speculativ (“sload”), ca mai jos:

219
sload (ld_adr1,target1)
sload (ld_adr2,target2)
if (a>b)
scheck (target1, recovery_adr1)
else
scheck (target2, recovery_adr2)

În primul rând, se observă că instrucţiunile de încărcare se execută


speculativ, înainte de a se determina condiţia de salt (a>b), conducând astfel la
grăbirea procesării. Desigur că, în acest caz, sunt necesare corecţii în situaţia în
care una dintre cele două încărcări speculative generează o excepţie (spre
exemplu, o excepţie de tip “page fault” – pagină inexistentă momentan în
memoria principală). Pentru prevenirea unor asemenea evenimente nedorite se
verifică prin instrucţiuni corespunzătoare (“scheck”), dacă cele două instrucţiuni
speculative au generat vreo excepţie şi, în caz afirmativ, se pointează la o rutină
de restabilire a contextului programului (recovery_adr) în vederea asigurării
unui mecanism precis de excepţii.
Ca exemplu de speculaţie de date se consideră secvenţa de program:

store (st_adr,date) ; scriere în memoria de date


load (ld_adr,target) ; citire din memoria de date
use (target) ; utilizare variabilă

Cum în acest caz mecanisme de analiză statică antialias (st_ad = ld_adr ?,


adică adresa de acces a instrucțiunii Load este egală cu cea a instrucțiunii
Store?) nu sunt practic posibile, execuţia “out – of – order” a acestor instrucţiuni
este posibilă numai prin utilizarea unor mecanisme speculative. Şi în cazul
secvenţei rescrise speculativ sunt necesare corecţii, pentru a rezolva cazul unei
excepţii produse prin execuţia speculativă a instrucţiunii “Load”, ori în cazul în
care se dovedeşte că ld_adr = st_adr (aliasuri de memorie). Într-o astfel de
situaţie, se dă controlul unei rutine de restabilire a contextului, în conformitate
cu logica iniţială de program, rutină care începe la adresa “recovery_adr” (se
implementează deci un mecanism de excepţii precise). Secvenţa optimizată ar fi
în acest caz următoarea:

aload (ld_adr,target)
store (st_adr,data)

220
acheck(target,recovery_adr) ;verificare eveniment nedorit
use (target)
Principiile execuţiei predicative
O altă caracteristică arhitecturală importantă inclusă în procesoarele IA-64
constă în execuţia predicativă a instrucţiunilor [Vin00b]. În această filozofie, o
instrucţiune se execută efectiv sau nu (“nop”- no operation) în funcţie de
valoarea de adevăr a unei variabile booleene de gardă sau a mai multor astfel de
variabile, conjugate prin conectorul ŞI logic. Spre exemplu, instrucţiunea P5
R6=(R7)+(R9) execută adunarea, numai dacă variabila de gardă P5=”true”, în
caz contrar neexecutându-se practic nimic (“nop”). Desigur că aceste variabile
de gardă booleene se setează/resetează ca urmare a acţiunii unor instrucţiuni
(predicate de ordinul întâi, în general). Ca exemplu în acest sens, instrucţiunea
P=compare (a>b), face P=”true” dacă a>b şi respectiv P=”false” dacă a≤b.
Majoritatea instrucţiunilor IA-64 sunt executabile condiţionat. După cum am
mai subliniat în acest tratat universitar, prin utilizarea unor transformări ale
programului obiect bazat pe instrucţiuni gardate, se elimină circa 25% - 30%
dintre instrucţiunile de salt, mărindu-se astfel lungimea medie a unităţilor
secvenţiale de program (“basic – block”-uri), cu influenţe favorabile asupra
procesului de scheduling. Este adevărat însă că dependenţele datorate
instrucţiunilor de salt candiţionat (branch) se vor regăsi acum sub forma unor
noi dependenţe de date prin variabilele de gardă (RAW – dependenţe tip “Read
after Write”) şi care conduc la secvenţialitatea execuţiei programului. Se
prezintă mai jos transformarea unei secvenţe de program prin predicare cu
eliminarea saltului:

if (a>b) ; P6=compare (a>b)


c=c+1 ; TP6 c=c+1;
else
d=d∗e+5 ; FP6 d=d∗e+5;

Se obseră că dependenţa de ramificaţie (a>b) s-a transformat acum în


dependenţă RAW prin variabila de gardă P6. Un beneficiu suplimentar constă în
paralelizarea celor două instrucţiuni gardate şi exclusive mutual, din punct de
vedere al execuţiei lor (TP6, FP6). În continuare, se prezintă un exemplu
sugestiv în legătură cu avantajele/dezavantajele execuţiei predicative a

221
instrucţiunilor în cadrul arhitecturii IA-64. Se consideră secvenţa “if – then –
else” ca mai jos :

if (r4)
r3 = r2 + r1 ; 2 cicli
else
r3 = r2 ∗ r1
utilizare r3 ; ; 18 cicli

S-a considerat deci că ramificaţia “if” are latenţa de doi cicli procesor, iar
ramificaţia “else” de 18 cicli procesor. Secvenţa va fi compilată prin instrucţiuni
gardate, eliminându-se instrucţiunile de ramificaţie, ca mai jos:

cmpne p1,p2 = r4,r0 ;0/0 compară pe diferit


(p1) add r3 =r2,r1 ;1/1 adunare întregi
(p2) setf f1=r1 ;1/1 conversie întreg – flotant
(p2) setf f2=r2 ;1/1
(p2) xma.l f3=f1,f2 ;9/2 înmulţire f1xf2 (flotant)
(p2) getf r3=f3 ;15/3 conversie flotant – întreg
(p2) utilizare r3 ;17/4

În comentariu, imediat după semnul “;”, sunt scrise două cifre: prima
semifică numărul ciclului în care instrucţiunea respectivă va fi lansată în
execuţie, dacă variabila de gardă p1 = 1 (“true”), iar a 2–a acelaşi lucru, în cazul
contrar, anume p1 = 0 (implicit p2 = 1). Considerând acum că instrucțiunea
“setf” durează 8 cicli, “getf” - 2 cicli, “xma” - 6 cicli şi că o predicţie incorectă a
branch-ului costă procesorul 10 cicli (pt. restaurarea stării), se pot analiza două
cazuri complementare sugestive.

Cazul I

Se presupune că ramura “if” se execută 70% din timp iar acurateţea


predicţiei branch-ului din codul iniţial (nepredicativ) este 90%. Timpul de
execuţie al secvenţei iniţiale este:
(2 cicli x 70%) + (18 cicli x 30%) + (10 cicli x 10%) = 7.8 cicli
Timpul de execuţie al secvenţei compilate prin predicare este:
(5 cicli x 70%) + (18 cicli x 30%) = 8.9 cicli

222
În acest caz execuţia predicativă este neeficientă.

Cazul II

Se presupune că ramura “if” se execută 30% din timp şi că acurateţea


predicţiei branch-ului este acum de doar 70%. Timpul de execuţie al secvenţei
iniţiale este:
(2 cicli x 30%) + (18 cicli x 70%( + (10 cicli x 30%) = 16.2 cicli
Timpul de execuţie al secvenţei compilate prin predicare este:
(5 cicli x 30%) + (18 cicli x 70%) = 14.1 cicli
În acest al 2-lea caz execuţia predicativă este mai eficientă, micşorând
timpul mediu de execuţie cu mai mult de doi cicli.
După cum am arătat deja în acest tratat universitar, o altă caracteristică
interesantă, deşi utilizată chiar de către pionierii procesoarelor RISC (“Reduced
Instruction Set Computing”) încă din 1980 în cadrul microprocesorului Berkeley
I RISC (Prof. David Patterson, Universitatea Berkeley, SUA), constă în lucrul
“în ferestre de registre” (“register windows”). Această tehnică este în legătură cu
minimizarea timpului de intrare/revenire într-o/dintr-o procedură. Astfel, se
evită atunci când este posibil salvarea în stivă a setului de regiştri locali ai
programului apelant, în vederea reutilizării sale în cadrul procedurii apelate.
Printr-un fel de “chemare a străbunilor” (Jack London...), Itanium folosește și el
acest mod de lucru, altfel, cam “uitat” în cadrul procesoarelor moderne. Acest
lucru se realizează exclusiv în modul ISA-64 prin alocarea unui nou set
(ferestre) de regiştri locali, procedurii apelate. Mai precis, IA-64 conţine 32 de
regiştri generali (abreviați GR0 – GR31), utilizabili de către toate procedurile
(globali), şi respectiv 96 de regiştri utilizabili în ferestre dinamice (locali),
alocabili deci prin software diferitelor proceduri (alloc). La rându-i, o fereastră
de regiştri locali alocată procedurii în curs, conţine două zone distincte: o zonă
care conţine regiştrii locali de lucru şi parametrii de intrare generaţi de către
programul apelant şi respectiv o zonă de regiştri care memorează rezultatele de
ieşire, care vor fi utilizate la revenire, de către programul apelant. La revenirea
din procedură (return) se comută automat pe fereastra anterioară de regiştri
locali. Desigur că salvarea/restaurarea regiştrilor locali în/din stivă apare ca
necesară numai în cazul unor depăşiri ale setului secundar de 96 de regiştri
utilizabili în ferestre dinamice. Este interesantă implementarea acestei tehnici în
cadrul relativ modernei arhitecturi IA-64, întrucât deşi utilizată încă de la
începuturile generaţiei a 2-a arhitecturale de microprocesoare (scalare “pipeline”

223
RISC), ea nu a fost foarte agreată ulterior (excepţie face famila de
microprocesoare SPARC, a companei SUN), din cauza creşterii latenţei căii
critice de date a microprocesoarelor (timpul maxim necesar celei mai lungi
operaţii interne, încadrabile într-un ciclu) şi deci, în consecinţă, reducerii
frecvenţei de tact a acestora. Probabil că Intel Co. a renunţat deliberat la
supremaţia frecvenţelor de tact (aici, microprocesoarele din familia Alpha 21264
- Compaq spre exemplu, au fost mereu cu un pas înainte), mizând pe creşterea
gradului mediu de ILP extras prin metode arhitecturale de sinergism hardware-
software, precum cele descrise succint până acum. În fond, compania Intel ştie
prea bine că performanţa se obţine actualmente preponderent prin inovații
arhitecturale (cca. 65%) şi abia mai apoi prin performanța tehnologiei (cca.
35%). Iar apoi, o frecvență mare a tactului crește puterea dinamică și energia
termică din cip.
Optimizarea buclelor de program
Arhitectura IA-64 se bazează esenţialmente pe forţa compilatorului
(schedulerului) care exploatează în mod static paralelismul la nivel de
instrucţiuni din codul obiect. Optimizarea buclelor de program este esenţială
întrucît, după cum prea bine se ştie, cca. 90% din timp se execută cca.10% din
program, iar acestă fracţiune de 90% constă cu precădere în bucle de program.
Dintre tehnicile software de paralelizare a instrucţiunilor aferente buclelor de
program, IA-64 utilizează tehnica “modulo scheduling” (introdusă de
cercetătorii Monica Lam și Bob Rau+) care, în esenţă, transformă bucla iniţială
astfel încât să se permită execuţia simultană a unor iteraţii diferite, prin pipeline-
izarea software a acestora. Pentru a da oarecare concreteţe acestor aspecte, se va
prezenta în continuare un exemplu de optimizare a unei bucle simple de
program, prin utilizarea tehnicii de paralelizare a unor iteraţii diferite (“software
pipelining” – tehnică atribuită cercetătoarei americane Monica Lam).
Se consideră bucla de program IA-64:
L1:
ld r4 = [r5],4 ;încărcare din memorie cu post-incrementarea adresei (+4)
add r7= r4,r9
st [r6]= r7,4 ;memorare r7 cu post-incrementarea adresei (r6 + 4 -> r6)
br.cloop L1 ;loop
Instrucţiunea de buclare inspectează un registru special în care s-a încărcat
iniţial numărul de iteraţii aferent buclei, şi dacă acesta are un conţinut pozitiv, se
decrementează şi se face saltul. De remarcat că toate instrucţiunile buclei sunt
dependente, ceea ce conduce la o execuţie fatalmente serială a acestora, în

224
concordanţă cu implacabila lege a lui Eugene Amdahl. Având în vedere
avatariile instrucţiunii de salt în structurile pipeline şi superscalare, se poate
considera că execuţia acestei secvenţe de program este una ineficientă, lentă.
Schedulerul ar putea transforma această buclă, în urma aplicării tehnicii
“software pipelining” (TSP), ca mai jos:

ld r4 = [r5],4 ;iteraţia 1
add r7 = r4,r9 ;iteraţia 1 PROLOG
ld r4 = [r5],4 ;iteraţia 2
L1: st [r6]=r7,4 ;iteraţia k∈[1,n-2]
add r7 = r4,r9 ;iteraţia k+1 NUCLEU
ld r4 = [r5],4 ;iteraţia k+2
br.cloop L1
st [r6] = r7,4 ;iteraţia n-1
add r7 = r4, r9 ;iteraţia n EPILOG
st [r6] = r7,4 ;iteraţia n (ultima)

Esenţa TSP rezultă imediat analizând secvenţa NUCLEU care pipeline-


izează trei instrucţiuni aparţinând unor trei iteraţii succesive ale buclei de
program. Prin aceasta, cele trei instrucţiuni ale buclei NUCLEU s-ar putea
executa în paralel, desigur dacă resursele hardware o permit. Secvenţele
PROLOG/EPILOG nu fac decât să “ajusteze” din punct de vedere semantic
implicaţiile buclei nucleu. Pentru a evalua câştigul de performanţă obţinut în
urma aplicării TSP, se va rescrie secvenţa anterioară, grupând pe acelaşi rând
instrucţiunile paralelizabile, deci executabile în acelaşi ciclu maşină. În
paranteze se va scrie numărul iteraţiei căreia îi aparţine instrucţiunea respectivă.

ld (1) ld (2) ;primul ciclu


add (1) ;al 2-lea ciclu etc.
L1: st (k) add (k+1) ld (k+2)
br.cloop L1
st (n-1) add (n)
st (n)

Un alt câştig important al metodei TSP constă în faptul că expansiunea


codului optimizat este moderată şi datorată exclusiv secvenţelor de
prolog/epilog. Nu acelaşi lucru se poate afirma despre alte metode de optimizare

225
a buclelor de program (exemplu “loop unrolling”, prezentată în continuare în
această lucrare – v. Cap.4), care determină expansiuni ale codului de până la
200%, cu repercursiuni negative asupra vitezei de procesare. În mod oarecum
ironic, optimizarea buclelor de program urmăreşte reducerea timpului de
execuţie dar, prin expansiunea de cod implicată, creşte rata de miss în cache-uri,
ceea ce determină creşterea timpului de execuţie !
Această execuţie concurentă a unor (porţiuni din) iteraţii diferite, necesită
în mod frecvent redenumiri ale regiştrilor utilizaţi, în vederea eliminării
dependenţelor de date între aceştia (WAR, WAW). Arhitectura IA-64 permite ca
fiecare iteraţie să utilizeze propriul set de regiştri, evitând astfel necesităţile de
desfăşurare a buclelor (“loop unrolling”). De asemenea, se menţionează că IA-
64 are inclus un puternic co-procesor FPP (Flotant Point Processor) în virgulă
mobilă, înzestrat inclusiv cu facilităţi de procesare 3D precum şi un procesor
multimedia compatibil semantic cu tehnologia Intel MMX şi SIMD (“Single
Instruction Multiple Data” – model vectorial).
O altă tehnică foarte cunoscută de optimizare a buclelor de program este
aşa numita “Loop Unrolling” (LU), care se bazează pe desfăşurarea buclei de un
număr de ori şi apoi optimizarea acesteia. În cadrul diferitelor copii de iteraţii
concatenate se redenumesc anumiţi regiştri în vederea eliminării dependenţelor
de date de tip WAR sau WAW. Pentru exemplificarea tehnicii LU se consideră
bucla anterioară:

L1:
ld r4 = [r5],4 ;ciclul 0
add r7= r4,r9 ;ciclul 2, s-a presupus aici latenţa instr. “ld” de 2 cicli
st [r6]= r7,4 ;ciclul 3
br.cloop L1 ;ciclul 3

Iată ce devine această buclă simplă, după ce este desfăşurată de către


compilator de 4 ori (s-a presupus că numărul de iteraţii este multiplu de 4 şi că
memoria cache de date deţine două porturi de acces), iar apoi optimizată, în
vederea unei procesări optimale din punct de vedere al timpului consumat:

add r15 = 4,r5


add r25 = 8,r5
add r35 = 12,r5
add r16 = 4,r6

226
add r26 = 8,r6
add r36 = 12,r6
L1:
ld r4 = [r5],16 ; ciclul 0
ld r14 = [r15],16 ; ciclul 0
ld r24 = [r25],16 ; ciclul 1
ld r34 = [r35],16 ; ciclul 1
add r7 = r4,r9 ; ciclul 2
add r17 = r14,r9 ; ciclul 2
st [r6] = r7,16 ; ciclul 3
st [r16] = r17,16 ; ciclul 3
add r27 = r24,r9 ; ciclul 3
add r37 = r34,r9 ; ciclul 3
st [r26] = r27,16 ; ciclul 4
st [r36] = r37,16 ; ciclul 4
br.cloop L1 ; ciclul 4

Preambulul şi redenumirile din cadrul buclei desfăşurate sunt necesare


eliminării conflictelor de nume. Cu excepţia ciclului 2, în fiecare ciclu sunt
utilizate din plin cele două porturi ale memoriei cache de date. Performanţa
obţinută este de 4 iteraţii în 5 cicli, faţă de o iteraţie în 4 cicli, cât obţinea bucla
iniţială, o îmbunătăţire absolut remarcabilă. Desigur, după cum se poate observa
în virtutea unui necruţător, dar etern, compromis performanţă – cost, lungimea
buclei optimizate creşte, cu repercursiuni defavorabile evidente, dar acceptabile
totuşi, având în vedere creşterea vitezei de execuţie.

227
4.2. MODELE DE PROCESARE ÎN ARHITECTURILE
SUPERSCALARE

În cazul procesoarelor superscalare sunt citate în literatura de specialitate


trei modalităţi distincte de procesare şi anume: In Order Issue In Order
Completion (IN - IN), In Order Issue Out of Order Completion (IN - OUT) şi
respectiv Out of Order Issue Out of Order Completion (OUT -OUT). Pentru
exemplificarea afirmaţiei de mai sus, să considerăm o secvenţă de instrucţiuni I1
- I6 cu următoarele particularităţi: I1 necesită doi cicli mașină pentru execuţie, I3
şi I4 sunt în conflict (hazard) structural, între I4 şi I5 există dependenţă de date
de tip RAW, iar I5 şi I6 sunt, de asemenea, în conflict structural. În aceste
condiţii şi considerând un procesor superscalar care poate aduce şi decodifica
două instrucţiuni simultan şi care deţine două unităţi de execuţie, avem situaţiile
următoare pe cele trei modele:
a) Modelul IN - IN
Este caracterizat prin faptul că procesorul nu decodifică următoarea
pereche de instrucţiuni, decât în momentul în care perechea anterioară se
execută. Aşadar, atât execuţia instrucțiunilor cât şi înscrierea rezultatelor
aferente acestora se face în ordinea din program, ca în tabelul următor.

Tabelul 4.2. Exemplu de procesare IN-IN

b) Modelul IN - OUT
Este caracterizat de faptul că execuţia propriu-zisă se face în ordinea
secvențială din program, în schimb înscrierea rezultatelor (WB) se face de îndată
ce o instrucţiune s-a terminat de executat. Modelul este mai eficient (din punct
de vedere al timpului de execuție) decât cel precedent, însă poate crea probleme
de genul întreruperilor imprecise, care trebuie evitate prin tehnici și metode deja
prezentate în Capitolul 3.

Tabelul 4.3. Exemplu de procesare IN-OUT

c) Modelul OUT - OUT


Este cel mai agresiv şi mai performant model de procesare a instrucţiunilor
într-un procesor superscalar. Instrucţiunile sunt aduse şi decodificate sincron,
desigur in order, presupunând deci existenţa unui buffer între nivelul de
decodificare şi cele de execuţie (buffer de prefetch și instructions window).
Astfel, creşte capacitatea de anticipare a instrucţiunilor independente dintr-un
program. Modelul permite o exploatare mai bună a paralelismului instrucţiunilor
la nivelul unui program dat, prin creşterea probabilităţii de determinare a unor
instrucţiuni independente, stocate în buffer. În consecință, gradul de utilizare al
unităților de execuție (UE), crește și el.

Tabelul 4.4. Exemplu de procesare OUT-OUT

Desigur că execuţia Out of Order este posibilă numai atunci când


dependenţele de date între instrucţiuni o permit. Cade în sarcina hardware-ului

229
eliminarea dependenţelor de tip WAR și WAW, prin redenumirea dinamică a
resurselor implicate şi alocarea instrucţiunilor din bufferul de prefetch la
diversele unităţi de execuţie (rutarea dinamică).

4.3. ARHITECTURA LUI R. TOMASULO

A fost proiectată şi implementată pentru prima dată în cadrul unităţii de


calcul în virgulă mobilă din cadrul sistemului IBM - 360 / 91 şi este atribuită lui
Robert Tomasulo, considerat a fi fost pionierul procesării superscalare de tip out
of order şi, pe acest motiv, laureat al prestigiosului premiu IEEE/ACM Eckert
Mauchly Award pe anul 1996, acordat celor mai performanţi cercetători,
constructori sau proiectanţi de calculatoare. O instructivă prezentare a lui
Tomasulo referitoare la implementarea procesării out of order a instrucțiunilor
în sistemul de calcul IBM - 360 / 91 este disponibilă pe YouTube, mai precis la
https://www.youtube.com/watch?v=S6weTM1tNzQ (accesat în mai 2016).
Arhitectura este una de tip superscalar, având deci mai multe unităţi de execuţie,
iar algoritmul de control al acestei structuri stabileşte relativ la o instrucţiune
adusă, momentul în care aceasta poate fi lansată în execuţie şi respectiv unitatea
de execuţie care va procesa instrucţiunea. Arhitectura permite execuţia multiplă
(paralelă) şi speculativă, de tip Out of Order, a instrucţiunilor şi constituie
modelul de referinţă în reorganizarea dinamică a instrucţiunilor într-un procesor
superscalar actual (spre ex., Intel Pentium IV). De asemenea, algoritmul de
gestiune aferent arhitecturii permite anularea hazardurilor WAR şi WAW printr-
un ingenios mecanism hardware de redenumire a regiştrilor, fiind deci posibilă
execuţia Out of Order a instrucţiunilor şi în aceste cazuri. Aşadar, singurele
hazarduri care impun execuţia In Order, determinată de secvențialitatea
intrinsecă a programului obiect, sunt cele de tip RAW.
În cadrul acestei arhitecturi, detecţia hazardurilor şi controlul execuţiei
instrucţiunilor sunt distribuite, iar rezultatele instrucţiunilor sunt "pasate
anticipat" (forwarding, bypassing) direct unităţilor de execuţie, prin intermediul
unei magistrale comune numită CDB (Common Data Bus). Arhitectura de
principiu este prezentată în Figura 4.5. Aceasta a fost implementată pentru prima

230
dată în unitatea de virgulă mobila (FPP) a calculatorului IBM 360/91, pe baza
căreia se va prezenta în continuare. Staţiile de rezervare (SR) memorează din
SIF (Stivă Instrucţiuni Flotante - pe post de buffer de prefetch aici) instrucţiunea
ce urmează a fi lansată spre execuţie (issue). Execuţia unei instrucţiuni începe
dacă există o unitate de execuţie neocupată momentan şi dacă operanzii sursă
aferenţi (mai precis valorile acestora) sunt disponibili în SR aferentă. Fiecare
unitate de execuţie (ADD, MUL) are asociată o SR proprie. Precizăm că, în
cadrul acestui exemplu, unităţile ADD execută operaţii (instrucțiuni) de adunare
/ scădere, iar unitaţile denumite MUL, operaţii de înmulţire / împărţire.
Modulele LB şi SB memorează datele încărcate din memoria de date (prin
instrucțiuni LOAD) respectiv datele care urmează a fi memorate (STORE).
Toate rezultatele provenite de la unităţile de execuţie şi de la bufferul LB sunt
trimise pe magistrala CDB, împreună cu un identificator (TAG), care semnifică
numele unității de execuție care a produs respectivul rezultat. Bufferele LB, SB
precum şi SR-urile deţin câmpuri de TAG, necesare în controlul hazardurilor de
date între instrucţiuni.

Figura 4.5. Arhitectura lui Robert Tomasulo

231
Există în cadrul acestei unităţi de calcul în virgulă mobilă şi deci în cadrul
mai general al procesării superscalare de tip out of order, trei stagii succesive de
procesare a instrucţiunilor şi anume:
1) Startare (Dispatch) - aducerea unei instrucţiuni din SIF (bufferul de
prefetch) într-o staţie de rezervare. Aducerea se va face numai dacă există o SR
disponibilă (neocupată). Dacă valorile operanzilor aferenţi se află în FPR (setul
de regiştri generali), vor fi aduşi în SR corespunzătoare. Dacă instrucţiunea este
de tip LOAD / STORE, va fi încărcată într-o SR numai dacă există un buffer
(LB sau SB) disponibil (neocupat). Dacă nu există disponibilă o SR sau un
buffer LB/SB, rezultă că avem un hazard structural şi, în consecință,
instrucţiunea va aştepta până când aceste resurse se eliberează.
2) Execuţie - dacă valoarea unui operand nu este disponibilă în registrul
sursă, prin monitorizarea magistralei CDB de către SR ("snooping" - spionaj), se
aşteaptă respectivul operand de la unitatea de execuție care îl va produce. În
momentul în care unitatea de execuție respectivă va plasa valoarea acelui
operand pe CDB, aceasta va fi preluată de SR (prin forwarding). În această fază
se testează existenţa hazardurilor de tip RAW între instrucţiuni. Când ambii
operanzi devin disponibili, se execută instrucţiunea în unitatea de execuţie
corespunzătoare.
3) Scriere rezultat (WB) - când rezultatul este disponibil în unitatea de
execuție, se înscrie pe CDB, împreună cu TAG-ul respectivei unități şi, de aici,
în FPR sau într-o SR care aşteaptă acest rezultat ("forwarding").
De observat că nu există pe parcursul acestor faze testări pentru hazardurile
de tip WAR sau WAW, acestea fiind eliminate prin însăşi natura algoritmului de
comandă, după cum se va constata imediat. De asemenea, operanzii sursă vor fi
preluaţi de către SR, direct de pe CDB, prin "forwarding", atunci când acest
lucru este posibil. Evident că ei pot fi preluaţi şi din FPR, în cazurile în care nu
vor fi produşi de instrucţiunile din staţiile de rezervare sau din unităţile de
execuţie.
O SR deţine 6 câmpuri cu următoarea semnificaţie:
OP - reprezintă codul operaţiei (opcode) instrucţiunii din SR.
Qj, Qk - codifică pe un număr de biţi numele unității de execuţie (ADD,
MUL etc.) sau numărul bufferului LB (Load Buffer), care urmează să genereze
operandul sursă aferent instrucţiunii din SR. Dacă acest câmp are valoarea

232
NULL, rezultă că operandul sursă este deja disponibil într-un câmp Vi sau Vj al
SR sau că, pur şi simplu, nu este necesar. Câmpurile Qj, Qk sunt pe post de
TAG, adică atunci când o unitate de execuţie sau un buffer LB "pasează"
rezultatul pe CDB, acest rezultat se înscrie în câmpul Vi sau Vj al acelei SR al
cărei TAG coincide cu numărul sau numele unităţii de execuţie sau bufferului
LB care a generat rezultatul. Desigur că, în acest caz, în câmpurile Q ale SR se
va înscrie automat valoarea NULL. Câmpurile Q și V sunt exclusive mutual
(dacă în Q avem NULL, valoarea este în V. Dacă în Q avem o valoarea diferită
de NULL – ex. numele unei unități de execuție – atunci valoarea din V este
invalidă).
Vj, Vk - conţin valorile operanzilor sursă aferenţi instrucţiunii din SR sau
valoarea NULL. Remarcăm din nou că doar unul dintre câmpurile Q respectiv V
sunt valide pentru un anumit operand.
BUSY – indică, atunci când este setat, că SR şi unitatea de execuţie
aferentă sunt ocupate momentan.
Regiştrii generali FPR şi bufferele SB deţin fiecare, de asemenea, câte un
câmp Qi, care codifică numărul unităţii de execuţie care va genera data ce va fi
încărcată în respectivul registru general, respectiv care va fi stocată în memoria
de date. De asemenea, deţin câte un bit de BUSY. Bufferele SB deţin, în plus, un
câmp care conţine adresa de acces, precum şi un câmp care conţine data de
înscris (conținutul unui registru CPU). Bufferele LB conţin doar un bit BUSY,
un câmp de adresă, dar și un câmp în care se va înscrie data citită din memorie.
Valoarea din acest câmp se va înscrie pe magistrala CDB, împreună cu numele
bufferului care a produs-o.
Spre a exemplifica funcţionarea algoritmului, să considerăm în continuare o
secvenţă simplă de program maşină:

Start Execuţie WB
1. LF F6, 27(R1) x x x
2. LF F2, 45(R2) x x
3. MULTF F0, F2, F4 x
4. SUBF F8, F6, F2 x
5. DIVF F10, F0, F6 x
6. ADDF F6, F8, F2 x

233
În continuare prezentăm starea SR şi a FPR în momentul definit mai sus,
adică prima instrucţiune încheiată, a 2-a în faza de execuţie, iar celelalte, aflate
în faza de startare.

Tabelul 4.5. Situaţia staţiilor de rezervare în prima instanţă

Tabelul 4.6.Starea regiştrilor generali în prima instanţă

Din aceste structuri de date implementate în hardware, rezultă, spre


exemplu, că SR ADD1 urmează să lanseze în execuţie instrucţiunea SUBF F8,
F6, F2. Valoarea primului operand (F6) se află deja în câmpul Vj, unde a fost
memorată de pe magistrala CDB ca urmare a terminării execuţiei primei
instrucţiuni (LF). Evident că rezultatul acestei instrucţiuni a fost preluat de pe
CDB în registrul F6, dar şi în bufferul LB1. Al 2-lea operand al instrucţiunii
SUBF nu este încă disponibil. Câmpul de TAG Qk arată că acest operand va fi
generat pe magistrala CDB cu "adresa" LOAD2 (LB2) şi deci această SR va
prelua operandul în câmpul Vk, de îndată ce acest lucru devine posibil.
Preluarea acestui operand se va face de către toate SR care au un câmp de TAG
identic cu LOAD2 (LB2).

234
Să considerăm, spre exemplu, că latenţa unităţilor ADD este de două
impulsuri de tact, latenţa unităţilor MUL este de 10 impulsuri de tact pentru o
înmulţire şi respectiv 40 impulsuri de tact, pentru o operaţie de împărţire.
"Starea" secvenţei anterioare în tactul premergător celui în care instrucţiunea
MULTF va intra în faza WB, va fi următoarea:

Start Execuţie WB
1. LF F6, 27(R1) x x x
2. LF F2, 45(R2) x x x
3. MULTF F0, F2, F4 x x
4. SUBF F8, F6, F2 x x x
5. DIVF F10, F0, F6 x
6. ADDF F6, F8, F2 x x x

În acest moment, starea staţiilor de rezervare şi a setului de regiştri generali


va fi cea prezentată în Tabelele 4.7 respectiv 4.8:

Tabelul 4.7. Situaţia staţiilor de rezervare în a doua instanţă

Tabelul 4.8. Starea regiştrilor generali în a doua instanţă

235
De remarcat că algoritmul a eliminat în mod natural hazardul WAR prin
registrul F6 între instrucţiunile DIVF şi ADDF şi a permis execuţia Out of Order
a acestor instrucţiuni, în vederea creşterii ratei de procesare (ADDF s-a încheiat,
în timp ce DIVF continuă procesarea). Într-adevăr, încheierea instrucțiunii
ADDF F6, F8, F2 înaintea instrucțiunii DIVF F10, F0, F6 este naturală, pentru
că ele ar putea fi independente de date în procesarea reală. Iar prin redenumirea
registrului F6 din instrucțiunea DIVF, acestea au devenit într-adevăr
independente în procesarea dinamică. Cum prima instrucţiune din program s-a
încheiat în acest moment, câmpul Vk aferent SR MUL2 va conţine valoarea
operandului instrucţiunii DIVF, permiţând deci ca instrucţiunea ADDF să fie
independentă de DIVF și să se încheie înaintea instrucţiunii DIVF. Chiar dacă
prima instrucţiune din program nu s-ar fi încheiat la momentul considerat aici,
câmpul Qk aferent SR MUL2 ar fi pointat la unitatea LOAD1 şi deci
instrucţiunea DIVF tot ar fi fost independentă de ADDF. Aşadar, algoritmul prin
"pasarea" rezultatelor către SR de îndată ce acestea devin disponibile, evită
hazardurile WAR. Pentru a pune în evidenţă întreaga "forţă" a algoritmului în
eliminarea hazardurilor WAR şi WAW prin redenumire dinamică a resurselor,
să considerăm bucla următoare:

LOOP: LF F0, 0 (R1) / încărcare din memorie


MULTF F4, F0, F2 / prelucrare
SD 0 (R1), F4 / memorare
SUB R1, R1, #4
BNEZ R1, LOOP

De remarcat secvențialitatea intrinsecă a acestei bucle (încărcare din


memorie – prelucrare – memorare). Prelucrarea (MULTF) este dependentă
RAW de încărcare (LF), iar memorarea (SD) este dependentă de prelucrare
(MULTF). Considerând o unitate de predicţie a branchurilor de tip "branch-
taken", două iteraţii succesive ale buclei se vor procesa ca mai jos (Tabelele
4.9):

Start Execuţie WB
LF F0, 0 (R1) x x

236
MULTF F4, F0, F2 x
SD 0 (R1), F4 x
LF F0, 0 (R1) x x
MULTF F4, F0, F2 x
SD 0 (R1), F4 x

Se observă o procesare de tip "Loop Unrolling" ("netezirea buclei") prin


hardware (Tabelele 4.9). Iterații succesive ale buclei sunt disponibile în bufferul
de prefetch, datorită predicției corecte a branch-ului (BNEZ), care este
preponderent Taken. Instrucţiunea LOAD din a 2-a iteraţie se poate executa
înaintea instrucţiunii STORE din prima iteraţie, întrucât adresele de acces sunt
diferite în câmpurile corespunzătoare din buffere. Ulterior şi instrucţiunile
MULTF se vor putea suprapune în execuţie, ca și instrucțiunile SD de altfel. În
acest caz, de ce nu există confuzie între registrul F0 al instrucțiunii MULTF din
iterația 1 și registrul F0 al instrucțiunii MULTF din iterația 2? Răspunsul este
simplu și este dat de conținutul tabelelor următoare, unde se vede că prima
instrucțiune MULTF așteaptă operandul sursă 1 (numit F0 în program) de la
unitatea hardware LOAD 1, în timp ce a 2-a instrucțiune MULTF îl așteaptă de
la unitatea LOAD 2. De remarcat în acest caz și hazardul de tip WAW prin
registrul F0 între instrucţiunile de LOAD, care s-a eliminat cu ajutorul SR şi a
bufferelor SB şi LB. Așadar, chiar dacă nu există paralelism la nivelul unei
iterații a acestei bucle, există în schimb paralelism la nivelul a două sau mai
multe iterații succesive. Acest paralelism este exploatat de CPU, cu ajutorul
predictorului dinamic de branch-uri și, în consecință, a procesării speculative a
instrucțiunilor din iterații diferite ale buclei de program. Astfel, se vor executa în
paralel instrucțiunile de încărcare – prelucrare – memorare aferente mai multor
iterații succesive. Evident că predicția cu acuratețe ridicată a branch-urilor este
esențială în acest proces.

237
Tabelul 4.9. Contextul procesorului aferent buclei de program

Arhitectura lui Tomasulo are deci avantajele de a avea logica de detecţie a


hazardurilor distribuită şi, prin redenumirea dinamică a resurselor, elimină
hazardurile de tip WAW şi WAR. Acest lucru este posibil pentru că resursele
(registrele) de tip operanzi sursă folosite de instrucțiuni şi aflate în starea
"BUSY", nu se adresează ca nume de regiştri logici, ci ca nume de unităţi de
execuţie ce vor produce în mod dinamic aceste surse. Finalmente se vor înscrie,
desigur, în acești regiștri logici. Tomasulo a fost primul om care a înțeles că
numele de registre logice atribuite de programator (compilator) variabilelor din
programul obiect static (arbitrare în fond!), implică dependențe de date de tip
WAR și WAW care, la rândul lor, implică procesarea secvențială a programului.
De aceea, arhitectura lui redenumește registrele implicate cu numele “viu”,
dinamic, al unităților hardware de execuție care vor produce acele rezultate. Spre
exemplu, el a înțeles că instrucțiunile care îl au pe registrul logic R3 pe post de
sursă, nu au nevoie în fond de conținutul lui R3, ci de valoarea – schimbătoare
în timp – care se va înscrie în acesta la un moment dat! Iar acestei valori el îi
asociază în procesarea hardware, numele unității de execuție care o va produce
și nu numele registrului logic respectiv. În această inovație ingenioasă constă
esența ideii algoritmului, cu beneficii importante asupra timpului de execuție.

238
Algoritmul lui Tomasulo denotă, în modesta opinie a autorului acestei cărți, o
înțelegere profundă a procesării instrucțiunilor, care a fertilizat enorm
proiectarea performantă a microprocesoarelor actuale, cu execuții speculative și
paralele ale instrucțiunilor. În schimb, arhitectura este complexă, necesitând deci
costuri ridicate. Este necesară o logică de control complexă, capabilă să execute
căutări / memorări asociative, cu viteză ridicată. Având în vedere progresele
mari ale tehnologiilor VLSI, variante uşor îmbunătăţite ale acestei arhitecturi se
aplică practic în toate procesoarele superscalare actuale (spre exemplu, pentru
reducerea conflictelor, se folosesc mai multe busuri de tip CDB).
Acest mecanism de forwarding din arhitectura lui Tomasulo, are meritul de
a reduce semnificativ din presiunea la "citire" asupra setului general de regiştri
logici, speculând dependenţele RAW între instrucţiuni.

4.4. O ARHITECTURĂ REPREZENTATIVĂ DE PROCESOR


SUPERSCALAR

Având în vedere ideile de implementare a execuţiilor multiple din


arhitectura lui Tomasulo, o arhitectură superscalară reprezentativă este
prezentată în Figura 4.6. Prin SR am notat staţiile de rezervare aferente unităţilor
de execuţie ale procesorului. Acestea implementează, printre altele, bufferul
"instruction window" necesar procesoarelor superscalare cu execuţie Out of
Order a instrucțiunilor. Numărul optim de locaţii al fiecărei SR se determină pe
bază de simulare, pe baza unor metodologii specifice [Flo03].
Deşi performanţa maximă a unei asemenea arhitecturi ar fi de 6
instrucţiuni/ciclu (câte unități de execuție deține în exemplul considerat), în
realitate, bazat pe simulări laborioase, s-a stabilit că rata medie de execuţie este
situată doar între 1-2 instrucţiuni / ciclu. În sub 1% din cazuri, măsurat pe
benchmark-uri nenumerice, de uz general deci, există un potenţial de paralelism
mai mare de 6 instrucţiuni / ciclu în cazul unei arhitecturi superscalare "pure".
Aceasta se datorează, în primul rând, capacităţii limitate a bufferului de prefetch
care constituie o limitare principială a oricărui procesor, exploatarea
paralelismului între instrucţiuni fiind limitată de capacitatea acestui buffer. În

239
tehnologia actuală acesta poate memora între 8 – 128 de instrucţiuni, capacităţi
mai mari ale acestuia complicând semnificativ logica de detecţie a hazardurilor
RAW după cum am arătat (vezi Paragraful 4.1). Desigur că la această
performanță relativ modestă contribuie și hazardurile din structurile pipeline,
miss-urile în cache-uri, excepțiile de tip Page Fault (v. mecanismul de memorie
virtuală), lipsa paralelismului din program etc. Prezentăm pe scurt rolul
modulelor componente din această schemă tipică de microprocesor superscalar
cu execuții out of order ale instrucțiunilor.
Decodificatorul, după ce decodifică instrucțiunile mașină, plasează
instrucţiunile multiple în SR- urile corespunzătoare (faza de dispatch). O unitate
funcţională poate starta execuţia unei instrucţiuni din SR, imediat după
decodificare, dacă instrucţiunea nu implică dependenţe, operanzii îi sunt
diponibili şi dacă unitatea de execuţie este liberă (issue). În caz contrar,
instrucţiunea aşteaptă în SR până când aceste condiţii vor fi îndeplinite. Dacă
mai multe instrucţiuni din SR-uri sunt simultan disponibile spre a fi executate pe
o anumită unitate funcțională, procesorul o va selecta pe prima din secvenţa de
instrucţiuni. După ce o anumită instrucțiune se execută, unitatea funcțională
respectivă pune rezultatul pe magistrala CDB, împreună cu TAG-ul aferent
(numele ei). De aici, rezultatul se scrie pe o așa numită „ciornă” a procesorului,
numită în jargonul de specialitate buffer de reordonare (ReOrder Buffer – ROB).
Desigur că acest rezultat este totodată preluat de toate SR-urile care au nevoie de
el (care conțin instrucțiuni dependente RAW de instrucțiunea executată). Se
scrie rezultatul, în mod out of order, în ROB și nu în setul de registre generale,
după cum am mai arătat, tocmai pentru a se permite implementarea unui
mecanism de excepții (întreruperi) precise. Altfel, la apariția unei întreruperi,
procesarea instrucțiunilor fiind out of order, s-ar putea ca procesorul să dețină
un context (conținutul registrelor generale etc.) a cărui salvare în stivă ar implica
imprecizii ale procesării la revenirea în programul întrerupt.

240
Figura 4.6. Arhitectura tipică a unui procesor superscalar

Desigur că este necesar un mecanism de arbitrare în vederea accesării


magistralei CDB de către diversele unităţi de execuţie (UE). În vederea creşterii
eficienţei, deseori magistralele interne sunt multiplicate. Prezentăm în Figura 4.7
circulaţia informaţiei într-o structură superscalară complexă, similară cu cea
implementată în cadrul microprocesorului Motorola MC 88110. Setul de regiştri
generali (FILE) este multiplicat fizic, conţinutul acestor seturi fizice este identic
însă în orice moment. Am considerat că UE - urile conţin şi staţiile de rezervare
aferente. Din acest motiv, având în vedere mecanismul de "forwarding"
implementat, comunicaţia între UE şi CDB s-a considerat bidirecţională.

241
Figura 4.7. Multiplicarea magistralelor şi a seturilor de regiştri
Există trei categorii de busuri comune şi anume: busuri rezultat (RB),
busuri sursă (SB) și busuri destinaţie (CDB). N corespunde numărului maxim de
instrucţiuni care pot fi lansate simultan în execuţie. Min (M, P) reprezintă
numărul maxim de instrucţiuni care pot fi terminate simultan. Uzual, se alege M
= P. Există implementate mecanisme de arbitrare distribuite, în vederea
rezolvării tuturor hazardurilor structurale posibile, pe parcursul procesărilor
instrucțiunilor.
Pe bază de simulare se încearcă stabilirea unei arhitecturi optimale (timp
minim de procesare, consum minim de energie electrică etc.) Astfel, se arată că
pentru o rată de fetch şi de execuţie de 4 instrucţiuni, procesarea optimă din
punct de vedere performanţă/cost impune 7 busuri destinaţie, 4 unităţi de
execuţie întregi şi 8 staţii de rezervare pentru unităţile LOAD / STORE. Pentru o
asemenea arhitectură s-ar obţine o rată de procesare de 2.88 instrucţiuni / tact,
măsurat însă pe benchmark-uri cu un puternic caracter numeric, favorizante deci
(Livermore Loops). Ideea de bază este însă că hazardurile structurale se elimină
şi aici prin multiplicarea resurselor hardware, deci fără pierderi de performanţă.
Gradul de multiplicare trebuie însă stabilit prin simulări ample, ori chiar prin
metode teoretice, de natură analitică.

242
Buffer-ul de reordonare (paragraf preluat prin traducere din limba engleză în
limba română, urmată de semnificative revizuiri și completări ulterioare ale
autorului, din cartea sa intitulată: VINȚAN N. LUCIAN – Prediction
Techniques in Advanced Computing Architectures, Matrix Rom Publishing
House, Bucharest, ISBN 978-973-755-137-5, 2007.)
Actualele microprocesoare superscalare cu procesări out of order ale
instrucțiunilor sunt implementate sub forma unor microarhitecturi cu procesări
speculative (prin execuții out of order, branch prediction etc.) ale instrucțiunilor
mașină, care în fiecare ciclu, aduc, decodifică și execută (simultan) mai multe
instrucțiuni independente de date. Ele utilizează algoritmul lui Tomasulo, sau
variațiuni ale acestuia, pentru implementarea execuțiilor out of order, precum și
o structură de date numită ReOrder Buffer (ROB). Desigur că mecanismul de
predicție dinamică a branch-urilor este esențial în acest context, întrucât permite
existența, în fereastra de procesare curentă, a mai multor basic-block-uri, cu
influențe benefice asupra gradului de paralelism la nivel de instrucțiuni (într-un
singur basic-block paralelismul este mai limitat.) Acest model de procesor
superscalar extinde ideea scheduling-ului dinamic al instrucțiunilor, introducând
posibilitatea execuției speculative a acestora. Arhitectura lui Tomasulo trebuie
să separe terminarea out of order a instrucțiunilor (nivelul Write Back - WB din
structurile pipeline) de terminarea lor completă (scrierea rezultatelor
instrucțiunilor în registrele logice sau în memorie), care trebuie să fie una în
ordinea instrucțiunilor din programul static, pentru a permite un mecanism
precis al excepțiilor, după cum am mai subliniat. Prin această separare, o
instrucțiune poate trimite în mod speculativ rezultatul său altor instrucțiuni
dependente RAW de ea, situate în stațiile de rezervare, fără însă a-l scrie în
contextul logic al CPU (registre interne logice și memorie). După ce
instrucțiunea nu mai este speculativă (out of order), așadar după faza sa de WB,
ea poate să actualizeze rezultatul în contextul logic al task-ului CPU. Această
ultimă fază de procesare, in order, se numește în literatura de specialitate
Commit. Prin introducerea acestei ultime faze in order, se asigură existența unui
mecanism precis de tratare a excepțiilor, chiar în condițiile execuției out of order
a instrucțiunilor. Așadar, acest buffer de reordonare permite procesarea out of
order a instrucțiunilor, dar tot el, re-ordonează instrucțiunile, după cum îi și

243
spune denumirea, adică determină terminarea instrucțiunilor în ordinea lor
inițială, cea dată de programul static.
Adăugarea acestei ultime faze numite commit necesită buffere
suplimentare (ciorna!) în vederea memorării temporare, pe parcursul fazei WB,
a rezultatelor instrucțiunilor procesate speculativ (out of order). Structura ROB
implementează tocmai aceste buffere. Structura ROB este, de asemenea,
utilizată pentru transmiterea speculativă (pentru că se face înainte de faza
Commit!) a rezultatelor aferente instrucțiunilor procesate out of order, către
stațiile de rezervare care au nevoie de acestea (conțin instrucțiuni dependente
RAW de cele care au produs rezultatele în ROB). Stațiile de rezervare
memorează instrucțiunile pe durata de timp dintre faza de issue și începutul fazei
de execuție. În această arhitectură, structura ROB implementează și funcția de
redenumire dinamică a regiștrilor, după cum se va arăta în continuare. Figura 4.8
arată structura de principiu a unui procesor superscalar cu execuții Out of Order
(OoO) ale instrucțiunilor, incluzând și structura ROB. Figura 4.9 prezintă
structura detaliată a unui ROB cu o capacitate de 128 de locații, tipică pentru
multe procesoare comerciale. Acesta implementează o structură de tip FIFO
(First In First Out) circular, cu posibilități de căutare asociativă (după conținut).
După ce rezultatul instrucțiunii din vârful ROB se va înscrie în registrul logic
destinație respectiv, această locație va ajunge disponibilă în coada ROB, urmând
a fi reutilizată de o altă instrucțiune care va fi decodificată. După cum se poate
observa în Figura 4.9, fiecare intrare în ROB conține 4 câmpuri: OPCODE,
R_DST, VAL și RDY. Câmpul OPCODE indică tipul instrucțiunii (ALU, cu
referință la memorie, ramificație etc.) Câmpul R_DST arată numărul registrului
destinație al instrucțiunii pentru instrucțiuni ALU sau LOAD, respectiv adresa
de memorie pentru instrucțiunile STORE (unde trebuie scrisă data în memorie).
Câmpul VAL este utilizat pentru memorarea rezultatului în ROB, până la faza de
Committ, când acesta se va scrie în registrul destinație. Bitul RDY indică dacă
rezultatul instrucțiunii din locația ROB este sau nu este disponibil. Astfel
structura ROB ar putea înlocui acele buffere pentu instrucțiunile de tip Store, din
arhitectura lui Tomasulo.

244
Figura 4.8. Schema bloc a unui microprocesor superscalar cu execuții OoO

245
Figura 4.9. Structura ROB
Fiecare stație de rezervare (SR) conține următoarele câmpuri:

• Op – codul instrucțiunii - operation code (opcode).


• Qj, Qk – pot conține adresele locațiilor din ROB care vor memora
valorile operanzilor sursă ale instrucțiunii. O valoare NULL într-un
câmp Q semnifică faptul că valoarea operandului sursă este deja
disponibilă într-unul din câmpurile Vj, Vk, sau că nu este necesară;
numim aceste câmpuri Q, operanzii potențiali (disponibili într-un viitor
imediat) ai instrucțiunii.
• Vj, Vk – în cazul în care conțin valori diferite de NULL, acestea
reprezintă valorile operanzilor sursă ai instrucțiunii; pentru instrucțiunile
de încărcare din memorie/scriere în memorie, câmpul Vj este utilizat
pentru memorarea indexului adresei (offset); numim aceste câmpuri V,
operanzii actuali ai instrucțiunii, pentru că aici se memorează valorile
surselor instrucțiunii.

246
• Addr – memorează adresa efectivă a instrucțiunilor cu referire la
memorie.
• Dest – reprezintă adresa intrării din ROB în care va urma să se scrie
rezultatul instrucțiunii respective (rezultat produs de unitatea de execuție
corespunzătoare, atașată SR).
• Busy – un bit care indică dacă o stație de rezervare este, sau nu este,
disponibilă.

Fiecare registru logic conține un câmp notat Qi care – dacă nu conține


valoarea NULL – indică adresa intrării ROB care conține, sau va conține,
valoarea care, pe timpul fazei de Commit, se va înscrie din ROB în respectivul
registru logic. În cazul în care Qi conține valoarea NULL, valoarea din
respectivul registru logic este validă, deci poate fi utilizată (citită) de orice altă
instrucțiune. Să considerăm pentru concretețea prezentării instrucțiunile
succesive SUB R9, R11, R12 ... ADD R7, R9, R10 (dependente RAW prin R9).
Exceptând faza de aducere a instrucțiunii din memorie, celelalte 6 faze succesive
de procesare pipeline ale instrucțiunilor sunt următoarele (vom exemplifica mai
jos pentru instrucțiunea curentă ADD R7, R9, R10):

1. Decode & Dispatch – decodifică și trimite instrucțiunea (ADD R7, R9, R10)
în stația de rezervare. Dacă toate stațiile de rezervare sunt ocupate sau dacă
ROB-ul este plin (spre exemplu, datorită unei instrucțiuni de LOAD cu miss
în cache, care a ajuns în vârful (head) ROB, unde va rămâne pentru mai mult
timp), atunci această fază va fi blocată, până la eliberarea ambelor structuri
implicate. Dacă există cel puțin o stație de rezervare liberă și coada (tail)
ROB este liberă, instrucțiunea va fi trimisă în stația de rezervare. Desigur că
este necesar un algoritm dinamic de rutare, care să trimită instrucțiunea în
stația de rezervare potrivită (dacă unitățile de execuție sunt neomogene).
Bitul Busy al stației de rezervare alocate este setat, iar bitul RDY al locației
din coada ROB (Tail_ROB) este resetat. În câmpul OPCODE al Tail_ROB
se scrie codul instrucțiunii decodificate (ADD la noi), iar în câmpul R_DST
se scrie numărul registrului (logic) destinație al acesteia (R7 în exemplul
nostru). Totodată, în câmpul Dest al stației de rezervare care memorează
instrucțiunea decodificată se va înscrie adresa locației din Tail_ROB (126 în

247
exemplul nostru, având în vedere instanța ROB din Figura 4.9). Astfel, pe
timpul procesării dinamice se va produce redenumirea acestui registru logic
destinație (R7), cu adresa locației Tail_ROB curente (126). Tot în această
fază se caută în setul de regiștri logici, valorile operanzilor sursă aferenți
instrucțiunii decodificate (R9, R10 în exemplul considerat). Dacă aceste
valori sunt disponibile, se vor încărca în câmpurile V ale stației de rezervare
și instrucțiunea ar putea trece în faza următoare (dacă există o unitate de
execuție liberă). Dacă însă valorile surselor nu sunt disponibile, atunci se
caută asociativ în structura ROB, mai precis în câmpurile R_DST ale
acestuia, după registrul sursă 1 respectiv 2 al instrucțiunii curente. Se caută
locațiile din ROB care au valoarea din câmpul R_DST= numărul registrului
sursă 1 respectiv cu numele registrului sursă 2 din ROB. În cazul în care, spre
exemplu, în ROB există mai multe locații cu proprietatea R_DST= numărul
registrului sursă 1 (hit-uri multiple), se va alege ultima locație, întrucât
alocările în ROB (prin Tail_ROB) s-au făcut în ordinea aducerii și
decodificării instrucțiunilor. Dacă valoarea operandului sursă este disponibilă
în ROB (bitul RDY=1), atunci aceasta este scrisă în unul din câmpurile Vj /
Vk ale stației de rezervare. Dacă însă RDY=0, adresa locației respective din
ROB va fi scrisă într-unul din câmpurile Qj / Qk ale stației de rezervare
aferente instrucțiunii. În cazul exemplului considerat, dacă se caută în ROB
după R9 în câmpul R_DST și se găsește locația 56, cu RDY=1 (v. Figura
4.9), se va scrie valoarea corespunzătoare VAL=2546 într-unul din câmpurile
Vj /Vk ale stației de rezervare aferente instrucțiunii ADD R7, R9, R10, notată
Q_SR (ADD). Dacă RDY ar fi fost 0 logic, se înscria 56 Q_SR (ADD),
pentru ca stația de rezervare să capteze ulterior valoarea de la unitatea de
execuție care o va produce (acea unitate a cărei stație de rezervare are în
câmpul Dest valoarea 56).
2. Issue – Dacă un operand sursă al instrucțiunii nu este încă disponibil în SR,
aceasta va monitoriza magistrala CDB pentru a capta (într-unul din câmpurile
sale V) valoarea acelui operand la momentul potrivit, adică atunci când va fi
produsă de către o altă instrucțiune. Dacă ambele valori ale operanzilor sursă
sunt disponibile în SR și dacă există o unitate de execuție liberă, atunci
instrucțiunea respectivă va fi trimisă înspre acea unitate. Prin această
întârziere a execuției instrucțiunilor până când ambii operanzi sursă sunt

248
disponibili, algoritmul respectă secvențialitatea impusă de dependențele de
date de tip RAW între instrucțiuni. În schimb, dependențele de tip WAR și
WAW sunt înlăturate prin redenumirea regiștrilor logici.
3. Execute (ALU) – unitățile funcționale vor executa instrucțiunile de tip ALU.
Evident că în cazul instrucțiunilor cu referire la memorie (Load/Store), în
această fază se calculează adresa datei din memorie. În cazul unei instrucțiuni
de salt condiționat, de obicei în această fază se calculează adresa țintă
(target) a acesteia. De remarcat că în această fază instrucțiunile se vor
executa out of order, datorită latențelor diferite ale unităților de execuție
(care pot fi pipeline-izate). Evident că acest fapt determină ca și fazele
următoare, împreună cu faza Issue de altfel, dar cu excepția fazei Commit, să
se proceseze out of order.
4. Data Memory – este activă numai pentru instrucțiunile cu referire la memoria
de date (procesor cu ISA de tip RISC). Pe durata acestei faze data este scrisă
în memoria de date (cazul unei instrucțiuni Store), respectiv încărcată în
registrul destinație (Load). Adresa locației din memoria de date a fost
calculată în faza anterioară (ALU).
5. Writeback – când rezultatul procesat de o unitate ALU este disponibil, acesta
va fi plasat pe magistrala CDB și scris în locația din ROB (câmpul VAL)
adresată de cîmpul Dest al stației de rezervare aferente. Evident că bitul RDY
se va pune pe 1 logic, iar bitul Busy din SR se va reseta. În cazul exemplului
nostru, când instrucțiunea ADD se va fi terminat (WB), rezultatul ei se va
înscrie la locația 126 din ROB care, foarte probabil, nu va mai fi la acel
moment Tail_ROB (va fi urcat în ROB între timp). Prin mecanismul de
forwarding implementat (CDB SR), rezultatul produs va fi, de asemenea,
scris într-unul din câmpurile Vj / Vk ale tuturor stațiilor de rezervare care îl
așteaptă. În cazul unei instrucțiuni de tip Store, dacă valoarea de memorat
devine disponibilă, se va înscrie în cîmpul VAL al locației din ROB alocate
pentru acea instrucțiune. Dacă rezultatul care trebuie memorat în memoria de
date nu este disponibil, magistrala CDB va fi monitorizată și când rezultatul
devine disponibil, va fi înscris în câmpul VAL al intrării ROB
corespunzătoare.
6. Commit – în mod normal această fază se declanșează atunci când
instrucțiunea a ajuns în Head_ROB iar rezultatul său este disponibil

249
(RDY=1). În acest caz, rezultatul din câmpul VAL va fi înscris în registrul
logic destinație indicat de câmpul R_DST (R7 în cazul exemplului
considerat) din locația ROB sau într-o locație din memoria de date (cazul
unei instrucțiuni Store). Practic, în acest moment registrul destinație și-a
încheiat redenumirea cu o locație din ROB, redevenind el însuși („Ca să pot
muri liniștit, pe mine/ Mie redă-mă!”, scria Eminescu în poezia sa, Odă în
metru antic). După acest proces, instrucțiunea este evacuată din ROB
(registrul astfel redenumit “moare, trecând în neant”) iar locația Head_ROB
va trece în Tail_ROB (FIFO circular), de unde va fi alocată unei alte
instrucțiuni decodificate (astfel, locația aceasta din ROB va „da o nouă viață”
unui alt registru logic). Așadar, procesarea in order a fazei Commit este
garantată de faptul că faza de dispatch se execută in order (evident, având în
vedere că aducerea de instrucțiuni se face in order, într-o structură FIFO
numită buffer de pre-fetch). Dacă în Head_ROB se află o instrucțiune de
branch incorect predicționată, structura ROB se va goli (instrucțiunile de
acolo sunt eronat speculative) iar procesarea este restartată, de pe ramura
corectă a branch-ului.

Așa cum poate fi remarcat, în acest caz al arhitecturilor cu procesări


speculative ale instrucțiunilor este foarte important momentul în care se produc
actualizările. Execuția speculativă (out of order) a instrucțiunilor este posibilă
tocmai datorită structurii ROB, pentru că pe această “ciornă” a CPU se scriu
rezultatele instrucțiunilor speculative. În schimb, scrierea „pe curat”, în setul de
registre logice sau în memoria de date, este posibilă doar în momentul în care
instrucțiunea respectivă nu mai este speculativă (ci se va procesa in order).
Capacitatea ROB este esențialmente determinată de numărul maxim de
instrucțiuni aflate în diferite stagii de procesări paralele, mai precis între fazele
de Dispatch și Commit. Desigur, rolul predicției dinamice a branch-urilor este
foarte important, pentru că predicțiile corecte oferă posibilitatea exploatării
paralelismului la nivel de instrucțiuni din cadrul mai multor basic-block-uri
speculativ aduse în CPU.
Pentru o mai bună înțelegere a funcționării ROB se vor aborda în continuare
câteva întrebări punctuale, care vor relua unele dintre problemele deja discutate
(repetitio mater studiorum!).

250
1. Ce se întâmplă în <Tail_ROB> după decodificarea instrucțiunii curente?

Dacă intrarea <Tail_ROB> este liberă, instrucțiunea decodificată se va aloca


acestei intrări. Astfel, în câmpul OPCODE se va înscrie codul instrucțiunii, în
câmpul R_DST se va înscrie numărul registrului logic destinație, iar în bitul
RDY se va înscrie 0. Valoarea din VAL este nerelevantă în acest moment
(evident, întrucât instrucțiunea nu a produs încă rezultatul.)

2. În momentul în care o instrucțiune de tip ALU va fi introdusă într-o stație de


rezervare, ce s-ar putea scrie în câmpurile Q?

Dacă operanzii sursă ai instrucțiunii vor fi disponibili în setul de registre


generale, atunci în câmpurile Qj și Qk se vor înscrie valorile NULL, iar valorile
respective (ale operanzilor sursă) se vor înscrie în câmpurile V din SR. În
câmpul Dest al stației de rezervare se va înscrie adresa locației din Tail_ROB.
(Ulterior, în faza WB, rezultatul instrucțiunii se va înscrie aici, la această adresă
din ROB.) Dacă cel puțin un operand sursă nu este disponibil în setul de registre
(să presupunem că operandul 2 nu este disponibil), atunci se face o căutare
asociativă în ROB de genul Instruction_code[Source_Reg2]=R_DST[ROB]?
Adresa din ROB a primei locații care răspunde pozitiv acestei căutări se va
introduce în câmpul Qk. De ce a primei locații (mai apropiată de Tail_ROB) și
nu a alteia? Pentru că instrucțiunile au fost alocate în ROB in order, pe durata
fazei de decode (reamintim că ROB este o structură de tip FIFO). Cum
operandul nu a fost găsit în setul de registre, un miss în procesul de căutare
asociativă nu este posibil.

3. Ce se întâmplă în ROB pe parcursul fazei WriteBack?

Instrucțiunea va înscrie rezultatul în câmpul VAL din ROB, la adresa dată de


câmpul Dest al stației de rezervare. Evident că bitul RDY al locației din ROB se
va pune pe 1. Reamintim faptul că acest rezultat pus pe magistrala CDB de către
un ALU va fi preluat într-un câmp V de toate SR-urile care au într-unul din
câmpurile Q valoarea Dest a stației de rezervare. După acest proces, în câmpul Q

251
implicat se scrie NULL. În cazul unei instrucțiuni Store, data se va înscrie în
câmpul V al locației din ROB (aceasta va fi scrisă în memoria de date in order,
pe durata fazei Commit).

4. Ce condiții trebuie îndeplinite pentru startarea fazei Commit?

Faza Commit startează numai dacă instrucțiunea este plasata în Head_ROB și


dacă rezultatul ei este disponibil, adică bitul RDY din Head_ROB este 1 logic.
Practic, în această fază se scrie valoarea locației din ROB în registrul destinație
sau în memoria de date (cazul unei instrucțiuni Store). Dacă în faza de Commit
(in order!) a apărut un eveniment de excepție (întrerupere externă,
derută/deviere etc.), conținutul ROB se golește, pentru că este speculativ și ar
împiedeca tratarea corespunzătoare a excepției prin rutina de tratare.

5. Considerând fazele Dispatch, Issue, Exec, Data-Mem, WriteBack, Commit,


care dintre aceste se vor procesa Out of Order. Justificare.

Fazele Issue, Exec, Data-Mem și WriteBack se vor procesa Out of Order pentru
că în faza WriteBack se scriu rezultatele numai în ROB (pe “ciornă”) nu și în
regiștrii generali (pe „curat”). Acest fapt este datorat în principal latențelor
diferite ale unităților de execuție neomogene. Din ROB, rezultatul se va scrie in
order în registrul destinație, pe durata fazei Commit. Dacă în faza de Commit se
constată apariția unui eveniment de excepție sau a unui branch predicționat
eronat etc., conținutul speculativ al ROB este golit. Astfel, toate instrucțiunile
procesate speculativ (datorită predicției eronate a branch-ului sau a procesării
out of order a instrucțiunilor) sunt, practic, șterse din ROB. Asfel se
implementează procesul de recovery, în cazul unei speculații (ex. branch
predicționat greșit).

6. Ce garantează în această implementare faptul că, într-adevăr, faza de Commit


se procesează In Order, așa cum se dorește?

Garanția este dată de faptul că faza de Decode & Dispatch este in order în mod
nativ (fiind succesivă instruction fetch-ului care este in order). În această fază de

252
Decode & Dispatch, se alocă instrucțiunea decodificată curent în Tail_ROB.
Cum ROB este o structură de date de tip FIFO și cum condiția pentru startarea
Commit este ca instrucțiunea să ajungă în Head_ROB și rezultatul ei să fie
disponibil (RDY=1), rezultă în mod clar că faza Commit se procesează in order
și nu altfel.

7. Care este rolul câmpului destinație din structura ROB?

Câmpul R_DST din ROB conține valoarea aferentă registrului destinație al


instrucțiunii (dacă aceasta deține așa ceva) sau adresa de memorie, pentru o
instrucțiune de tip Store. Pe durata fazei Commit, valoarea din ROB se scrie în
registrul destinație codificat (adresat, indexat) tocmai de acest câmp R_DST. Să
nu uităm faptul că pe parcursul fazei de Dispatch, dacă valoarea a cel puțin un
operand sursă nu este disponibilă în registrul sursă, se caută în ROB în mod
asociativ dacă Instruction_code[Source_Reg]=R_DST[ROB] (v. întrebarea 2).

Ca o alternativă la structura ROB, există implementate tehnici hardware de


redenumire dinamică a registrelor logice (dynamic register renaming). Acestea
nu folosesc structuri ROB, ci un set extins de registre fizice (invizibile pentru
programator). Pe parcursul fazei de Dispatch registrul destinație al instrucțiunii
este redenumit cu un registru fizic disponibil din cadrul setului extins. În acest
mod se evită secvențialitățile impuse de dependențele de date de tip WAR
(Write After Read) și WAW (Write After Write) între instrucțiuni. După faza
ultimă, de Commit, acest registru fizic redevine liber, putând fi utilizat pentru
redenumirea unui alt registru logic (destinație). Conversia fizic-logic și reciproc
se face prin intermediul unei simple tabele de mapare, implementate în
hardware. În alte implementări, CPU așteaptă după de-alocare, până în
momentul în care o altă instrucțiune va modifica acel registru destinație. În acest
ultim caz, un registru s-ar putea să rămână alocat mai mult timp decât ar fi strict
necesar (cazul procesorului MIPS R10000). Procesoarele MIPS
R10000/120000, Alpha 21264, Pentium III, IV etc. implementează redenumirea
dinamică a registrelor prin adăugarea a până la 80 de registre fizice
suplimentare.

253
Totuși, în ultimii ani există opinii ale unor cercetători care susțin că
microprocesoarele viitorului vor trebui să renunțe la implementarea unei
structuri ROB centralizate și să implementeze tehnici mai performante,
distribuite, precum, spre exemplu, cele numite checkpoint-oriented processing
models. Checkpointing-ul constă, în principiu, în salvarea, la anumite momente
ale procesării, contextului logic al procesorului, obținut în urma unei procesări
in order, așa încât să fie posibilă, la nevoie, restaurarea sa la o stare anterioară,
cunoscută. Structura ROB limitează scalabilitatea arhitecturii pentru că nu
permite lărgirea semnificativă a ferestrei curente de instrucțiuni dinamice,
datorită centralizării sale. Dacă, spre exemplu, o instrucțiune mare consumatoare
de timp (Load cu miss în cache, înmulțiri/împărțiri etc.) atinge Head_ROB și
mai necesită încă foarte mulți cicli până să se încheie, structura ROB se poate
umple în timp scurt, nemaipermițând la un anumit moment dat decodificarea
niciunei alte instrucțiuni. Practic, într-o asemenea situație, procesorul va stagna
procesarea instrucțiunilor, până la rezolvarea instrucțiunii din Tail_ROB. Ideea
de esență a checkpointing-ului constă în faptul că uneori este mai eficient să se
reconstruiască starea logică a procesorului (de fapt a procesului rulat de acesta),
decât să se memoreze în mod explicit starea CPU în ROB, pentru fiecare
instrucțiune dinamică. Checkpointing-ul memorează această stare logică numai
în anumite puncte selectate ale execuției programului (branch-uri eronat
predicționate, Load-uri cu miss în cache etc.) și va restaura starea pentru
instrucțiuni individuale numai când va fi necesar. Cercetătorii au arătat că o
asemenea proiectare este mai scalabilă și mai eficientă decât una bazată pe un
ROB centralizat. O asemenea soluție permite mii de instrucțiuni dinamice în
curs de procesare, simultan. Cercetătorii unui grup de la Universitatea
Politecnica Catalunya din Barcelona, conduși de profesorul Mateo Valero, în
colaborare cu Intel Research, au dezvoltat această idee în detalii - Kilo-
Instruction Processors Project. Provocarea principală constă în proiectarea unei
asemenea arhitecturi fără necesități prea mari de resurse suplimentare (buffere
pentru instrucțiuni Load/Store, registre fizice etc.) și deci cu posibilitatea unor
frecvențe de tact ridicate.

Microprocesorul superscalar Grid ALU Processor

254
Procesorul superscalar Grid ALU Processor (GAP) a fost proiectat și
implementat la Universitatea din Augsburg, în grupul de cercetare condus de
profesorul Theo Ungerer [Uhr10]. Grid ALU Processor este un procesor
superscalar cu execuții in order ale instrucțiunilor, care deține o matrice
reconfigurabilă de ALU-uri. Această matrice (numită Grid Array) – care
reprezintă principala noutate a structurii – procesează instrucțiunile în mod
asincron, pe principiul fluxului de date (data driven, deci nu instruction driven,
cum se face în majoritatea procesoarelor), adică o instrucțiune aritmetică se
execută de îndată ce operanzii ei sursă devin disponibili. În acest sens, o
instrucțiune dependentă RAW de o altă instrucțiune situată în această matrice, se
va asigna unei ALU situate în rândul imediat următor, astfel încât să preia
rezultatul instrucțiunii de care depinde, de îndată ce acesta este disponibil.
Astfel, asteptările după operanzii sursă sunt reduse la maximum. Fiecare coloană
a matricii corespunde unui registru arhitectural (de aceea se configurează, de
obicei, 32 de coloane, aferente regiștrilor R0-R31). O instrucțiune este automat
asignată coloanei care corespunde registrului ei destinație.
Așadar, bazele arhitecturii GAP constau în execuția asincronă a
instrucțiunilor, realizată în matricea reconfigurabilă de unități aritmetico-logice.
Unitatea de aducere a instrucțiunilor este similară cu cea a unui procesor
superscalar, având o capacitate de 4/8/16 instrucțiuni simultane. Nu este nevoie
de un buffer pentru instrucțiunile care nu pot fi trimise spre unitatea de
decodificare din cauza dependențelor de date, deoarece acestea sunt rezolvate de
către unitatea de decodificare, de cea de configurare și de matricea de ALU-uri.
Unitatea de decodificare convertește instrucțiunile în operații, pe care le
plasezează în matricea de unități ALU, astfel încât o instrucțiune dependentă
RAW de o alta, situată în aceeași matrice, sa fie atribuita unei ALU de pe rândul
următor. Unitatea de configurare va scrie valorile generate de unitatea de
decodificare în regiștrii de configurare din matricea ALU. Acești regiștri de
configurare ai matricii memorează practic fluxul de operatii ce trebuie realizate,
fiind impărțiți pe nivele de configurare. Cu cât numărul acestor nivele crește, va
crește și flexbilitatea configurării matricii de ALU-uri.

255
Figura 4.9.b. Arhitectura procesorului GAP (preluată din [Uhr10])

După aceasta, unitățile aritmetico-logice vor începe să execute


instrucțiunile în mod asincron, pe principiul fluxului de date (data-flow). Acest
proces de execuție are loc cât timp nu se ajunge la o instrucțiune de salt și
matricea nu este plină de instrucțiuni în curs de execuție. Dacă matricea se va
umple, atunci evident că nu mai pot fi atribuite noi instrucțiuni în ea. Când
această matrice se va goli, fie și parțial, ca urmare a execuției instrucțiunilor
situate aici, front-end-ul va începe din nou să o umple cu următoarele
instrucțiuni. Dacă în execuție apare o instrucțiune de salt, se diferențiază două
cazuri. Dacă adresa de salt este cea a primei instrucțiuni din matrice (va fi
procesată o bucla de program), nu mai este nevoie să se aducă din nou, din
memorie, corpul buclei, pentru că acesta este deja memorat în matrice. Celelalte
stagii pipeline stagnează atât timp cât se execută o buclă, economisind astfel
energie și crescând performanța. Dacă însă adresa de salt nu este una de început
a unei bucle de program, atunci matricea ALU va fi golită și, apoi, umplută din
nou cu instrucțiunile precedente celei de salt condiționat, situată la finele buclei.
În comparație cu un procesor superscalar sau cu unul de tip VLIW/EPIC,
procesorul GAP are anumite avantaje. În primul rând, este posibil ca front-end-
ul să trimită spre execuție chiar și instrucțiuni cu dependențe de date de tip
RAW, prin poziționarea lor în rânduri diferite ale matricii ALU, în funcție de
dependențele concrete dintre acestea. Al doilea avantaj este dat de modul în care

256
se tratează buclele de program. Într-un procesor superscalar in order clasic,
instrucțiunile buclei sunt aduse din memorie, apoi decodificate și executate,
acest proces repetându-se pentru fiecare iterație a buclei. Dacă nici nu se
predicționează salturile condiționate, atunci vor exista și cicli de așteptare
(acoperirea BDS) între iterațiile buclei. Ca alternativă, procesorul GAP plaseaza
însă toate instrucțiunile în matricea ALU, până când apare în execuție o
instrucțiune de salt. Așa cum am mai precizat, dacă adresa de salt, care în mod
normal ar goli matricea ALU, este egală cu cea a primei instrucțiuni care a fost
plasată în matrice, atunci se va efectua o optimizare: regiștrii din vârful matricii
vor primi valorile produse de instrucțiunile de după execuția saltului și execuția
va continua fără nicio întârziere. Deoarece unitățile din front-end pot fi oprite în
timpul execuției unei bucle, se poate reduce astfel, în mod semnificativ,
consumul de energie electrică.
Un alt avantaj constă în execuția asincronă a instrucțiunilor din matricea
de unități ALU. Presupunând că în unele operații execuția nu durează un întreg
ciclu CPU (sau un număr întreg de cicli), rezultă că există perioade de timp care
pot fi și ele folosite în procesare, spre deosebire de modelul sincron, unde se
pierd. Prin execuție asincronă în cadrul matricii ALU și cu ajutorul unei metode
de sincronizare speciale, se folosesc și acești timpi pentru a executa
instrucțiunile dependente RAW, cu latențe de mai puțin de un ciclu per
instrucțiune. Spre exemplu, dacă avem m instrucțiuni dependente RAW, într-un
procesor superscalar tipic, după aducerea și decodificarea acestora în n cicli
CPU, vom avea un timp total de execuție de n + m * 1 cicli/instrucțiune. În
schimb, procesorul GAP poate executa acest flux de instrucțiuni în doar n + m *
0.75 cicli/instrucțiune, fiind deci mai rapid (s-a considerat că execuția unei
instrucțiuni pe modelul asincron durează 0.75 perioade de tact).
În fine, alt avantaj al acestei arhitecturi este acela că nu necesită software
special pentru plasarea instrucțiunilor în matricea ALU sau pentru calcularea
configurațiilor matricii, precum alte arhitecturi reconfigurabile similare. Aceste
procese sunt realizate integral de unitățile de decodificare și de configurare, care
sunt integrate în structurile pipeline de procesare. Astfel, microprocesorul GAP
poate executa cod standard, compilat pentru procesoare superscalare, fară a mai
fi nevoie de recompilare. Parametrii de configurare ai arhitecturii GAP sunt
urmatorii: numărul de rânduri/coloane ale matricii ALU, numărul de nivele de

257
configurare ale acesteia și dimensiunea, respectiv modul de organizare al cache-
ului. A determina dimensiunile optimale pentru această matrice, inclusiv
configurarea ei internă, dar și pentru arhitecturile cache aferente GAP, este un
proces important de optimizare.
De subliniat că există mai multe simulatoare software extrem de utile
studiului microprocesoarelor superscalare cu procesări out of order ale
instrucțiunilor. Recomandăm în acest sens, în special, simulatorul numit SatSim
('Superscalar Architecture Trace Simulator') utilizat și de noi, în lucrările
practice de laborator pentru studenții din domeniul “Calculatoare și tehnologia
informației” [Flo03]. Prin intermediul unor asemenea simulatoare software se
poate aprofunda procesarea paralelă a instrucțiunilor, mecanismele de
redenumire dinamică a registrelor, modul de procesare a instrucțiunilor prin
intermediul structurilor pipeline de procesare, limbajul de asamblare aferent etc.

4.5. PROBLEME SPECIFICE INSTRUCŢIUNILOR DE RAMIFICAŢIE


ÎN ARHITECTURILE MEM

Să considerăm o secvenţă de program care se execută astfel:

PC=743644 : I1
I2
I3
I4
I5 (branch condiţionat)
PC=342234 : I6
I7
I8
I9 (branch condiţionat)

Dacă am presupune că BDS-ul este de doi cicli, procesarea secvenţei de


mai sus pe un procesor superscalar (sau VLIW) cu procesare In Order, care
poate aduce şi executa maximum 4 instrucţiuni / ciclu, s-ar desfăşura ca în

258
Tabelul 4.10. Se observă că pentru a compensa BDS-ul de 10 instrucţiuni, ar
trebui introduse în acesta, 10 instrucţiuni anterioare instrucţiunii I5 şi care să nu
o afecteze. Acest lucru este practic imposibil, de unde rezultă că asemenea
metode sunt inefective pentru procesoarele superscalare sau/și super-pipeline.
Din acest motiv predicţia hardware a branch-urilor pe baza unui BTB sau a unei
scheme corelate pe două nivele, este implementată deseori în aceste procesoare.
Pentru ca metodele de predicţie prezentate pe larg în Capitolul 3 să funcţioneze
şi în acest caz, sunt necesare câteva completări, datorate aducerii şi execuţiei
multiple a instrucţiunilor.

Tabelul 4.10. Efectul BDS-ului într-un procesor superscalar

Se va considera că o locaţie a memoriei cache de instrucţiuni conţine 4


câmpuri. Fiecare câmp, la rândul său, va fi format din: codul instrucţiunii
respective, tag-ul format din biţii de adresă cei mai semnificativi, indexul de
succesor (IS) şi indexul branch-ului în locaţie (IBL). Sub-câmpul IS indică
următoarea locaţie din cache - ul de instrucţiuni (I-Cache) predicţionată a fi
adusă şi respectiv prima instrucţiune care trebuie executată din cadrul acestei
locaţii. Sub-câmpul IBL indică dacă există sau nu o instrucţiune de salt în locaţia
din cache şi dacă da, locul acesteia în cadrul locaţiei. Pentru secvenţa anterioară
de program, intrările în memoria I-Cache se prezintă ca în Figura 4.8 (IBL s-a
exprimat în binar).

259
Figura 4.10. Structura intrărilor I-Cache într-un procesor superscalar
Aşadar sub-câmpul IS pointează spre prima instrucţiune care trebuie
executată în cadrul unei locaţii din cache, iar sub-câmpul IBL spre o eventuală
instrucţiune de salt din cadrul aceleiaşi locaţii, predicţionată că se va face.
Adresa completă a instrucţiunii la care se va face saltul este conţinută în tabelele
de predicţie corespunzătoare.
În continuare, se vor prezenta câteva tehnici software utilizate în
procesoarele superscalare şi VLIW. Aceste alternative pot simplifica mult
complexitatea hardware a procesorului. După cum se va vedea, utilizarea unor
asemenea optimizări software elimină necesitatea execuţiei Out of Order a
instrucţiunilor, a bufferului "instruction window", redenumirii dinamice a
regiştrilor etc.

4.5.b. MICROPROCESOARE MULTI-MICROTHREAD


paragraf preluat și adaptat din lucrarea: VINȚAN N. LUCIAN – Predicție
și speculație în microprocesoarele avansate, Editura Matrix Rom, București,
ISBN 973-685-497-3, 2002 [Vin02]

O altă categorie de microarhitecturi interesante și larg utilizate, care prin


modelul SMT (Simultaneous Multithreading) generalizează modelul de procesor
superscalar, o constituie aşa numitele microprocesoare multi-microthread (cu
microfire multiple de execuţie simultană). Un “procesor multi-microthread”
(PMT) deţine abilitatea de a procesa simultan instrucţiuni provenite din thread-

260
uri (“microfire de execuţie”) diferite, facilitând astfel execuţia programelor
“multifir”. În accepţiunea clasică din ingineria software, un thread (fir de
control) reprezintă o secvenţă atomică de program (cea mai mică entitate
software căreia sistemul de operare îi alocă resurse hardware pentru procesare),
concurentă la nivelul procesului părinte, recunoscută de către sistemele de
operare. În consecință, sistemul de operare permite mai multor fire de execuţie
să ruleze concurent, alocându-le resursele necesare în acest scop (de memorie –
zone de cod, date, stivă și respectiv de CPU). Firele au propriul spaţiu de cod şi
de stivă dar, spre deosebire de task-uri (procese), ele comunică printr-o zonă
partajată (shared) de memorie (zona de date a task-ului respectiv). Firele
partajează informaţia de stare a task-ului părinte precum şi anumite resurse
hardware alocate acestuia. În general, într-o paradigmă concurentă de
programare (Open MP, MPI, PVM, Open CL, Cuda etc.), un task este constituit
din mai multe fire de execuţie. În general, în cadrul modelelor de programare
concurentă cu memorie partajată, variabilele partajate au aceeaşi adresă şi
acelaşi înţeles pentru toate firele de execuție. În accepţiunea din arhitectura
calculatoarelor, mai largă, un microfir de execuţie poate fi un task, un fir de
execuţie dintr-un task, dar poate fi constituit şi din entităţi software implicite
(deci nepuse în evidență în mod explicit, prin limbajul concurent), de
granularitate mai mare, precum iteraţii ale unei bucle de program, secvențe de
cod cvasi-independente sau proceduri (rutine) din cadrul unui fir explicit, toate
executabile în paralel (execuţie concurentă). După cum se va arăta în continuare,
firele se activează-dezactivează pe parcursul execuţiei programelor, comunicând
între ele în mod implicit (prin accesarea spațiului de date partajat) şi
sincronizându-şi activităţile. Această tehnică se mai numeşte uneori, în mod
inexact, multitasking, deoarece programul se poate ocupa cvasi-simultan de mai
multe sarcini. PMT gestionează o listă a microthread-urilor active şi decide într-
o manieră dinamică asupra instrucţiunilor pe care să le lanseze în execuţie.
Coexistenţa mai multor thread-uri active permite exploatarea unui tip de
paralelism numit “Thread Level Parallelism” (TLP), adică paralelism la nivelul
microfirelor concurente de execuţie. În continuare, pentru comoditate, vom numi
un microfir, pur şi simplu, fir sau thread. Instrucţiunile din thread-uri diferite,
fiind în general independente între ele, se pot executa în paralel, ceea ce implică
grade superioare de utilizare ale resurselor CPU precum şi mascarea latenţelor

261
unor instrucţiuni aflate în execuţie. În acest ultim sens, de asemenea, gestiunea
branch-urilor este simplificată, latenţa acestora putând fi (măcar parţial)
acoperită prin instrucţiuni mașină aparţinând unor thread-uri diferite şi deci,
independente de condiţia de salt. De asemenea, efectul defavorabil al miss-urilor
în cache-uri poate fi contracarat prin acest multithreading (dacă un thread
generează un miss în cache spre exemplu, CPU-ul - Central Processing Unit -
poate continua procesele de aducere ale instrucţiunilor din cadrul celorlalte
thread-uri). Aşadar, “groapa” semantică între conceptele de procesare
multithreading a aplicaţiilor HLL şi procesorul hardware convenţional (care nu
sesizează semantica aplicației HLL) este umplută tocmai de aceste
“microprocesoare multithread“. Prin urmare, TLP-ul reprezintă o extensie, de
granularitate mai mică (deci granulă mai mare!), a paralelismului ILP
(Instruction Level Parallelism), exploatat de către microprocesoarele
superscalare sau de cele VLIW, EPIC etc.
Deşi multithreading-ul îmbunătăţeşte performanţa globală (rata medie de
instrucțiuni dinamice executate per ciclu), se cuvine a se remarca faptul că viteza
de procesare a unui anumit thread, în sine, nu se îmbunătăţeşte prin această
tehnică. Mai mult, este de aşteptat chiar ca viteza de procesare pentru fiecare
thread în parte să se degradeze, întrucât resursele CPU trebuie partajate între
toate thread-urile active, situate în structurile pipeline de procesare. Cu alte
cuvinte, acest TLP se pretează a fi exploatat, fireşte, în modurile de lucru ale
sistemelor cu multiprogramare sau/şi multithread (concurente). Partajarea
multiplelor resurse hardware în vederea implementării mai multor “contexte” de
procesare aferente fiecărui thread, implică probleme dificile în cadrul unui PMT
(mecanisme de aducere a mai multor instrucţiuni, de la adrese diferite şi
necontigue de memorie, structuri de predicţie multiple – corespunzătoare firelor
multiple, lansarea în execuţie a mai multor instrucţiuni aparţinând unor thread-
uri distincte, partajarea resurselor hardware de către diferitele fire de execuție
etc). Simularea şi optimizarea unor arhitecturi PMT devin extrem de sofisticate,
clasicele benchmark-uri gen SPEC (de tip monofir), nemaifiind aici de mare
ajutor. Trebuie lucrat în medii software bazate pe multi-programare sau multi-
threading, ceea ce nu este deloc uşor de implementat şi, mai ales, de simulat şi
evaluat.

262
Latenţa memoriei principale, prea mare în raport cu viteza de procesare a
microprocesorului, este o problema esenţială în sistemele de calcul actuale,
numită şi memory wall. Spre exemplu, în cadrul unui sistem multimicroprocesor
cu memorie partajată (“DSM - Data Shared Memory”), procesoarele sunt
conectate la modulele fizice de memorie printr-o reţea de interconectare, mai
mult sau mai puţin complexă (unibus, crossbar, interconectări dinamice
multinivel etc.) Dacă un anumit procesor doreşte, spre exemplu, să citească o
anumită locaţie din spaţiul logic unic de adresare, el va lansa o cerere de acces
printr-o instrucţiune de tip Load (sau un mesaj, în cadrul sistemelor
multiprocesor de tip message passing, cu memorii fizic distribuite dpdv logic).
Aceasta se va propaga de la procesor la modulul fizic de memorie, prin
intermediul reţelei de interconectare. Modulul de memorie va furniza data
procesorului după un timp propriu de acces la citire, intrinsec circuitului, prin
intermediul aceleiaşi reţele de interconectare (care are și ea o latență proprie).
Intervalul de timp dintre cererea procesorului şi recepţionarea datei de către
acesta, se numeşte latenţă. În cazul sistemelor actuale aceasta devine tot mai
mult o problemă datorită creşterii vitezei microprocesoarelor cu cca. 58 % pe an,
în timp ce timpul de acces al memoriilor de tip DRAM scade cu doar 6-7 % pe
an (după alţi autori acesta scade doar cu cca. 3-4% pe an, în timp ce densitatea
de integrare a acestor memorii creşte cu 40-60% pe an).
De exemplu, pe un multiprocesor DEC Alpha Server 4100 SMP având 4
procesoare Alpha 21164 la frecvențe de tact de 300 MHz (perioadă de 3,33 ns),
latenţele la citire sunt:
• 7 tacte (deci cca. 23 ns) în cazul unui miss pe nivelul 1 de cache (L1) şi
hit pe nivelul 2 de cache (L2).
• 21 tacte în cazul unui miss pe nivelul L2 (on cip) şi hit pe L3 (situat pe
placa de bază, deci off-cip).
• 80 tacte pentru miss în întreaga ierarhia de cache-uri şi accesarea DRAM-
ului (memoria principală) prin intermediul rețelei de interconectare.
• 125 de tacte pentru un miss care a fost servit din cache-ul altui procesor
(se adaugă aici şi latenţa reţelei de interconectare).
Spre exemplu, pentru un multiprocesor DSM de tip Silicon Graphics SGI
Origin 2000, care poate interconecta până la 1024 de microprocesoare, ne putem
aştepta la latenţe de 200 de tacte sau chiar mai mult. Este evident că aceste

263
latenţe mari se traduc, în principal, prin aşteptări prohibitive din partea
procesorului respectiv.
Una dintre strategiile arhitecturale relativ recente de a contracara problema
latenţelor mari ale sistemelor de memorie o constituie microprocesoarele
multithread dedicate (MMT). În principiu, multithreading-ul a migrat aici din
nivelul înalt al sistemelor de operare şi aplicaţiilor HLL (High Level
Languages), pe verticală, în cadrul firmware-ului şi al hardware-ului
microprocesoarelor moderne. Printr-o definiţie succintă şi intuitivă, un MMT
diferă de un microprocesor convenţional, de tip “monofir” ("single threaded"),
prin faptul că facilitează procesarea simultană a mai multor instrucţiuni
aparţinând unor thread-uri ("fire de execuţie") diferite, care însă sunt toate
candidate înspre a fi executate, în mod concurent, de către procesor. Similar cu
procesoarele convenţionale de tip "monofir", starea unui MMT constă în
contextul momentan al regiştrilor logici ai procesorului respectiv al memoriei;
diferenţa specifică rezidă în faptul că există în principiu mai multe perechi (PC -
Program Counter şi SP – Stack Pointer) şi seturi logice de regiştri generali,
permiţându-se astfel diferenţierea contextelor momentane aferente thread-urilor
în curs de execuţie. Iată deci, într-un mod succint şi principial, cum aceste
caracteristici specifice ale MMT-urilor facilitează procesarea multithread, de la
nivelul sistemului de operare şi aplicaţiilor HLL, până la cel al hardware-ului
procesoarelor actuale.
O altă noţiune importantă este aceea de fir de execuţie blocant respectiv
neblocant ("blocking or non-blocking"). Noţiunea se referă la blocarea fluxurilor
de instrucţiuni în cadrul structurilor pipeline de procesare a acestora, structuri
indispensabile procesoarelor multithreading dedicate de care ne ocupăm în
cadrul acestui paragraf. Un fir blocant poate stagna structura pipeline de
procesare pe un procesor convenţional "monofir", în cazul apariţiei unor
hazarduri specifice (structurale, de date, de ramificaţie, latenţe etc.). În schimb,
pe o arhitectură MMT aceste blocări pot fi evitate prin activarea unui alt thread,
realizată prin comutarea de context în cadrul procesorului (context switching).
Comutarea de context într-un procesor "monofir", în general, consumă timp
(salvări/restaurări de contexte în/din memorie), astfel încât mascarea unui blocaj
datorat unui miss în cache este practic compromisă, cel puţin parţial.

264
Firele neblocante sunt datorate componentei scheduler (reorganizator sau
optimizator de cod) din cadrul compilatorului. Acesta partiţionează programul în
mici thread-uri (microthreads), activarea unuia făcându-se numai atunci când
toate datele aferente devin disponibile. Aceleaşi mecanisme hardware trebuie
utilizate pentru a sincroniza comunicaţiile inter-procese între firele aflate în stare
de aşteptare. Ca exemple de fire blocante, amintim aici firele din cadrul
sistemelor de operare P(Osix), Solaris sau chiar întregi procese Unix din cadrul
unui sistem de operare Unix de tip multifir, dar chiar şi microthread-uri generate
de către compilator pentru a exploata potenţialul unui MMT.
În cadrul acestui scurt paragraf ne vom ocupa de acele arhitecturi MMT
dedicate şi care se dezvoltă pe cunoscutele structuri de procesoare RISC, EPIC
(Explicitly Instruction Set Computing – vezi Intel IA 64, procesorul “Itanium”),
VLIW (“Very Long Instruction Word”) şi superscalare [Vin00, Vin00b].
Principala cerinţă pentru un MMT o constituie abilitatea de a gestiona două sau
mai multe fire în paralel şi un mecanism care să permită comutarea contextelor
acestora. Fireşte, această comutare este de dorit să fie cât mai rapidă (0÷3 tacte
CPU). Ea este facilitată, după cum am mai menţionat, de mai multe PC-uri, SP-
uri şi seturi de regiştri logici, asociate firelor de execuţie.
În principiu, după modul în care un fir intră şi respectiv iese în/din
execuţie, există trei modalităţi distincte de procesare multithreading:
• prin întreţeserea instrucţiunilor în fiecare nou ciclu ("cycle by cycle
interleaving" sau fine grain multithreading), adică în fiecare ciclu CPU o
instrucţiune dintr-un alt thread este adusă şi lansată în procesare în structura
pipeline a CPU.
• prin întreţeserea blocurilor de instrucţiuni, adică instrucţiunile dintr-un
thread sunt executate până când apare un eveniment (hazard) ce produce o
latenţă mare de execuție. Acest eveniment implică o comutare de context, deci
sunt activate instrucţiunile din alt bloc-thread (“block interleaving” sau coarse
grain multithreading).
• multithreading simultan ("simultaneous multithreading" sau hyper-
threading cum l-au numit cei de la compania Intel), care constă într-o combinare
a multithreading-ului (TLP) cu procesarea superscalară (ILP). Instrucţiunile -
aparţinând sau nu unor fire de execuţie diferite - sunt lansate înspre unităţile
funcţionale aferente procesorului superscalar în mod simultan, în vederea

265
ocupării optimale a resurselor acestuia (grad de utilizare superior al resurselor
hardware).
De menţionat că o problemă criticabilă a MMT-urilor constă în faptul că, în
general, procesează mai puţin performant decât un procesor superscalar
echivalent un program de tip "monofir". Foarte important, în toate cazurile este
necesar un scheduler (planificator) de thread-uri (hardware, software sau
hibrid). Acesta exploateaza efectiv paralelismul TLP din cadrul programului
rulat.

Figura 4.11. Modele de procesare a instrucțiunilor

Modelul cu întreţesere la nivel de ciclu (cycle by cycle interleaving)

În cadrul acestui model, numit în literatura de specialitate şi "fine-grain


multithreading", procesorul comută pe un alt thread după fiecare aducere de
instrucţiune. În principiu, prin multiplexarea instrucţiunilor aparţinând unor
thread-uri diferite, pot fi anulate sau ameliorate efectele hazardurilor RAW
(Read After Write), hazardurilor de ramificaţie, latenţelor mari ale unor
instrucțiuni etc. De asemenea, comutarea de context are în acest caz latenţă nulă.
Modelul necesită un număr de thread-uri cel puţin egal cu numărul de nivele ale
structurii pipeline, pentru a fi certă anularea hazardurilor mai sus-amintite. De
remarcat totuşi că multiplexările instrucţiunilor din cadrul mai multor thread-uri
limitează viteza de procesare a unui singur thread. Există, în esentă, două
modalităţi de a limita această deficienţă:

266
a) O tehnică statică, integrată în scheduler, care permite lansarea
succesivă în structura pipeline a unor instrucţiuni din cadrul aceluiaşi thread,
dacă acestea nu sunt dependente de instrucţiuni anterioare aflate în curs de
procesare în structură. Această tehnică implică modificarea structurii ISA
(Instruction Set Architecture) a microprocesorului, în sensul adăugării câtorva
biţi fiecărui format de instrucţiune. Aceşti biţi vor informa câte instrucţiuni din
acelaşi thread o vor urma succesiv pe cea lansată în structură. Fireşte, tehnica
aceasta (numită "dependence lookahead") se pretează a fi utilizată atunci când
numărul de thread-uri este insuficient.

b) O tehnică hardware, prin care se adaugă structurii pipeline o logică


suplimentară de detecţie şi rezolvare a hazardurilor, atunci când se procesează
un singur thread, deci un hibrid între un CPU clasic şi unul de tip multithreading
cu întreţesere la nivel de ciclu.
Unul dintre cele mai cunoscute sisteme muliprocesor, care utilizează până
la 256 de procesoare MMT este "Tera Multithreaded Architecture" (MTA),
proiectat şi implementat la Seattle, S.U.A., de către cunoscuta companie Tera
Computer Co. Un procesor are un nucleu (kernel) ISA de tip VLIW, utilizează
modelul cu întreţesere a thread-urilor la nivel de ciclu, suportă maximum 128 de
thread-uri distincte (numite "streams") pe care le comută la fiecare 3 ns (f=333
MHz). În vederea creşterii vitezei de procesare monofir se foloseşte tehnica
"dependence lookahead", anterior descrisă pe scurt.
Procesorul MTA deţine maximum 512 module de memorie interconectate
la procesoare printr-o reţea toroidală 3D, având 2816 noduri de rutare. MTA
poate suporta pâna la 512 Gocteţi de memorie internă şi poate atinge o
performanţă maximă, pe benchmark-uri numerice în virgulă mobilă, de 256
GFLOPS. Sistemul exploatează paralelismul pe toate cele trei nivele de
granularitate succesive: la nivel de instrucţiuni (ILP) [Vin00], la nivelul
“intermediar” de thread-uri (TLP) cât şi la nivel de aplicaţii paralele
(multiprogramare propriu-zisă). Prima achiziţie comercială a unui asemenea
supercomputer s-a făcut în aprilie 1998 de către "San Diego Supercomputer
Center" din S.U.A.

Modelul cu întreţeserea blocurilor (block interleaving)

267
Acest model numit şi "coarse grain multithreading", execută un singur
thread până în momentul în care apare un eveniment ce declanşează comutarea
de context. Uzual, asemenea evenimente apar atunci când fluxul de instrucţiuni
din structura pipeline se blochează, spre exemplu datorită unei operaţii având o
latenţă relativ mare, datorită unei așteptări etc. Comparând cu modelul anterior,
se remarcă faptul că în acest caz este necesar un număr mai mic de thread-uri
distincte; de asemenea performanţa procesării unui singur thread este
comparabilă cu cea obtenabilă pe un procesor clasic (superscalar) echivalent d.
p. d. v. logic (al ISA – Instruction Set Architecture).

Time

Resurse (ex. UE)


Figura 4.12. Modelul de procesare superscalar, ineficient (un thread)

268
Time

Resources (e.g. FUs)


Figura 4.13. Modelul Block Interleaving (două thread-uri; 1 + 1)
Există principial două modalităţi de comutare a thread-urilor în cadrul
acestui model: statică şi respectiv dinamică. În continuare se vor explicita sumar
fiecare dintre aceste tehnici de comutare a blocurilor.

a. Comutare statică
În acest caz, comutarea este dictată prin program (compilator), printr-o
instrucţiune special dedicată în ISA. În principiu, timpul de comutare este aici de
un tact, având în vedere că după aducerea şi decodificarea instrucţiunii care
comută thread-ul, aceasta trebuie evacuată din structura pipeline. Dacă nu s-ar
evacua, timpul de comutare ar fi nul, dar întârzierea ar fi determinată de însăşi
procesarea acestei instrucţiuni "inutile". Există implementări în care comutarea
de context nu se face în mod explicit, ca mai sus, ci într-un mod implicit. Astfel,
spre exemplu, după introducerea fiecărei instrucţiuni de citire din memorie
(LOAD), se determină comutarea, tocmai spre a se evita latenţa miss-ului
potenţial în memoria cache. Fireşte, în cazul unui hit în cache-ul de date,

269
comutarea aceasta este, practic, inutilă. O altă posibilitate constă, spre exemplu,
în comutarea după fiecare scriere în memoria de date (STORE). Raţiunea ar fi
legată şi de asigurarea consistenţei secvenţiale a memoriilor locale în sistemele
multimicroprocesor, adică asigurarea faptului că un procesor citeşte din
memorie ultima dată înscrisă în respectiva locaţie. În fine, în cadrul altor
cercetări / implementări se comută thread-urile (blocurile, mai precis) după
fiecare instrucţiune de ramificaţie (branch), în scopul reducerii latenţelor
aferente sau chiar renunţării la implementarea predicţiei dinamice a acestora. În
schimb, performanţa în cazul "monofir" este drastic diminuată în acest caz.
Totuşi, această modalitate este eficientă în cazul blocurilor cu branch-uri dificil
de predicţionat (spre exemplu, branch-uri indirecte, generate de polimorfismele
din programarea obiectuală, branch-uri nepolarizate într-un anumit context
dinamic – unbiased branches etc.)

b. Comutarea dinamică
În acest caz, comutarea blocurilor se declanşează datorită unui eveniment
dinamic, apărut deci pe parcursul procesării hardware (run-time). În general,
comutarea dinamică implică timpi mai mari de comutare decât cea statică,
datorită necesităţii evacuării tuturor instrucţiunilor din structura pipeline,
anterioare stagiului care a declanşat comutarea. Şi aici, ca şi în cazul comutării
statice, putem avea câteva modalităţi distincte de comutare. Astfel, se poate
comuta blocul curent pe un miss în cache. Din păcate, acesta este detectat relativ
târziu în structura pipeline, implicând astfel timpi de comutare considerabili. O
altă modalitate poate comuta pe activarea unui semnal specific ("switch-on-
signal"), dat de o întrerupere, derută ori de recepţionarea unui mesaj. În fine, se
întâlnesc şi modele de comutare hibride de gen "conditional switch", în care
comutarea se realizează printr-o instrucţiune specială (caracter static), însă
numai în cazul în care aceasta întâlneşte un anumit context dinamic (instanţă
hardware). Astfel, spre exemplu, o instrucţiune tip "switch" poate fi introdusă de
către compilator după un grup de instrucţiuni LOAD/STORE. Dacă grupul
respectiv de instrucțiuni a generat "miss"-uri, comutarea se face, altfel nu.
Un exemplu experimental de astfel de procesor, a fost implementat la
prestigioasa universitate MIT din S.U.A. şi se numeşte "MIT Sparcle", întrucât
derivă din arhitectura binecunoscutului procesor RISC numit Sparc (Sun Co.)

270
Procesorul este scalar, are 4 contexte independente (seturi de regiştri, PC-uri,
SP-uri şi stări). Aşadar, pot fi procesate simultan în structura pipeline până la 4
thread-uri distincte. Sunt implementate strategiile de comutare a blocurilor de tip
"cache miss" şi respectiv "switch-on –signal", anterior descrise succint.
Comutarea se face prin hardware deci, de către controller-ul de cache (care
implementează inclusiv protocoale de coerenţă pentru conectarea în sisteme
multimicroprocesor). Penalizarea de comutare este una relativ mare, de 14 tacte.

Figura 4.14. Structura procesorului MIT Sparcle

4.3 Modelul multithreading simultan


Cele două modele de procesoare multithreading, anterior prezentate, sunt
modele eficiente pe procesoare scalare RISC ori chiar pe procesoare de tip

271
VLIW sau EPIC. Modelul "simultaneous multithreading" (SMT) derivă din
arhitectura superscalară cu procesări out of order, cea mai populară actualmente,
care lansează în execuţie mai multe instrucţiuni independente în fiecare ciclu şi,
asemenea MMT-urilor, conţine resurse hardware pentru contexte multiple
procesate simultan. Instrucţiunile independente procesate simultan într-un ciclu
pot proveni din acelaşi fir (ILP) sau din fire distincte (TLP), de unde avantajul
esenţial şi popularitatea acestor microprocesoare (numite de compania Intel,
hyperthreaded).
Datorită faptului că un procesor SMT exploatează simultan atât
paralelismul la nivel de instrucţiuni cât şi cel la nivel de thread-uri, adaptându-se
astfel în mod dinamic la cele două tipuri de paralelisme (ILP şi TLP),
performanţa acestuia o depăşeşte, teoretic cel puţin, pe cea a modelelor
anterioare. Practic există un fir principal, care se execută cu paralelism la nivelul
instrucțiunilor sale. Dacă într-un anumit ciclu mai există unități de execuție
libere, se încearcă ocuparea acestora cu instrucțiuni independente din firele
secundare. Preţul constă însă într-o organizare hardware ceva mai complexă
decât în cazurile anterioare.
Într-un model de procesor SMT lansarea instrucţiunilor în execuţie se poate
face din buffere de prefetch separate, în mod simultan. Aşadar, unităţile
funcţionale ale procesorului pot fi ocupate cu instrucţiuni din thread-uri diferite,
mascându-se astfel latenţele instrucţiunilor provenite dintr-un singur fir de
execuţie. Un procesor SMT menţine starea fiecărui thread în hardware şi permite
comutarea rapidă între thread-uri.
Unitatea de fetch (aducere) de instrucţiuni poate aduce în fiecare ciclu,
instrucţiuni din thread-uri distincte, crescând astfel probabilitatea de a aduce
instrucţiuni nespeculative. Fireşte, unitatea de fetch poate avea o politică
selectivă, relativă la thread-urile pe care le alege. Spre exemplu, ar putea să le
aleagă pe acelea care oferă un beneficiu de performanţă imediat. Procesoarele
SMT pot fi organizate, principial, în două moduri distincte:
• Ele pot partaja, prin thread-uri diferite, în mod agresiv, nişte resurse
hardware (ex. buffer-ele de prefetch şi structurile pipeline de procesare ale
instrucţiunilor, structura ROB etc.), atunci când aceste thread-uri nu conţin
suficient de mult paralelism la nivel de instrucţiuni. Deci procesoarele SMT
adaugă un hardware minimal la superscalarele convenţionale; complexitatea

272
adăugată se referă în principiu la un “tag” (identificator) adăugat fiecărei
instrucţiuni dintr-un anumit thread, seturi multiple de regiştri logici etc.
• Al 2-lea model organizaţional multiplică toate buffer-ele interne ale
procesorului superscalar, asociind câte un astfel de buffer unui thread, la un
moment dat (v. Figura 4.15, dreapta). Această organizare este mai complexă, dar
face modelul SMT mai natural, raportat la paradigma multithreading, conducând
astfel la o alocare mai eficientă a thread-urilor la resursele hardware existente.
De observat că arhitectura SMT nu dă rezultate deosebite pe programe de tip
“monofir”.
Problema esențială este cea a scheduler-ului care exploateaza ILP si TLP
deopotrivă.

Simultaneous
Superscalar Fine-Grained Coarse-Grained Multiprocessing Multithreading

Thread 1 Thread 3 Thread 5


Thread 2 Thread 4 Idle slot

Figura.4.15. Comparație între diferite modele de procesare (superscalar,


multithreads, multi-cores)

273
O interesantă cercetare pe bază de simulare a avut loc la Universitatea din
Washington, S.U.A. Arhitectura SMT dezvoltată era centrată pe o arhitectură
superscalară, cu posibilitatea lansării în execuţie a până la 8 instrucţiuni
independente simultan, precum şi cu posibilitatea procesării a 8 fire de execuţie.
În fiecare ciclu de fetch instrucţiuni se aduc câte 8 instrucţiuni din două thread-
uri diferite, pe baza unui algoritm cu priorităţi rotitoare ("round-robin"). Pentru
simulare s-au folosit binecunoscutele benchmark-uri SPEC’98, care au fost
executate simultan pe post de thread-uri distincte (ceea ce, evident, constituie o
simplificare, întrucât nu se simulează comunicarea între acestea). Pe această
configuraţie s-a obţinut o rată medie de execuţie de 6,64 instrucțiuni/ciclu,
remarcabilă având în vedere că un procesor superscalar (monofir) echivalent, ar
obţine doar cca. 2-3 instr./ciclu. De asemenea, studii relativ recente asupra
eficienţei arhitecturilor SMT, cu posibilitatea pocesării a 8 fire de execuţie în
procesarea bazelor de date şi respectiv multimedia, arată că aceste arhitecturi
realizează creşteri de performanţă de cca. 300 % în comparaţie cu arhitecturi
monofir având resurse hardware/software similare.
În literatura de specialitate s-a propus o abordare inedită, care integrează
predicţia valorilor în arhitecturile de tip multithread [Vin07]. Ideea de bază
constă în execuţia concurentă a unor thread-uri speculative din cadrul
programului. Acestea sunt determinate pe timpul rulării programului cu ajutorul
predictorului de branch-uri. Fireşte că, în vederea execuţiei thread-urilor
speculative împreună cu cele nespeculative, este necesar ca procesorul să ofere
contexte hardware multiple. Fiecare thread va avea propria sa fereastră de
instrucţiuni. O problemă esenţială este aceea a determinării efective a thread-
urilor speculative. Spre exemplu, în cazul unei bucle de program, thread-urile
speculative ar putea fi constituite din chiar iteraţiile buclei respective. Acesta
este cazul proiectului STAMPede de la Universitatea Carnegie Mellon, SUA,
condus de către profesorul Todd Mowry
(www.cs.cmu.edu/~tcm/STAMPede.html). Pentru programele non-numerice,
caracterizate prin grade limitate de paralelism, este foarte probabil să existe
inter-dependenţe între aceste thread-uri. O rezolvare simplă în acest sens, ar
consta în serializarea acestor dependenţe de date. Predicţia valorilor (v. în
continuare) ar putea comprima lanţurile de instrucţiuni dependente,
îmbunătăţind semnificativ performanţa acestor arhitecturi multifir. Cercetări

274
laborioase au arătat că o asemenea arhitectură hibridă a implicat o creştere de
performanţă de 40% faţă de o arhitectură superscalară monofir şi respectiv de
9% faţă de o arhitectură superscalară monofir, având înglobat un predictor hibrid
de valori.
În concluzie, procesoarele multithread au apărut din necesităţi evidente de
mapare a hardware-ului pe softurile actuale de nivel înalt, care exploatează şi ele
acest concept, încă mai de demult. Aceste MMT-uri şi în special procesoarele
SMT constituie o tendinţă serioasă a actualei generaţii arhitecturale de
microprocesoare (cea de a 4-a). Alături de reutilizarea dinamică a instrucţiunilor
(“Dynamic Instruction Reuse” – tehnică de comprimare nespeculativă a
lanţurilor de instrucţiuni dependente RAW) şi predicţia valorilor instrucţiunilor
(“Value Locality and Value Prediction” – tehnică speculativă), ce vor fi
prezentate în paragrafele următoare ale acestei lucrări, dar şi de alte concepte
arhitecturale, precum multiprocesoarele integrate pe un singur cip (multi and
many-cores), credem că SMT-urile se vor impune în continuare, în vederea
exploatării agresive a ILP şi TLP.
De remarcat până la acest moment al cursului nostru că dimensiunea
entităților paralelizate a crescut pe parcurs, de la paralelismul temporal al fazelor
instrucțiunilor din structurile pipeline, până la firele de execuție, puse în
evidență la nivelul limbajului concurent.

Pipeline (faze) ILP (instructiuni) TLP (threads, resurse partajate)


Multi & Many-Cores (tasks, procesoare distincte)

4.6. OPTIMIZAREA BASIC-BLOCK-URILOR ÎN ARHITECTURILE


MEM

În figura următoare se prezintă principalele faze de lucru ale unui


compilator.

275
Frontend Backend

Generare
Analiza Generare
format Optimizare
lexicală cod obiect
intermediar
Cod Sursă Cod Obiect
Analiza Analiza
sintactică semantică

Figura.4.15.b. Structura generică a unui compilator

Frontend-ul conține toate fazele care sunt dependente de limbajul HLL


care este compilat. Datele de intrare sunt constituite din fișierele care conțin
codul sursă al aplicației HLL. Printre fazele conținute aici, se numără analiza
lexicală, analiza sintactică, analiza semantică precum și generarea formatului
intermediar. Rezultatul front-end-ului este dat de formatul intermediar, care este
folosit pentru a comunica cu următoarea secțiune, numită backend. Aceasta
conține toate fazele care sunt relativ independente de limbajul HLL compilat.
Datele de intrare sunt reprezentate de formatul intermediar generat de către
front-end. Printre fazele conținute aici se numără analiza și optimizarea codului,
selectarea de instrucțiuni specifice procesorului, alocarea registrelor, scheduling
și respectiv generarea codului obiect, direct executabil.
Analiza lexicală împarte textul într-o serie de componente de bază, numite
token-uri (jetoane). Acestea pot reprezenta cuvinte cheie specifice limbajului,
numere, șiruri de caractere, nume de variabile, operatori, semne de punctuație
etc. Analiza sintactică are ca intrare o serie de token-uri și determină structurile
de limbaj care sunt generate de către diverse secvențe de genul: declarații de
variabile, expresii, structuri conditionale (if, else) și repetitive (while, for) etc.
Rezultatul este dat de un arbore de sintaxă, unde nodurile reprezintă diverse
structuri ale limbajului, identificate în fișierul sursă. Analiza semantică
validează codul sursă, conform regulilor limbajului. Are ca intrare un arbore de
sintaxă și emite erori/avertizări în momentul în care s-a detectat o structură
necorespunzătoare. Formatul intermediar constituie o reprezentare de nivel mai
jos decat arborele de sintaxă, asemănătoare unui limbaj de asamblare. Acest
format este ales astfel încât analiza și aplicarea optimizărilor să fie simplificate
(spre exemplu aici, byte-codul mașinii virtuale Java). Faza de optimizare este

276
prezentă în toate compilatoarele moderne. Ea este formată din mai mulți pași,
fiecare urmărind să îmbunătățească una sau mai multe caracteristici ale
formatului intermediar ( spre exemplu, eliminarea de instrucțiuni redundante,
evaluarea de constante, optimizarea buclelor de program, reducerea numărului
de branch-uri etc.) Codul obiect specific unui procesor se generează în mai
multe etape, după cum urmează:
• Înlocuirea instrucțiunilor din formatul intermediar cu una sau mai multe
instrucțiuni reale ale procesorului.
• Formatul intermediar folosește un număr nelimitat de registre, care trebuie
să fie mapate pe registrele reale ale procesorului, aflate în număr mai
redus. Alocatorul de registre decide care dintre registre sa fie mapate pe
registrele reale ale CPU; atunci când nu mai există registre disponibile,
trebuie să se decidă care valori vor fi stocate (temporar) pe stivă.
• Scheduling-ul reordonează instrucțiunile, astfel încât să determine un
paralelism la nivelul instrucțiunilor cât mai ridicat. Pe această etapă ne
vom focaliza, în continuare, în această lucrare.
• Generarea codului obiect, într-un format specific platformei hardware,
cod transmis link-editorului pentru generarea aplicației finale, direct
executabile.

Ca şi în cazul procesoarelor scalare, reorganizarea statică (scheduling)


reprezintă procesul de aranjare a instrucţiunilor din cadrul unui program obiect
astfel încât acesta să se execute într-un mod cvasi-optimal din punct de vedere al
timpului de procesare (mai nou, și din punct de vedere al energiei consumate).
Procesul de reorganizare a instrucţiunilor determină creşterea probabilităţii ca
procesorul să aducă simultan din cache-ul de instrucţiuni și să execute mai multe
instrucţiuni independente de date. De asemenea, asigură procesarea eficientă a
operaţiilor critice din punct de vedere temporal, în sensul reducerii prin mascare
a latenţelor specifice acestor operaţii. Se va aborda mai întâi problema
optimizării statice a " basic block"-urilor. De remarcat că scheduling-ul static în
procesoarele superscalare poate determina o simplificare substanţială a
arhitecturii hardware aferente acestora. Astfel, aceste microprocesoare pot fi
implementate în hardware cu execuții in order a instrucțiunilor, ceea ce
simplifică substanțial organizarea hardware a acestora. Execuția instrucțiunilor

277
se va face totuși out of order, datorită faptului că scheduler-ul static a
reorganizat instrucțiunile mașină.
Se va analiza acum spre exemplificare următoarea secvenţă de program:

I1: ADD R1, R11, R12


I2: ADD R1, R1, R13
I3: SLL R2, R3, #4; R2<--R3 deplasat logic la stânga cu 4 poz. binare
I4: AND R2, R1, R2
I5: ADD R1, R14, R15
I6: ADD R1, R1, R16
I7: ADD R1, R1, #6
I8: LD R1, (R1)
I9: LD R4, (R4)
I10: ADD R1, R4, R1
I11: OR R1, R1, R2
I12: ST R1, (R4)

Considerând un procesor superscalar care decodifică simultan 4 instrucţiuni


şi deţine 4 unităţi de execuţie (două unităţi ALU, o unitate LOAD / STORE şi o
unitate pentru deplasări logice / rotiri), procesul de execuţie al secvenţei
anterioare s-ar desfăşura ca în Tabelul 4.11 (am presupus că doar instrucţiunile
LOAD au latenţa de doi cicli maşină):

278
Tabelul 4.11.Execuţia unui program neoptimizat pe un procesor
superscalar

De remarcat că rata de procesare a acestei secvenţe este de 12 instrucţiuni


per ciclu, adică în medie 1,1 instrucţiuni / ciclu (s-a considerat procesare In
Order, din punct de vedere al ferestrelor de execuţie şi respectiv Out of Order,
în cadrul unei ferestre curente de execuţie). Se observă că paralelismul potenţial
al secvenţelor de instrucţiuni I1 - I4 respectiv I5 - I12 nu este exploatat. În
continuare se va prezenta un algoritm de reorganizare în "basic block"-uri
(unităţi secvenţiale de program) în vederea unei execuţii cvasi-optimale pe un
procesor superscalar sau VLIW.

4.6.1. PARTIŢIONAREA UNUI PROGRAM ÎN "BASIC-BLOCK"-URI

Se are în vedere construirea grafului de control al unui program dat.


Algoritmul de partiţionare constă, principial, în următorii doi paşi:
1) Determinarea setului de lideri în cadrul programului. Se numeşte lider,
prima instrucţiune dintr-un program, instrucţiunea destinaţie a oricărei
instrucţiuni de salt/apel/revenire/branch sau orice instrucţiune următoare unei
instrucţiuni de branch/salt/apel/revenire.

279
2) Partiţionarea programului în unităţi secvenţiale (basic-blocks) şi
construirea grafului de control. Fiecare unitate secvenţială conţine un singur
lider şi toate instrucţiunile de la acest lider până la următorul, exclusiv.
Se determină predecesorii imediaţi faţă de o unitate secvenţială de program.
Poate fi un predecesor imediat al unei unităţi secvenţiale date, orice unitate
secvenţială care se poate executa înaintea unităţii date.
Se determină succesorii unei unităţi secvenţiale de program. Se numeşte
succesor al unei unităţi secvenţiale de program orice unitate secvenţială de
program care poate să se execute după execuţia celei curente. În figura de mai
jos (Figura 4.16) se dă un exemplu de partiţionare a unui program dat în "basic
block"-uri precum şi graful de control al programului.

Figura 4.16. Partiţionarea unui program în basic-block-uri

4.6.2. CONSTRUCŢIA GRAFULUI DEPENDENŢELOR DE DATE


ASOCIAT

280
După cum se va putea constata, în stabilirea unei secvenţe reorganizate de
program în vederea unei procesări cvasioptimale pe un procesor superscalar sau
VLIW, graful dependenţelor de date aferent unei unităţi secvenţiale de program,
se va dovedi deosebit de util. Un arc în acest graf semnifică o dependenţă RAW
între cele două stări. Instrucţiunile care utilizează date din afara unităţii
secvenţiale de program se vor plasa în vârful grafului, astfel încât în ele nu va
intra nici un arc. Pentru o instrucţiune dată se caută în jos proxima dependenţă
RAW. Trebuie verificate dependențele de date între oricare două instrucțiuni din
N ( N − 1)
cele N ale basic-block-ului respectiv ( C N2 = verificări). Cu aceste reguli
2
simple, graful dependenţelor de date corespunzător secvenţei de program
anterioare este prezentat mai jos (Figura 4.17):

Figura 4.17. Graful dependenţelor de date asociat

În stânga arcului este scrisă latenţa operaţiei (instrucțiunii) respective. În


dreapta arcului este scrisă latenţa maximă a drumului, măsurată dintr-un vârf al
arcului, până în starea respectivă. Latența drumului într-o anumită stare
(instrucțiune) curentă este egală cu latența anterioară maximă a drumului până la

281
respectiva instrucțiune + latența instrucțiunii curente. Graful dependenţelor de
date specifică deci relaţii de ordine între instrucţiuni, absolut necesare execuţiei
corecte a programului considerat.
Graful precedenţelor

Se obţine pe baza grafului dependenţelor de date, în baza faptului că există


cazuri în care acesta poate să nu cuprindă toate precedenţele necesare unei
corecte reorganizări. Altfel spus, acest graf nu pune în evidenţă relaţiile de
precedenţă strictă în lansarea în execuţie, impuse de către dependenţele de tip
WAR respectiv WAW între instrucţiuni.
Spre exemplu, între instrucţiunile I2 şi I5 există o dependenţă de tip WAW,
iar între instrucţiunile I4 şi I5 una de tip WAR. Aceste dependenţe ar obliga
schedulerul să proceseze I2 şi I4 înaintea instrucţiunii I5. Şi totuşi aceste
dependenţe (mai degrabă conflicte de nume) între secvenţele I1 - I4 şi respectiv
I5 - I10, pot fi eliminate prin redenumirea regiştrilor care determină
dependenţele WAR şi WAW (în cazul nostru registrul R1). Astfel, de exemplu,
dacă în cadrul secvenţei de instrucţiuni I1-I4 se redenumeşte registrul R1 cu un
alt registru disponibil în acel moment (de ex. cu R5), atunci secvenţele I1 - I4 şi
respectiv I5 - I10 devin complet independente, permiţând o procesare paralelă
mai accentuată.
Redenumirea regiştrilor, ca şi în cazul procesoarelor scalare, se poate face
static, adică prin software, în momentul compilării, sau dinamic, prin hardware,
în momentul procesării. Trebuie deci redenumiţi acei regiştri care determină
dependenţele WAR şi WAW între ramuri independente ale unităţii secvenţiale
de program. Se arată că redenumirea regiştrilor creşte numărul regiştrilor
utilizaţi dar şi timpul de viaţă al unui registru. Prin timp de viaţă al unui registru
se înţelege numărul instrucţiunilor cuprinse între prima instrucţiune care
actualizează respectivul registru şi respectiv ultima instrucţiune care-l citeşte,
înainte de o nouă, posibilă, actualizare a acestuia. Aşadar, redenumirea
regiştrilor creează dificultăţi alocării regiştrilor. Redenumirea se poate face pe
durata timpului de viaţă al registrului.
Important este însă faptul că prin redenumire, graful precedenţelor devine
inefectiv, singurul care impune precedenţe reale, fundamentale, fiind deci graful
dependenţelor de date prin dependenţele RAW intre instrucţiuni. Precedenţele

282
impuse prin dependenţe de tip WAR şi WAW au fost puse în evidenţă prin linii
întrerupte în figura anterioară (Figura 4.17). De asemenea, pentru o corectă
execuţie, trebuie respectată ordinea instrucţiunilor LOAD / STORE, dacă
adresele acestora pot fi cumva egale (aliasuri de memorie). Aşadar instrucţiunile
I8 şi I9 trebuie să preceadă instrucţiunea I12. Această ultimă constrângere însă,
nu introduce în cazul concret analizat precedenţe suplimentare în graful
dependenţelor de date. Această problemă - numită şi analiză antialias - a fost
detaliată în Capitolul 3.

4.6.3. CONCEPTUL CĂII CRITICE

Calea critică a grafului dependenţelor de date reprezintă drumul cu latenţă


maximă (cel mai lung lanț de instrucțiuni dependente RAW, din punct de vedere
al numărului de cicli implicați), fiind deci reprezentată în exemplul analizat aici
de secvenţa de instrucţiuni I5, I6, I7, I8, I10, I11 şi I12. Latenţa acestei căi este
de 8 cicli. Conceptul căii critice este important, deoarece acesta indică faptul că
după scheduling-ul static, profitând la maximum de paralelismul între
instrucţiuni, programul se va putea executa în minimum 8 cicli, adică într-un
timp egal cu latenţa căii critice. Prin strategia sa, scheduler-ul va trebui ca în
fiecare ciclu, pe cât posibil, să execute câte o instrucţiune din calea critică,
încercând simultan să suprapună peste această instrucţiune şi alte instrucţiuni
independente din program.
Într-un procesor ipotetic, ideal, având resurse infinite (suficient de multe
pentru a procesa în paralel oricâte instrucțiuni independente), scheduler-ul optim
ar trebui pur şi simplu să urmeze calea critică, suprapunând peste operaţiile de
aici, operaţii (instrucțiuni) din alte căi. În cazul apariţiei unui hazard WAW sau
WAR între instrucţiuni trebuie redenumite registrele implicate, dacă este posibil.
Spre exemplu, în cazul anterior prezentat, simultan cu instrucţiunea I5 s-ar putea
executa instrucţiunile I1, I3 şi I9 în condiţiile în care procesorul ar deţine
suficiente resurse hardware (două unităţi ALU, o unitate de shiftare şi o unitate
LOAD / STORE, în acest caz). De asemenea, datorită hazardului WAW dintre
instrucțiunile I1 şi I5, în instrucţiunea I1 ar trebui redenumit registrul R1 cu un
alt registru disponibil din setul de regiştri. Cum în practică un procesor nu deţine

283
întotdeauna suficiente resurse în vederea executării instrucțiunilor de mai sus,
rezultă că timpul de execuţie al programului reorganizat este mai mare sau cel
mult egal cu latenţa căii critice.
O reorganizare optimă ar însemna să se simuleze execuţia tuturor
variantelor posibile de programe reorganizate şi să se măsoare ratele medii de
procesare aferente, alegându-se varianta de program cu rata cea mai mare.
Pentru programe suficient de mari, acest deziderat ar implica uneori săptămâni
sau chiar ani de procesare, devenind deci prohibit. În practică, se preferă
utilizarea unor algoritmi euristici, bazaţi pe graful dependenţelor de date, care
dau rezultate apropiate de cele optimale, în schimb necesită timpi de execuţie
acceptabili. Aşadar problema optimalităţii teoretice a scheduling-ului, nu se
pune din cauza timpului enorm necesar acestui proces complex. În plus,
algoritmii euristici utilizaţi în practică dau rezultate bune.

4.6.4. ALGORITMUL "LIST SCHEDULING" (LS)

Este unul dintre cei mai reprezentativi algoritmi de optimizare statică a


execuției unui basic-block (optimizare locală deci), fapt pentru care va fi
prezentat pe scurt. Timpul de execuţie este rezonabil, întrucât algoritmul se
execută într-o singură trecere, de jos în sus (bottom to top), prin graful
dependenţelor de date, generând în majoritatea cazurilor reorganizări optimale.
Algoritmul LS parcurge graful dependenţelor asociat unităţii secvenţiale de
program de jos în sus. În fiecare pas se încearcă lansarea în execuţie a
instrucţiunilor independente disponibile. După ce aceste instrucţiuni au fost puse
în execuţie, instrucţiunile precedente devin disponibile spre a fi lansate în pasul
următor. Fiecărei instrucţiuni i se ataşează un așa numit grad de prioritate, egal
cu latenţa căii instrucţiunii. Dacă apare un conflict la resurse hardware comune
între două sau mai multe instrucţiuni, are prioritate instrucţiunea cu un grad de
prioritate mai mare (latența drumului aferent instrucțiunii). Precizând că iniţial
se setează un contor de cicli la o valoare maximă, paşii algoritmului sunt
următorii:
1) Instrucţiunea cea mai prioritară dintre instrucţiunile disponibile în setul
curent este lansată în execuţie, dacă nu implică o resursă ocupată în acest ciclu.

284
2) Dacă o instrucţiune a fost pusă în execuţie în pasul 1, resursele utilizate
de aceasta vor fi setate ca fiind ocupate, pentru un număr de cicli egali cu latenţa
instrucţiunii. Pentru exemplul nostru se va considera latenţa instrucţiunilor
LOAD de doi cicli, iar latenţa celorlalte instrucţiuni de doar un ciclu. De
remarcat că dacă aceste latențe asignate în mod static vor fi depășite în
procesarea reală (hardware), nu este nicio problemă, întrucât procesorul va
introduce stări suplimentare de așteptare în structurile pipeline de procesare,
după cum am arătat.
3) Dacă instrucţiunea a fost lansată în execuţie în pasul 1, ea va fi ştearsă
din lista instrucţiunilor disponibile în acel ciclu. Dacă instrucţiunea nu a fost
lansată în execuţie datorită unui conflict, reorganizarea va continua cu o altă
instrucţiune disponibilă.
4) Se repetă paşii 1 - 3, până când nu mai există nicio instrucţiune
disponibilă în acest ciclu.
5) Se decrementează contorul de cicli.
6) Se determină următorul set de instrucţiuni disponibile. Precizăm că o
instrucţiune este disponibilă dacă diferenţa între numărul ciclului în care a fost
lansată în execuţie instrucţiunea succesoare şi numărul ciclului curent, este egală
cu latenţa instrucţiunii.
7) Dacă setul determinat la pasul 6 este consistent, se trece la pasul 1. În
caz contrar, reorganizarea este completă.
Aplicarea algoritmului pe graful din exemplul considerat generează
următoarea ordine de execuţie a instrucţiunilor (v. Tabelul 4.12). De remarcat că
această ordine nu a ținut cont de vreo restricție hardware (practic s-a considerat
că procesorul deține resurse hardware infinite, adică suficient de multe încât să
nu existe hazarduri structurale.)

285
Tabelul 4.12. Ordinea de execuţie a instrucţiunilor în urma optimizării

Execuţia instrucţiunilor pe un procesor superscalar In Order Issue (din


punct de vedere hardware), cu 4 unităţi de execuţie, se va face ca în tabelul de
mai jos (Tabelul 4.13):

Tabelul 4.13. Execuţia programului optimizat

De remarcat că în ciclul 4 a existat un conflict structural între instrucţiunile


I8 şi I9 (instrucțiuni de tip Load). S-a dat prioritate instrucţiunii I8 pentru că are
un grad de prioritate superior (5>2). Invers n-ar fi putut fi, pentru că I8 şi I7 sunt
dependente RAW (prin registrul R1), de unde rezultă necesitatea prioritizării
după latenţa nodurilor respective. Pe această secvenţă de program se obţine o
rată medie de procesare de 12 / 8 = 1.5 instrucțiuni / ciclu, faţă de doar 1.1 instr.

286
/ ciclu, cât era rata de procesare a variantei de program inițiale, executată pe
acelaşi procesor superscalar.
O altă observaţie foarte importantă este aceea că scheduling-ul poate
îmbunătăţi semnificativ gradul de utilizare al resurselor hardware, după cum de
altfel se poate observa şi în cazul analizat aici. O variantă de algoritm similar,
care însă ar parcurge graful de sus în jos, ar genera următoarea execuţie (Tabelul
4.14):

Tabelul 4.14. O altă posibilă execuţie a programului optimizat

Latenţa instrucţiunii I8 şi dependenţa RAW între I8 şi I10, au impus ca în


ciclul 4 să nu se execute nicio operaţie. Performanţa este însă şi în acest caz de
1.5 instr. / ciclu. Este de așteptat ca performanţa unui procesor superscalar de tip
In Order Issue care procesează un basic-block reorganizat optimal, este în
general mai bună decât performanţa unui procesor superscalar Out of Order care
procesează programul neoptimizat, pentru că în al doilea caz paralelismul între
instrucţiuni este limitat de capacitatea bufferului de prefetch (instruction
window).
În literatura de specialitate sunt citate două variante principiale de realizare
a reorganizărilor software, între care există un compromis fundamental. Prima,
este metoda post-scheduling, care implică după compilare mai întâi alocarea
regiştrilor, iar apoi reorganizarea (optimizarea) propriu-zisă. În acest caz,
reorganizatorul este constrâns la nişte false dependenţe de date, datorită faptului
că alocatorul de regiştri reutilizează un registru cât mai mult posibil, rezultând
un timp de viaţă mai mic al acestuia, ceea ce duce la aceste false dependenţe de

287
date, care se rezolvă de către scheduler prin redenumirea regiştrilor (în exemplul
nostru a fost necesară redenumirea registrului R1). A doua metodă se numeşte
pre-scheduling şi presupune mai întâi realizarea reorganizării codului obiect, iar
apoi alocarea regiştrilor. În acest caz este posibil ca alocatorul de regiştri să nu
poată păstra toate variabilele în regiştri, deoarece schedulerul, prin redenumire,
măreşte timpul de viaţă al regiştrilor utilizaţi. Algoritmul LS pune la dispoziţia
structurii hardware paralelismul la nivel de instrucţiuni dintr-un program, pe
care structura respectivă îl exploatează la maximum.
În cele ce urmează, vom aborda problema optimizării globale a
programelor pentru procesoarele cu execuţie multiplă a instrucţiunilor.

4.7. PROBLEMA OPTIMIZĂRII GLOBALE ÎN CADRUL


PROCESOARELOR MEM

În continuare vom prezenta câteva tehnici software legate de optimizarea


globală a programelor pentru procesoarele superscalare (cu execuții in order ale
instrucțiunilor, din punct de vedere hardware) şi VLIW. În paragraful precedent
s-au prezentat câteva tehnici în vederea optimizării "basic-block"-urilor
(optimizare locală). Optimizarea "basic-block"-urilor aferente unui program nu
implică în mod necesar optimizarea întregului program, datorită problemelor
legate de instrucţiunile de ramificaţie, care conectează aceste blocuri de
instrucțiuni.
Reorganizarea programelor care conţin branch-uri este mai dificilă, întrucât
aici "mutările" de instrucţiuni pot cauza incorectitudini ale programului
reorganizat, care ar trebui, desigur, corectate. Această optimizare a întregului
program obiect se mai numeşte şi optimizare globală. Problema optimizării
globale este una de mare actualitate şi interes, întrucât paralelismul la nivelul
basic-block-urilor, după cum arătau încă din anii '70 pionieri ai calculatoarelor,
precum profesorul Michael Flynn (Universitatea Stanford, SUA), este relativ
scăzut (2-3 instrucţiuni, întrucât un astfel de bloc nu depășește, în medie,
măsurat pe programele de uz general, 6 instrucțiuni). Deoarece majoritatea
programelor HLL sunt scrise în limbaje imperative şi pentru maşini secvenţiale,

288
cu un număr foarte limitat de registre în vederea stocării temporare a
variabilelor, este de aşteptat ca gradul de dependenţe între instrucţiunile
adiacente să fie ridicat. Aşadar, pentru mărirea nivelului de paralelism este
necesară suprapunerea execuţiei unor instrucţiuni situate în basic-block-uri
diferite, ceea ce conduce la ideea optimizării globale. În fond, în abordarea
hardware, prin predicția dinamică a instrucțiunilor de salt condiționat se urmărea
același scop, anume paralelizarea unor instrucțiuni din basic-block-uri diferite,
prin procesări predictiv-speculative ale instrucțiunilor.
Numeroase studii au arătat că paralelismul programelor de uz general poate
atinge, în variante idealizate (resurse hardware nelimitate, renaming perfect,
analiză antialias perfectă, predicție perfectă a branch-urilor etc.) în medie 50-60
instrucţiuni simultane [Vin00]. De remarcat că schedulerele actuale cele mai
performante, raportează performanţe cuprinse între 3-7 instrucţiuni simultane.
Se apreciază ca realistă obţinerea în viitorul apropiat a unor performanţe de 10-
15 instrucţiuni simultane, bazat pe îmbunătăţirea tehnicilor de optimizare
globală. Problema optimizării globale este una deschisă la ora actuală, având o
natură NP – completă (Non-deterministic Polynomial-time, cum sunt numite în
teoria complexității algoritmilor). Se prezintă în continuare în acest sens doar
tehnica numită "Trace Scheduling", datorită faptului că este oarecum mai simplă
şi mai clar documentată în literatura de specialitate. Considerăm că marea
majoritate a tehnicilor de optimizare globală nu au încă o justificare teoretică
foarte solidă, bazându-se deseori pe o euristică dependentă de caracteristicile
arhitecturii / programului de optimizat. Frecvent, ele au drept scop execuţia unei
instrucţiuni cât mai repede posibil, prin mutarea instrucţiunilor peste mai multe
basic- block-uri. Constrângerile sunt date în principal de resursele hardware
limitate. Pentru claritate, se va considera în continuare că instrucţiunile de salt
nu conţin BDS-uri.

4.7.1. TEHNICA "TRACE SCHEDULING" (TS)

Este atribuită cercetătorului american Dr. Joshua Fischer (ulterior laureat


IEEE/ACM Eckert-Mauchly Award), pe atunci la Universitatea din New York,
S.U.A., care a implementat-o în cadrul calculatorului VLIW numit BULLDOG

289
(1986). O implementare mai veche a acestei tehnici a fost legată de optimizarea
microprogramelor în arhitecturi microprogramate orizontal. Se defineşte o cale
(numită aici "Trace", deși poate că denumirea “Path” - cale, ar fi fost mai
adecvată) într-un program ce conţine salturi condiţionate, o ramură particulară a
acelui program, legată de o asumare dată a adreselor țintă ale acestor salturi.
Rezultă deci că un program care conţine n salturi condiţionate, va putea avea
maximum 2n posibile căi (trace-uri). Mai jos se arată un model de program
static conținând n instrucțiuni de salt condiționat și care generează 2n posibile
căi distincte de rulare a acestui program, după cum fiecare din aceste n branch-
uri poate să se facă (Taken) sau nu (Not Taken). Pentru un n suficient de mare
este evident că nu se poate efectua optimizarea tuturor acestor 2n posibile căi de
execuție ale programului. Iată de ce optimizarea globală este o problemă de tip
NP-hard. Soluțiile nu pot fi decât euristice, negarantând optimalitatea.

BRANCH <cond_1>, T1
…; Not Taken
T1: INSTR; Taken

BRANCH <cond_2>, T2
…; Not Taken
n
T2: INSTR; Taken ∑C
k =0
k
n =2n posibile căi (trace-uri)



BRANCH <cond_n>, Tn
…; Not Taken
Tn: INSTR; Taken

Mai jos se prezintă și un exemplu mai favorabil de structură de program,


în care la 2n-1 branch-uri corespund practic doar 2n-1 posibile căi distincte de
rulare.

290
Figura 4.18. Model de program cu 2n-1 branch-uri și 2n-1 căi
Aşadar, o cale a unui program va traversa mai multe unităţi secvenţiale din
acel program. În figura următoare (Figura 4.19) se prezintă un program compus
din doar două căi distincte, şi anume cele numite TRACE1 şi TRACE2.

Figura 4.19. Exemplu de trace-uri pe o secvenţă de program

291
Tehnica TS este similară cu tehnicile de reorganizare în "basic-block"-uri,
cu deosebirea că aici se va reorganiza o întreagă cale şi nu doar un singur basic -
block. În esenţă, ea se bazează pe optimizarea celor mai probabile căi în a fi
executate (o posibilă euristică). Așadar, sunt necesare metode statice de predicție
a căilor celor mai probabile pentru instrucțiunile de salt condiționat. Spre
exemplificare, să considerăm o secvenţă de program C şi secvenţa
corespunzătoare de program obiect, obţinută prin compilare:

a[ i ] = a[ i ]+1;
if (a[ i ] < 100) {
count = count +1;
*(b + sum) = *( b + sum) + a[ i ];}

1: SLL R1, i, 2; Shift Logic Left variabilă i, cu 2 poziții


2: ADD R1, R1, base_a; adresa lui a[i] R1
3: LD R2, (R1); a[ i ] R2
4: ADD R2, R2, 1; a[ i ]+1 R2
5: ST R2, (R1); asignare a[ i ]
6: CPLT R1, R2, 100; a[ i ] < 100?
7: JMPF R1, LABEL
8: ADD count, count, #1
9: ADD R1, base_b, s_off; adresa lui b [sum] R1
10: LD R3, (R1)
11: ADD R3, R3, R2
12: ST R3, (R1)
LABEL: ...

Pentru a putea aplica TS, compilatorul trebuie să aibă criterii rezonabile de


predicţie a salturilor condiţionate, în vederea construirii căilor cu cea mai mare
probabilitate de execuţie, care să fie optimizate. În aceste căi se consumă cea
mai mare parte a timpului de procesare. În general, predicţia software a branch-
urilor se face pe baza analizei statice a programului (spre exemplu, instrucțiunile
de tip LOOP, de la finele unei bucle de program, vor fi predicționate, evident, ca

292
fiind Taken) dar și a informaţiilor rezultate din anterioare execuţii ale
programului neoptimizat (profilings) sau a altor algoritmi euristici, înglobaţi în
compilator (scheduler). Execuţia secvenţei anterioare pe un procesor superscalar
care decodifică 4 instrucţiuni simultan şi deţine 5 unităţi de execuţie, se va face
ca în Tabelul 4.15. S-a considerat că toate instrucțiunile se execută într-un singur
ciclu, cu excepția celor de tip LOAD, care necesită doi cicli. De asemenea, s-a
considerat că saltul condiţionat, preponderent nu se va face şi că procesorul
execută instrucțiunile In Order (la nivelul ferestrelor dinamice de 4 instrucțiuni).

Tabelul 4.15. Execuţia trace-ului neoptimizat

293
Figura 4.20. Graful de control aferent secvenţei de optimizat

Datorită paralelismului limitat al acestei secvenţe de program se obţine o


rată medie de execuţie de doar o instrucțiune / ciclu. Această performanță medie
este una foarte modestă, având în vedere că procesorul este unul “cu mușchi”,
având 4 unități de decodificare și 5 unități de execuție, din păcate foarte puțin
utilizate în procesarea secvenței de instrucțiuni considerate, după cum se poate
constata din Tabelul 4.15. Este încă o dovadă că dacă software-ul nu se pliază pe
caracteristicile arhitecturii hardware, performanțele nu vor fi cele dorite, în ciuda
faptului că procesorul este unul performant, în sine. Iată de ce optimizarea
programului, ținând cont de caracteristicile arhitecturii hardware, este absolut
necesară. Graful de control al acestei secvenţe este complus din doar două basic
block-uri interconectate, ca în Figura 4.20.

294
Figura 4.21. Graful dependenţelor pentru trace-ul considerat

Se va considera, în continuare, că programul compilator a ales calea prin


care saltul nu se face, ca fiind cea mai probabilă, spre a fi optimizată, ca pe un
singur basic-block, prin intermediul alogoritmului List Scheduling, anterior
descris. Aşadar, instrucţiunea de salt condiționat va fi tratată în acest caz ca
oricare alta. Iată de ce algoritmul de optimizare globală se numește Trace
Scheduling, fiind o generalizare a celui numit List Scheduling, la nivelul unei
întregi căi (trace). Graful precedenţelor de date asociat secvenţei anterioare de
program este prezentat în Figura 4.21. Pentru a elimina dependenţele WAR şi
WAW între cele două ramuri paralele, vom redenumi R1 cu un alt registru
disponibil (spre exemplu RS1) pe ramura 9, 10, 11, 12. De remarcat că această
problemă se datorează alocatorului de regiştri din cadrul compilatorului.
Instrucțiunea 8, fiind independentă de celelalte, se poate procesa în oricare ciclu.
Aplicând acum algoritmul LS pe graful dependenţelor de date aferent căii
respective, obţinem următoarea ordine inversă de execuţie (v. Tabelul 4.16).
Latența căii critice este de 7 cicli în acest caz.

295
Tabelul 4.16. Ordinea de execuţie în urma optimizării

Aşadar, execuţia se va face în 7 cicli, ca mai jos:

1)1 1: SLL R1, i, 2


2)2 2: ADD R1, R1, base_a
3)3,9 3: LD R2, (R1)
9)10 9: ADD RS1, base_b, s_off
5)4,8 10: LD R3, (RS1)
6)5,6,11 4: ADD R2, R2, 1
7)12,7 8: ADD count, count, 1
5: ST R2, (R1)
6: CPLT R1, R2, 100
11: ADD R3, R3, R2
12: ST R3, (RS1)
7: JMPF R1, LABEL
LABEL: ...

În consecință, prin suprapunerea operaţiilor din cele două basic block-uri s-


a obţinut o rată medie de procesare de 1.71 instrucțiuni / ciclu, superioară celei
de dinaintea optimizării. Problema care apare însă, este următoarea: ce se
întâmplă dacă saltul se face ? Răspunsul ar consta în utilizarea unor coduri de
compensaţie pentru instrucțiunile incorect executate în acest caz (pentru că în
urma optimizării, instrucțiunile 8-12 se vor executa speculativ!) Tehnica TS
presupune mutarea instrucţiunilor dintr-un basic block în altul şi această acțiune

296
presupune anumite compensaţii în vederea păstrării corectitudinii programului
reorganizat. Prezentăm mai jos compensaţiile necesare atunci când o
instrucţiune este mutată dintr-un basic block într-unul succesor, respectiv într-
unul predecesor (Figura 4.22).

Figura 4.22. Compensaţii la migrarea instrucţiunilor dintr-un basic-block în


altul
În exemplul prezentat am avut o migrare a întregului bloc 8-12 în blocul
precedent 1-7, deci compilatorul (inteligent!) va trebui să introducă în al 2-lea
bloc, instrucţiuni de compensare, în vederea anulării unor operaţii, absolut
necesare atunci când saltul se face, ca mai jos:

1: SLL R1, i, 2
2: ADD R1, R1, base_a
3: LD R2, (R1)
9: ADD RS1, base_b, s_off
10: LD R3, (RS1)
4: ADD R2, R2, 1
8: ADD count, count, 1
5: ST R2, (R10)
6: CPLT R1, R2, 100

297
11: ADD RS3, R3, R2
12: ST RS3, (RS1)
7: JMPF R1, C2
C1: JMP LABEL
C2: SUB count, count, 1
C3: ST R3, (RS1)
LABEL:

Anularea operației count = count +1 s-a făcut simplu, prin introducerea


instrucțiunii de compensare C2, anume SUB count, count, 1. În vederea anulării
operaţiei speculative *(b+sum) = *(b+sum) + a[ i ] în cazul în care saltul se
face, s-a introdus de către compilator linia C3. Pentru ca această anulare să fie
posibilă, a fost necesară redenumirea registrului R3 cu un altul disponibil (RS3
aici) în instrucţiunile I11 şi I12. Altfel, instrucţiunea I11 ar fi alterat R3 şi deci
anularea asignării variabilei *(b+sum) în memorie ar fi fost imposibilă.
Registrul RS3 conține asignarea de pointeri, iar registrul R3 rămâne intact,
putând fi restaurat la nevoie. Desigur că atunci când instrucțiunea de salt
condiționat JMPF R1, LABEL se va face, execuția codurilor compensatoare
adiționale înrăutățește perormanța de execuție a programului. Din fericire acest
fapt este compensat din plin de faptul că, atunci când acest salt condiționat nu se
va face, ceea ce se întâmplă mai frecvent, execuția programului este mult
îmbunătățită. De remarcat similitudinea între corecţiile software şi tehnicile
hardware de execuţie speculativă, prin care se redenumesc în mod dinamic
resursele procesorului. În continuare, se prezintă o serie de caracteristici
arhitecturale cu scopul facilizării implementării tehnicii TS. Compensarea este
eficientă numai dacă ciclii suplimentari introduşi prin aceasta nu depăşesc
numărul ciclilor eliminaţi prin tehnica TS. O compensare eficientă presupune:
- O acurateţe ridicată a predicţiei statice a branch-urilor de către compilator,
obţinută prin statistici rezultate din rularea programului și analiza codurilor
salturilor. În acest sens, programele numerice se pretează mai bine la
optimizarea prin TS decât cele de uz general.
- Procentaj relativ scăzut al instrucţiunilor de ramificaţie din program.
- Codurile de compensaţie să fie posibile şi fezabile.

298
- Paralelism hardware pronunţat în vederea procesării codurilor de
compensare, cu introducerea unui număr minim de cicli suplimentari.
După cum am mai precizat, într-un program dat, reorganizatorul selectează
căile pentru scheduling, utilizând tehnici de predicţie software a branch-urilor
(predicție statică). Pentru început, se aplică algoritmul TS pentru calea cea mai
probabilă de a fi executată. Pe timpul acestui proces se vor adăuga coduri de
compensare. Apoi se selectează următoarea cale considerată cea mai probabilă.
Aceasta va fi de asemenea reorganizată prin tehnica TS. Reorganizarea se referă
inclusiv la posibilele coduri compensatoare, introduse de către procesul anterior
de reorganizare. Algoritmul TS se va repeta pentru fiecare cale în parte. Pentru
limitarea timpului de reorganizare, referitor la căile mai puţin frecvente în
execuţie, se poate renunţa la optimizarea globală în favoarea optimizării exclusiv
a basic block-urilor componente, prin algoritmul LS. Nu există încă criterii
clare, care să stabilească momentul în care optimizarea TS să fie abandonată
(condiția de oprire a algoritmului de optimizare).
O situaţie oarecum ironică este legată de optimizarea programelor pentru
procesoare superscalare cu procesări out of order ale instrucțiunilor, datorită
faptului că, în acest caz, logica hardware de lansare în execuţie a instrucţiunilor
va relua în mod redundant căutarea instrucţiunilor independente (detectate prin
scheduling-ul static). Această operaţie este inutilă, fiind realizată anterior de
către optimizatorul software. Din acest motiv, acesta ar trebui să marcheze
grupele de instrucţiuni paralelizate, astfel încât hardware-ul să nu mai reia inutil
această operaţie. Într-un asemenea context, procesarea Out of Order a
instrucţiunilor – caracteristică majoră şi complexă a multor procesoare
superscalare – apare ca fiind complet inutilă. Este încă o dovadă a faptului că
paradigma procesării instrucţiunilor trebuie să fie una relativ simplă, dar în
acelaşi timp nu mai simplă decât este necesar, în virtutea unor principii de
eficienţă, flexibilitate şi compatibilitate.
Pe de altă parte, scheduling-ul static nu este întotdeauna posibil să lucreze
în tandem cu cel dinamic. Iată un exemplu simplu în acest sens. Să considerăm
secvența de două instrucțiuni mașină succesive (notate cu Ij și Ik), prima cu
execuție condiționată (se execută doar dacă variabila booleană B2 este 1 logic),
ca mai jos:

299
Ij:TB2 ADD R1, R3, R4
Ik: SUB R6, R1, R12

Într-un procesor superscalar cu execuții out of order ale instrucțiunilor,


această secvență poate crea probleme în procesarea dinamică. La înscrierea
instrucțiunii Ik în stația de rezervare corespunzătoare, dacă registrul R1 nu este
încă disponibil, nu se va ști ce să se înscrie în câmpul Q din stația de rezervare.
Aici s-ar putea înscrie numele unității de execuție care va produce rezultatul
instrucțiunii Ij (R1). Din păcate, dacă în acest moment valoarea garzii B2 nu este
cunoscută, atunci nu se va putea ști cu certitudine ce să se scrie în cîmpul Q
aferent stației de rezervare în care este memorată instrucțiunea Ik. Dacă la un
moment ulterior B2 va fi 0 logic, atunci în acest câmp Q va trebui să se scrie
numele unității de execuție aferente instrucțiunii care îl va produce pe R1 (pe
post de destinație). Aceasta va fi o instrucțiune care precede instrucțiunea Ij din
program. Dacă însă B2 va fi 1 logic, atunci în câmpul Q se va scrie numele
unității de execuție care a procesat rezultatul instrucțiunii Ij. Putem scrie
schematic: Ik (R1)_Q_Tag în SR? Se scrie aici Ij sau o altă instrucțiune
precedentă lui Ij, care produce R1? Iată deci că scheduling-ul static nu este
compatibil în acest caz cu cel dinamic, pentru că Ik nu se mai poate executa în
paralel cu Ij, tocmai datorită gardării lui Ij prin software (dacă nu s-ar fi utilizat
execuția condiționată, nu apărea problema specificată.) Mai general, renaming-
ul prin software ca și execuția condiționată a instrucțiunilor generează noi
dependențe de tip RAW între instrucțiuni, care desigur că reduc posibilitățile de
procesare paralelă a instrucțiunilor.

4.8. OPTIMIZAREA BUCLELOR DE PROGRAM

Această optimizare este foarte importantă pentru că în buclele de program


se consumă cea mai mare parte a timpului de execuţie aferent respectivului
program, după regula statistică binecunoscută care afirmă că aproximativ " 90%
din timp se execută cca. 10% din program". Iată de ce optimizarea buclelor

300
reprezintă o preocupare esenţială la ora actuală, implementată în majoritatea
compilatoarelor.

4.8.1. TEHNICA "LOOP UNROLLING"

Pentru ilustrarea principiului de optimizare, să considerăm următoarea


buclă de program:
for (i = 0; i < 64; i + +) {
sum + = a[ i ];}
Din compilarea acestei bucle rezultă următoarea secvenţă de program
obiect de tip RISC:

LOOP: 1:LD R1, (a_ptr); latență 2 cicli


2:ADD a_ptr, a_ptr, 4
3:CLT count, a_ptr, end_ptr
4:ADD sum, sum, R1
5:JMPT count, LOOP

Execuţia a două iteraţii succesive pe un procesor superscalar "in order


issue" se va desfăşura ca în tabelul de mai jos (Tabelul 4.17).

Tabelul 4.17. Execuţia buclei neoptimizate

301
S-a considerat că procesorul aduce 4 instrucţiuni simultan şi deţine 5 unităţi
de execuţie distincte (BRN - unitate de execuţie a instrucţiunilor de ramificaţie).
Se remarcă faptul că, în acest caz, rata medie de procesare este de 1.25
instrucțiuni / ciclu sau, altfel spus, de 0.25 bucle/ciclu. După aplicarea tehnicii
Loop Unrolling de două ori (două iterații succesive desfășurate - unrolled)
secvenţa anterioară devine:

LOOP: 1: LD R1, (a_ptr)


2: ADD a_ptr, a_ptr, 4
3: ADD sum, sum, R1
4: LD R1, (a_ptr)
5: ADD a_ptr, a_ptr, 4
6: ADD sum, sum, R1
7: CLT count, a_ptr, end_ptr
8: JMPT count, LOOP

Reorganizând basic block-ul precedent prin metoda LS anterior descrisă,


obţinem următoarea secvenţă de program (justificați!):

LOOP: 1: LD R1, (a_ptr)


2: ADD a_ptr, a_ptr, 4
4: LD R2, (a_ptr)
5: ADD a_ptr, a_ptr, 4
7: CLT count, a_ptr, end_ptr
3: ADD sum, sum, R1
6: ADD sum, sum, R2
8: JMPT count, LOOP

Execuţia acestei bucle pe acelaşi procesor superscalar se va face ca în


Tabelul 4.18:

302
Tabelul 4.18. Execuţia buclei optimizate

S-a obţinut astfel o rată medie de procesare de 2 instrucțiuni / ciclu sau 0.5
bucle / ciclu, deci practic performanţa s-a dublat faţă de exemplul precedent.
Explicația acestui fapt pozitiv este simplă, întrucât în două basic-block-uri există
mai mult paralelism la nivelul instrucțiunilor, decât într- unul singur. Desigur că
bucla inițială se putea desfășura de 4, 8, 16, 32 ori (divizori ai lui 64), cu
beneficii asupra performanței. Dacă bucla se desfășoară mai mult, crește
paralelismul la nivel de instrucțiuni (prin faptul ca se poate exploata ILP la
nivelul mai multor basic-block-uri), dar, pe de altă parte, crește și lungimea
buclei optimizate, cu repercursiuni negative asupra ratei de miss în cache.
Optimalitatea desfășurării buclei constă într-un proces fin de tuning, care să
găsească un compromis optimal între avantajele și dezavantajele implicate.
Se pune acum problema: ce se întâmplă dacă numărul de iteraţii este
necunoscut în momentul compilării ? Pentru aceasta să considerăm secvenţa de
mai jos:
for (i = 0; i < n ; i + +) {
sum + = a[ i ] ;
}
Această secvenţă de program va fi compilată ţinând cont şi de aplicarea
tehnicii LU de desfășurare a buclei, ca mai jos:

LOOP: 1: LD R1, (a_ptr)


2: ADD a_ptr, a_ptr, 4
3: ADD sum, sum, R1
4: CLT count, a_ptr, end_ptr
5: JMPF count, EXIT
6: LD R1, (a_ptr)
7: ADD a_ptr, a_ptr, 4

303
8: ADD sum, sum, R1
9: CLT count, a_ptr, end_ptr
10: JPMT count, LOOP
EXIT:

Graful de control corespunzător acestei secvenţe este prezentat în Figura


4.15.

Figura 4.23. Graful de control asociat buclei netezite

Aşadar, în astfel de cazuri, după aplicarea tehnicii LU se obţine o cale


formată din două sau mai multe basic block-uri concatenate. Pentru optimizare
se poate aplica algoritmul TS asupra grafului de control, pe calea cea mai
probabilă (în cazul concret considerat, în care saltul condiționat JMPF count,
EXIT nu se face), dar aceasta poate complica şi reduce sever eficienţa tehnicii
Loop Unrolling.

4.8.2. TEHNICA "SOFTWARE PIPELINING"

304
Tehnica software pipelining (TSP) este utilizată în reorganizarea buclelor
de program, astfel încât fiecare iteraţie a buclei de program reorganizată să
conţină instrucţiuni aparţinând unor iteraţii diferite din bucla originală. Această
reorganizare are drept scop scăderea timpului de execuţie al buclei prin
eliminarea hazardurilor intrinseci, eliminând astfel stagnările inutile pe timpul
procesării instrucţiunilor. Prezentăm principiul acestei tehnici bazat pe un
exemplu simplu. Aşadar să considerăm secvenţa de program următoare:

LOOP: LD F0, 0(R1)


ADD F4, F0, F2
SD 0(R1), F4
SUBI R1, R1, #8
BNEZ R1, LOOP

Bucla realizează modificarea unui tablou de numere reprezentate în virgulă


mobilă, prin adăugarea unei constante conţinută în registrul F2 la fiecare dintre
acestea. De remarcat că hazardurile RAW între instrucţiunile I1, I2 respectiv I2,
I3 determină stagnări în procesarea pipeline a instrucţiunilor acestei bucle. Nu
există paralelism ILP la nivelul unei iterații a buclei, datorită dependențelor
RAW de date arătate. Într-o primă fază, TSP desfăşoară, însă doar în mod
simbolic, bucla în iteraţii succesive, eliminând actualizarea contorului şi saltul
înapoi.

iteraţia i: LD F0, 0(R1)


ADD F4, F0, F2
SD 0(R1), F4
iteraţia i+1: LD F0, 0(R1)
ADD F4, F0, F2
SD 0(R1), F4
iteraţia i+2: LD F0, 0(R1)
ADD F4, F0, F1
SD 0(R1), F4

305
În a doua fază, instrucţiunile selectate din cele trei iteraţii se grupează în
cadrul unei noi bucle, ca mai jos:

LOOP: SD 0(R1), F4; memorează în M(i), iterația nr. i


ADD F4, F0, F2; modifică scalarul M(i+1)
LD F0, - 16(R1); încarcă elementul M(i+2)
SUBI R1, R1, #8;
BNEZ R1, LOOP;

Observăm că prin pipeline-izarea software a instrucţiunilor din cadrul


acestei bucle reorganizate s-au eliminat hazardurile de date anterioare şi deci
stagnările implicate de către acestea. Practic s-a înțeles că nu există ILP la
nivelul unei iterații a buclei și deci paralelismul trebuie exploatat la nivelul a trei
iterații succesive. Bucla se execută pe un procesor pipeline scalar în doar 5
impulsuri de tact, adică la viteză maximă, asumând o predicţie perfectă a saltului
final. De remarcat că pe un procesor superscalar cu suficiente unităţi de
execuţie, primele trei instrucţiuni s-ar putea executa simultan, ceea ce pe
varianta originală a buclei nu se putea, din cauza hazardurilor RAW intrinseci
iterației curente. Aşadar TSP este deosebit de eficientă pe procesoarele cu
execuţie multiplă a instrucţiunilor. De multe ori este necesară intervenţia
hardului sau a softului pentru eliminarea posibilelor hazarduri WAR/WAW care
apar în cadrul buclei reorganizate. Totuşi, pentru a fi funcţională, bucla
anterioară are nevoie de un preambul şi respectiv de un postambul. Preambulul
necesar constă în execuţia instrucţiunilor din iteraţiile 1 şi 2 care nu au fost
executate în cadrul buclei (LD - 1, LD - 2, ADD - 1). Analog, postambulul
constă în execuţia instrucţiunilor care nu au fost executate în ultimele două
iteraţii (ADD - ultima, SD - ultimele 2 iteraţii).

LD F0, (R1) iteratia 1


ADD F4, F0, F2 iteratia 1 PREAMBUL
LD F0, -8(R1) iteratia 2

LOOP: SD 0(R1), F4; iteratia (i)/1/n-2; memorare


ADD F4, F0, F2; (i+1)/2/n-1; prelucrare

306
LD F0, - 16(R1); (i+2)/3/n; încărcare
SUBI R1, R1, #8;
BNEZ R1, LOOP;

SD (R1), F4 (n-1)-a iteratie


ADD F4, F0, F2 (n), ultima iteratie POSTAMBUL
SD -8(R1), F4 (n)

Alături de tehnica LU, TSP este des utilizată în optimizarea buclelor de


program. Spre deosebire de LU, aceasta consumă mai puţin spaţiu de cod.
Frecvent, cele două tehnici sunt combinate, în vederea creşterii performanţei.
Tehnica LU se concentrează pe eliminarea codurilor redundante din cadrul
buclelor (actualizări contoare, loop-uri). Bucla se procesează la viteză maximă
doar pe parcursul iteraţiilor desfăşurate. TSP încearcă, în schimb, obţinerea unei
viteze mari de execuţie pe întreg timpul procesării buclei. Toate compilatoarele
moderne implementează tehnici de optimizare a basic-block-urilor și a buclelor
de program (v. spre exemplu opțiunile de optimizare ale compilatorului GCC).

4.9. ARHITECTURI CU TRANSPORT DECLANŞAT

Arhitecturile cu transport declanşat (TTA - Transport Triggered


Architectures) reprezintă o expresie limită a filosofiei RISC de exploatare a
paralelismului instrucţiunilor, având practic o singură instrucțiune cu execuție
condiționată (MOVE). Apariţia lor (Henk Corporaal și Hans Mulder,
Universitatea din Delft, Olanda, 1991-1992) a fost determinată de slăbiciunile
modelelor superscalar şi VLIW, datorate în special gradului scăzut de utilizare al
resurselor hardware (unităţi funcţionale, busuri interne, regiştri generali etc) şi
complexităţii ridicate.
Esenţa TTA constă în separarea transportului datelor, de prelucrarea lor
efectivă. Dacă în arhitecturile convenţionale (numite, prin contrast, OTA -
Operation Triggered Architectures) declanşarea execuţiei unei operaţii este
iniţiată chiar de către operaţia (instrucţiunea) respectivă, în cadrul TTA această

307
declanşare este iniţiată în mod automat de către un transport al operandului sursă
într-un aşa-numit registru trigger (T), ataşat fiecărei unităţi funcţionale din
sistem. Un procesor TTA este compus din unităţi funcţionale complet
independente, interconectate printr-o aşa-zisă reţea de transport. Fiecare unitate
funcţională deţine trei regiştri: registrul operand (O), registrul trigger (T), care
declanșează la încărcare în mod automat operația respectivă şi registrul rezultat
(R). Transportul şi înscrierea unui operand în registrul T, determină automat
activarea unităţii funcţionale respective. După cum am mai subliniat, un
procesor TTA deţine o singură instrucţiune (MOVE - transfer), care se poate
executa condiţionat pe maximum două variabile de gardă de tip boolean.
Planificarea mutărilor între diversele unităţi funcţionale (FU - Functional Unit)
devine o sarcină software a compilatorului, nemaifiind integrată hardware în
CPU, ca în cazul clasic de procesor (OTA). Desigur că unităţile funcţionale cu
latenţă mai mare de un tact pot fi pipeline-izate, cu beneficii asupra timingului
de execuție a instrucțiunilor. De menţionat că, având o singură instrucţiune,
logica de decodificare TTA practic nu există. Cele două figuri care urmează,
4.24 şi 4.25, prezintă schema bloc de principiu a unui procesor TTA şi respectiv
pipeline-izarea unei anumite unităţi funcţionale din cadrul acestuia.

Figura 4.24. Schema bloc a unei arhitecturi TTA

308
Figura 4.25. Pipeline-izarea execuţiei în cazul unei micro-arhitecturi TTA
În continuare se prezintă două exemple sugestive: o "instrucţiune" de
adunare şi respectiv una de salt condiţionat, implementate (emulate) în TTA.

ADD R3, R2, R1 R1-->ADD_O, R2-->ADD_T (declanşare)


ADD_R-->R3

if R2 = R3 goto Adr R2-->EQ_O, R3-->EQ_T


EQ_R-->B1 (var. booleană)
[B1]Adr-->PC (execuţie condiţionată de val. B1)

De remarcat ca sunt posibile mai multe transporturi simultane, funcţie de


lărgimea de bandă a reţelei de transport. Intervalul de timp dintre declanşare şi
mutarea rezultatului trebuie controlat prin software, astfel încât să acopere
latenţa unităţii funcţionale respective.

Avantajele TTA

Datorită simplităţii hardware a logicii de decodificare, control şi planificare


a instrucţiunii, procesoarele TTA permit frecvenţe de tact extrem de ridicate.
Mai mult, separarea unităţilor funcţionale de reţeaua de transport, permite
pipeline-izarea optimală a FU. Numărul de regiştri generali poate fi redus
drastic, datorită faptului că trebuie stocate mai puţine date temporare, multe

309
dintre aceste date circulând direct între FU-uri, fără ca să trebuiască să fie
memorate într-un registru de uz general. Stagiile pipeline şi regiştrii operanzi
sunt utilizaţi pe post de regiştri temporari. Un rezultat produs de o FU, dar care
nu poate fi utilizat direct de alt FU, poate fi lăsat temporar în unitatea FU care l-
a produs, dacă acesta nu blochează alte rezultate anterior necesare, ale aceleiaşi
FU. Interesant, aici bypassing-ul (forwarding-ul) datelor este gestionat prin
program, spre deosebire de procesoarele superscalare care realizează acest
proces prin hardware (vezi Algoritmul lui Tomasulo). Spre exemplificare, se
consideră două instrucţiuni RISC dependente RAW prin registrul R3, ca mai jos:

ADD R3, R1, R2 R1 ADD_O, R2 ADD_T;


ADD R5, R3, R4 ADD_R ADD_O, R4 ADD_T, ADD_R R3;
ADD_R R5;

Se observă forwarding-ul realizat prin software (ADD_R ADD_O) şi care


optimizează execuţia cu un ciclu. Ca urmare se reduce presiunea la citire asupra
setului de regiştri generali, analog cu mecanismul lui Tomasulo, doar că aici prin
software. Dacă în continuare compilatorul constată că timpul de viaţă aferent
registrului R3 s-a încheiat, secvenţa anterioară devine mai simplă:

ADD R3, R1, R2 R1 ADD_O, R2 ADD_T


ADD R5, R3, R4 ADD_R ADD_O, R4 ADD_T
ADD_R R5

Astfel se reuşeşte oprirea scrierii inutile în registrul R3 şi deci reducerea


presiunii la scriere asupra setului de regiştri, lucru principial imposibil la nivelul
procesoarelor OTA.
Un alt avantaj al arhitecturilor TTA constă în flexibilitatea şi scalabilitatea
performanţelor (similar cu rețelele de calculatoare). Flexibilitatea constă într-o
facilă schimbare a funcţionalităţii, în concordanţă cu aplicaţia specifică.
Scalabilitatea este determinată în principal de posibilitatea adăugării de noi FU-
uri, fără modifcări arhitecturale majore şi de mărirea capacităţii de transport în
vederea creşterii gradului de paralelism. Divizarea operaţiilor în transporturi

310
elementare oferă un grad de paralelism mai fin, care determină posibilităţi mai
eficiente de scheduling static şi deci performanţe superioare.
Structurile TTA se pretează foarte bine la proiectarea de procesoare
dedicate unor aplicaţii specifice (ASP-Application Specific Processor),
parametrii FU şi ai reţelei de transport putând fi setaţi în acord cu aplicaţia ţintă.
Deşi mediatizate intens în literatura de specialitate, TTA-urile nu constituie în
opinia autorului acestei cărți tehnice, o paradigmă novatoare a conceptului de
procesor, reprezentând mai degrabă o superclasă a arhitecturilor VLIW
tradiţionale, cu deosebirea că "sparg" atomicitatea instrucţiunii maşină în aşa-
zise transporturi. Se permite astfel un control mai mare al execuţiei din partea
compilatorului, cu posibilităţi superioare de optimizare a codului. Performanţele
par remarcabile, depăşind la nivel de simulare, respectiv implementare cu
FPGA-uri, cu 25-50% variante OTA echivalente (I-860).
Cu toate că la ora actuală aceste procesoare există probabil doar la nivel de
prototip, firme comerciale importante precum Intel şi Hewlett Packard au iniţiat
cercetări în domeniul TTA şi deci n-ar fi deloc exclus ca unele idei să se
regăsească în viitoarele microprocesoare avansate produse de aceste companii.

4.10. EXTENSII ALE ARHITECTURILOR MEM PE BAZĂ DE


REUTILIZARE ȘI PREDICȚIE A INSTRUCȚIUNILOR

Acest paragraf este scris pe baza unor texte prelucrate și actualizate, dintr-o
monografie tehnică publicată de autor anterior [Vin02]. Din punct de vedere
arhitectural, mai precis din punct de vedere al procesării instrucțiunilor, se
consideră că până la ora actuală au existat trei generaţii de (micro)procesoare de
succes comercial, după cum urmează:
- generaţia I, caracterizată în principal prin execuţia secvenţială a fazelor
(ciclilor maşină) aferente instrucţiunilor- maşină dar și a instrucțiunilor. Pionierii
acestei generaţii sunt desigur inventatorii calculatorului numeric, în principal
inginerii Eckert şi Mauchly, alături de cel care ulterior a teoretizat şi a îmbogăţit
conceptul, în persoana marelui om de ştiinţă american John von Neumann.

311
- generaţia a II-a de procesoare, exploata paralelismul temporal aferent
instrucţiunilor maşină prin suprapunerea fazelor (pipeline). Primul reprezentant
comercial a fost sistemul CDC-6600 (1964) proiectat de către cel mai mare
creator de calculatoare de înaltă performanţă şi totodată unul dintre pionierii
supercalculatoarelor, Seymour Cray. În anii '80, (micro)procesoarele RISC
scalare au reprezentat această generaţie (Dr. J. Cocke de la IBM şi Prof. D.
Patterson de la Univ. Berkeley fiind doar doi dintre pionierii promotori ai
acestor idei, după cum am mai menționat).
- generaţia a III-a, utilizată și în prezent, este caracterizată de procesarea
mai multor instrucţiuni independente simultan, prin exploatarea unui paralelism
spaţial la nivelul diverselor unităţi funcţionale de procesare. Execuţia
instrucţiunilor se face Out of Order, utilizând deci tehnici de reorganizare
(dinamică sau statică) a instrucţiunilor în vederea minimizării timpului global de
execuţie. Pionierul acestei generaţii a fost sistemul anilor '60 IBM-360/91
(printre proiectanţi Anderson, Sparacio, Tomasulo, Goldschmidt, Earle etc.). La
ora actuală generaţia aceasta este reprezentată prin microprocesoarele
superscalare, VLIW, hyperthreading etc.
- generația a IV-a, care a apărut din punct de vedere comercial prin anul
2004 (Intel), și care integrează mai multe procesoare de generația a 3-a pe un
singur cip, într-un sistem de calcul paralel (multicore). Se exploatează pe lângă
paralelismul la nivelul instrucțiunilor și cel la nivelul firelor de execuție și al
proceselor. Necesită paradigme concurente de programare HLL.
De câţiva ani, în laboratoarele de cercetare (în special cele academice!) se
întrezăresc câteva soluţii privind caracteristicile majore ale următoarei decade,
generaţia a “3-a îmbunătățită” am numi-o, pe care le vom analiza succint şi
fatalmente incomplet, în continuare. O analiză mai detaliată se găsește în
monografia noastră [Vin07].
În ultimii ani, procesul de proiectare al procesoarelor s-a modificat radical.
Astăzi, accentul principal nu se mai pune doar pe implementarea hardware, ci pe
proiectarea arhitecturii hardware-software într-un mod holistic, integrator. Se
porneşte de la o arhitectură de bază, care este modificată şi îmbunătăţită
dinamic, prin simulări laborioase pe benchmark-uri reprezentative (Stanford,
SPEC '92, '95, '06, SPLASH-2 etc., pentru procesoarele de uz general). De
exemplu, proiectanţii firmei Intel, pentru procesorul Intel Pentium Pro (P6), au

312
pornit de la o structură iniţială care conţinea un pipeline cu 10 nivele,
decodificator cu 4 instrucţiuni / ciclu, cache-uri separate pe instrucţiuni şi date
de capacitate 32 KO fiecare şi un total de 10 milioane tranzistori. Comportarea
fiecărei componente a arhitecturii (efectul capacităţii primului nivel (L1) cache,
numărul de nivele în pipeline, comportarea logicii de predicţie a salturilor
condiționate, numărul de unităţi funcţionale etc.) a fost simulată prin software,
prin rularea a aproximativ 200 benchmark-uri, cu peste două miliarde de
instrucţiuni! Rezultatul final a impus un procesor cu un pipeline pe 14 nivele, 3
instrucţiuni decodificate în fiecare ciclu, 8 KO L1 cache de date şi 8 KO L1
cache de instrucţiuni, cu un total de aproximativ doar 5.5 milioane tranzistoare
integrate.
Costul proiectării este relativ mare şi include, în principal, elaborarea unei
arhitecturi dinamice (puternic parametrizabile), scrierea unui compilator, de
C/C++ în general, pe arhitectura respectivă, scheduler (optimizator) pentru codul
obiect, simulator puternic parametrizabil si complex, programe de interpretare a
rezultatelor. Spre exemplu, microprocesorul MIPS-4000 s-a creat prin efortul a
30 de ingineri, timp de 3 ani. Costul cercetării-proiectării a fost de 30 milioane
dolari, iar cel al fabricării efective de numai 10 milioane dolari. Numai pentru
simulare şi evaluare s-au consumat circa 50.000 ore de procesare pe maşini
având performanţe de peste 20 MIPS.
Oricum, arhitecturile cu execuţii multiple şi pipeline-izate ale
instrucţiunilor (superscalare, VLIW etc.) dau deja anumite semne de "oboseală",
limitările fiind atât de ordin tehnologic (în special densități de putere mari –
Power wall, implicând disipații termice tot mai semnificative și mai dificil de
efectuat) cât şi arhitectural. Caracteristicile arhitecturale complexe implică
tehnologii tot mai sofisticate, unele dintre acestea încă nedisponibile. Pe de altă
parte, performanţele lor cresc doar asimptotic pe actualele paradigme
arhitecturale. Totuşi, schimbări fundamentale sunt mai greu de acceptat în
viitorul apropiat, în primul rând datorită compilatoarelor optimizate, având drept
scop exploatarea mai pronunţată paralelismului la nivel de instrucţiuni și micro-
fire de execuție, deoarece acestea sunt deosebit de complexe şi puternic
dependente de caracteristicile hardware.
Există deja opinii care arată că arhitecturile superscalare şi VLIW conţin
limitări fundamentale şi care ar trebui analizate şi eventual eliminate. Dintre

313
aceste limitări, amintim doar conflictele la resurse, datorate în principal
centralizării acestora. O idee interesantă, bazată pe descentralizarea resurselor,
are în vedere implementarea mai multor aşa numite "Instruction Windows" (IW)
- un fel de buffere de prefetch multiple, în locul unuia singur şi respectiv pe
conceptul de multithreading. Lansarea în execuţie a instrucţiunilor se face pe
baza determinării celor independente din fiecare IW. Desigur că trebuie
determinate şi dependenţele inter- IW- uri. Ideea principală constă în execuţia
paralelă a mai multor secvenţe de program aflate în IW- uri diferite, bazat pe
mai multe unităţi funcţionale (multithreading). Astfel, spre exemplu, două
iteraţii succesive aferente unei bucle de program pot fi procesate în paralel dacă
sunt memorate în IW- uri distincte. O asemenea idee facilitează implementarea
conceptelor de expandabilitate şi scalabilitate, deosebit de utile în dezvoltarea
viitoare a arhitecturii.
În esenţă, un procesor cu execuţii multiple ale instrucţiunilor este compus
din două mecanisme decuplate: mecanismul de aducere (fetch) a instrucţiunilor,
pe post de producător, şi respectiv mecanismul de execuţie a instrucţiunilor, pe
post de consumator. Separarea între cele două mecanisme (arhitectură decuplată)
se face prin bufferele de instrucţiuni şi staţiile de rezervare, ca în Figura 4.26.
Instrucţiunile de ramificaţie şi predictoarele hardware aferente acţionează printr-
un mecanism de reacţie (feed-back) între consumator şi producător. Astfel, în
cazul unei predicţii eronate, bufferul de prefetch și bufferul de reordonare
trebuie să fie golite (măcar parţial) iar adresa de acces la cache-ul de instrucţiuni
trebuie şi ea modificată, în concordanţă cu adresa reală la care se face saltul.

Figura 4.26. Arhitectură superscalară decuplată


Pe baze statistice se arată că un basic-block conţine, pe programele de uz
general, doar 5-6 instrucţiuni în medie, ceea ce înseamnă că rata de fetch a
instrucţiunilor este limitată la cca. 5-6, aducerea simultană a mai multor

314
instrucţiuni fiind deci inutilă (limitarea fetch bottleneck sau Flynn’s bottleneck,
cum se mai numește). Desigur, această limitare fundamentală ar avea consecinţe
defavorabile şi asupra consumatorului, care ar limita principial şi rata medie de
execuţie a instrucţiunilor (IR - Issue Rate) la această valoare (5-6).
Să considerăm că programul accesează un anumit număr de locații
secvențiale din memorie, iar apoi accesează o locație de memorie în mod
nesecvențial (spre exemplu, CPU procesează instrucțiuni secvențiale, iar la un
moment dat, procesează un salt necondiționat sau un salt condiționat de tip
Taken). Să notăm cu λ probabilitatea unui acces nesecvențial la memorie (spre
exemplu, un salt necondiționat sau unul condiţionat de tip Taken). În această
ipoteză, probabilitatea accesării a k locații secvențiale de memorie de către CPU
este dată de formula:

p (k ) = λ (1 − λ ) k −1

Formula semnifică probabilitatea unui acces nesecvențial ( λ ), urmat de


alte (k-1) accese secvențiale (1- λ ). S-a utilizat formula intersecţiei a două
evenimente: . Dacă A şi B sunt evenimente
independente, rezultă că . Rezultă că lungimea medie a
locațiilor de memorie accesate în mod secvențial de către procesor (LB) este dată
de relația (medie ponderată a tuturor lungimilor posibile):

1
Lb = ∑ kp (k ) = λ
k ∈[1, ∞ )

În [Wol07] pg. 305 formula de mai sus, reprezentând lungimea medie


a locațiilor de memorie accesate în mod secvențial, este prezentată în mod
eronat. Formula anterioară, corect prezentată de noi, se demonstrează
elementar, ca mai jos.

n n
n(1 − λ ) n +1 − (n + 1)(1 − λ ) n + 1
Sn= ∑ kp(k ) = λ ∑ k (1 − λ ) k −1 =
k =1 k =1 λ

315
Identitatea anterioară s-a obținut derivând identitatea de mai jos, care
exprimă suma unei progresii geometrice, anume:

n
1 − (1 − λ ) n
∑ (1 − λ ) k = (1 − λ )
k =1 λ

1
Rezultă imediat, prin trecere la limită, că nlim Sn = . De altfel, rezultatul
− >∞ λ
anterior este unul intuitiv, afirmând, practic, că dacă probabilitatea unei
instrucțiuni de salt care se face este λ , atunci lungimea medie a basic-block-ului
1
este .
λ
Progresele semnificative în algoritmii de lansare în execuţie impun însă
depăşirea acestei bariere. În acest sens, cercetările actuale insistă pe
îmbunătăţirea mecanismelor de aducere a instrucţiunilor prin următoarele tehnici
principale:
- predicţia simultană a mai multor ramificaţii / tact, rezultând deci rate de
procesare sporite (număr mediu de instrucțiuni executate per ciclu)
- posibilitatea accesării şi aducerii simultane a mai multor basic- block-uri
din cache, chiar dacă acestea sunt nealiniate, prin utilizarea unor cache-uri
multiport
- păstrarea unei latenţe reduse a procesului de aducere a instrucţiunilor, în
contradicţie cu cele două cerinţe anterioare.
Alţi factori care determină limitarea ratei de fetch a instrucţiunilor (FR-
Fetch Rate) sunt: lărgimea de bandă limitată a interfeţei procesor - cache, miss-
urile în cache, predicţiile eronate ale ramificaţiilor etc.
O paradigmă interesantă, situată în prelungirea conceptului de
superscalaritate şi care poate constitui o soluţie interesantă faţă de limitările mai
sus menţionate, o constituie trace-procesorul, adică un procesor superscalar
având o memorie trace-cache (TC). De altfel a și fost deja implementată pe plan
comercial în procesoarele Intel Pentium. Ca şi cache-urile de instrucţiuni
(Instruction-Cache, IC), TC este accesată cu adresa de început a noului bloc de
instrucţiuni ce trebuie executat, în paralel eventual cu IC (dacă cea din urmă mai
este necesară). În caz de miss în TC, instrucţiunea va fi adusă din IC sau - în caz

316
de miss şi aici - din memoria principală. Spre deosebire însă de IC, TC
memorează instrucţiuni contigue din punct de vedere al secvenţei lor de
execuţie, în locaţii contigue ale memoriei TC. Așadar, o linie din TC memorează
un segment de instrucţiuni executate dinamic şi secvenţial în program (trace-
segment). Evident, un trace-segment poate conţine mai multe basic-block-uri
(unităţi secvenţiale de program). Aşadar, o linie TC poate conţine N instrucţiuni
sau M basic- block-uri, N>M, înscrise acolo pe parcursul execuţiei lor.

Figura 4.27. Ansamblul trace-cache respectiv predictor multiplu


Memoria TC este accesată cu adresa de început a basic-block-ului A, în
paralel cu predictorul multiplu de salturi (vezi Figura 4.27). Acesta, spre
deosebire de un predictor simplu, predicţionează nu doar adresa de început a
următorului basic- block ce trebuie executat, ci toate cele (M-1) adrese de
început aferente următoarelor (M-1) basic- block-uri care urmează după basic
block-ul curent, notat cu A. Cei (M-1) biţi generaţi de către predictorul multiplu
(taken/ not taken) selectează spre logica de execuţie doar acele blocuri din linia
TC care sunt predicţionate că se vor executa (în cazul acesta, doar blocurile A şi
B, întrucât predictorul a selectat blocurile ABD că se vor executa, în timp ce în
linia TC erau memorate doar blocurile ABC).
O linie din TC conţine:

317
- N instrucţiuni în formă decodificată, fiecare având specificat blocul căreia
îi aparţine. Aici se pot memora și informații referitoare la dependențele între
aceste instrucțiuni, întrucât cele N instrucțiuni au fost alocate în linia din TC
după execuția lor. Aceste informații pot fi deosebit de utile ulterior, în execuția
instrucțiunilor respective.
- cele maximum M-1 posibile adrese destinaţie aferente celor M blocuri
stocate în linia din TC. Acestea sunt necesare pentru adresarea predictorului
multiplu de branch-uri (v. Figura 4.30).
- un câmp care codifică numărul (maximum M-1) şi "direcţiile" (taken / not
taken) salturilor memorate în linia TC.
Înainte de a fi memorate în TC, instrucţiunile pot fi pre-decodificate, în
scopul înscrierii în TC a unor informaţii legate de dependenţele de date ce
caracterizează instrucţiunile din linia TC curentă. Aceste informaţii vor facilita
procese precum bypassing-ul datelor între unităţile de execuţie, redenumirea
dinamică a regiştrilor cauzatori de dependenţe WAR (Write After Read) sau
WAW (Write After Write) între instrucţiuni etc., deosebit de utile în vederea
procesării paralele, prin tehnici de tip Out of Order, a instrucţiunilor. O linie din
TC poate avea diferite grade de asociativitate, în sensul în care ea poate conţine
mai multe pattern-uri de blocuri, toate având desigur aceeaşi adresă de început
(A), ca în Figura 4.28.

Figura 4.28. Selecţia dintr-o linie trace-cache asociativă

318
Aşadar, segmentele începând de la aceeaşi adresă (A), sunt memorate în
aceeaşi linie asociativă din TC, până la umplerea acesteia. Ca şi în structurile TC
neasociative, verificarea validităţii liniei selectate se face prin compararea
(căutarea) după tag. Deosebirea de esenţă, constă în faptul că aici este necesară
selectarea - în conformitate cu pattern-ul generat de către predictorul multiplu de
branch-uri - trace-ului cel mai probabil în a fi executat, dintre cele conţinute în
linia respectivă. Este posibil ca această selecţie complexă să dureze mai mult
decât în cazul neasociativ şi, prin urmare, să se repercuteze negativ asupra
duratei procesului de aducere a instrucţiunilor (fetch). Avantajul principal însă,
după cum se observă şi în figură, constă în faptul că este probabil să se furnizeze
procesorului un număr de blocuri "mai lung" decât un TC simplu. Astfel, spre
exemplu, dacă pattern-ul real de blocuri executate este ABD, structura TC semi-
asociativă îl va furniza fără probleme în cazul unei predicții corecte a acestuia,
în schimb o structură TC neasociativă, care conţine doar pattern-ul ABC,
evident va furniza în această situaţie doar blocurile AB ca fiind reutilizabile.
Pe măsură ce un grup de instrucţiuni este procesat, el este încărcat într-o
aşa-numită "fill unit" (FU - unitate de pregătire). Rolul FU este de a asambla
instrucţiunile dinamice, pe măsură ce acestea sunt executate, într-un așa numit
trace-segment, în ordinea execuției lor. Segmentele astfel obţinute sunt
memorate în TC. După cum am mai subliniat, este posibil ca înainte de scrierea
segmentului în TC, FU să analizeze instrucţiunile din cadrul unui segment spre a
marca, în mod explicit, dependenţele de date dintre ele. Acest lucru va uşura mai
apoi lansarea în execuţie a acestor instrucţiuni, întrucât ele vor fi aduse din TC şi
introduse direct în staţiile de rezervare aferente unităţilor funcţionale. Unitatea
FU se ocupă deci de colectarea instrucţiunilor lansate în execuţie, asamblarea lor
într-un grup de N instrucţiuni (sau M basic-blockuri) şi înscrierea unui asemenea
grup într-o anumită linie din TC. Există, desigur, cazuri când o FU poate crea
copii multiple ale unor blocuri în TC. Această redundanţă informaţională poate
implica degradări ale performanţei, dar, pe de altă parte, lipsa redundanţei ar
degrada valoarea ratei de fetch a instrucţiunilor deci şi performanţa globală.

319
Figura 4.29. Segmente asamblate pe timpul execuţiei unei bucle de program
Se poate deci afirma că un TC exploatează reutilizarea eficientă a
secvenţelor dinamice de instrucţiuni, reprocesate frecvent, în baza a două motive
de principiu: localizarea temporală a trace-urilor şi respectiv comportarea
predictibilă a salturilor, în virtutea comportării lor anterioare (modelabile deci
prin procese stohastice de tip Markov). Aşadar, TC memorează trace-uri în
scopul eficientizării execuţiei programului şi nu doar în scopul eficientizării
procesului de aducere al instrucţiunilor. Aceasta, pe motiv că un segment din
trace conţine numai instrucţiuni care se vor executa. În cazul memoriei IC, dacă
într-un bloc există o ramificaţie efectivă (un branch care se face), instrucţiunile
următoare se aduceau inutil, împlicând consum inutil de energie electrică,
întrucât nu s-ar fi executat.
Cum TC trebuie să lucreze într-o strânsă dependenţă cu predictorul
multiplu de salturi condiționate, se impune îmbunătăţirea performanţelor acestor
predictoare. Se pare că soluţia de viitor va consta într-un predictor multiplu de
salturi, al cărui rol principal constă în predicţia simultană a următoarelor
maximum (M-1) salturi, asociate celor maximum M blocuri stocabile în linia
TC. Spre exemplu, pentru a predicţiona simultan 3 salturi printr-o schemă de
predicţie corelată pe două nivele, trebuie expandată fiecare intrare din structura
de predicţie PHT (Pattern History Table), de la un singur numărător saturat pe 2
biţi, la 7 astfel de automate de predicţie, ca în Figura 4.22. Astfel, predicţia
generată de către primul predictor (taken / not taken) va multiplexa rezultatele
celor două predictoare asociate celui de al doilea salt posibil a fi stocat în linia

320
curentă din TC. Ambele predicţii aferente primelor două salturi vor selecta la
rândul lor unul dintre cele 4 predictoare posibile pentru cel de-al treilea salt ce ar
putea fi rezident în linia TC, predicţionându-se astfel cvasi-simultan mai multe
salturi. Să remarcăm, totuși, o secvențialitate intrinsecă a acestui proces, care
implică un timing de predicție mai ridicat. Dacă predictorul multiplu furnizează
cvasi-simultan mai multe PC-uri, TC rezolvă elegant şi problema aducerii
simultane a instrucţiunilor pointate de aceste PC-uri, fără multiportarea pe care
un cache convenţional ar fi implicat-o. Practic, se folosesc versiuni de
predictoare corelate. Din motive de timing însă, un salt nu este predicționat aici
funcție de comportamentul real al anterioarelor salturi (taken/not taken), cum ar
fi normal, ci funcție de predicția acestora. De remarcat că adresele respectivelor
salturi, care indexează tabela de predictoare, provin de la structura TC după cum
am mai menționat.

Figura 4.30. Predictor a trei salturi succesive


Asemenea predictoare multiple, în conjuncţie cu structuri de tip TC conduc
practic la o nouă paradigmă a procesării unui program maşină numită "multi-
flow", caracterizată deci prin procesarea în paralel a mai multor basic-block-uri
dintr-un program. În continuare, se prezintă rezultatele unei cercetări bazată pe
simulare asupra conceptelor novatoare de TC şi predictor multiplu, integrate
într-o arhitectură superscalară extrem de agresivă dezvoltată la Universitatea din
Michigan, SUA. În esenţă, investigaţia subliniază următoarele aspecte:

321
- creşterea gradului de asociativitate a memoriei TC de la 0 (mapare
directă) la 4 (asociativitate în blocuri de 4 blocuri/ set) poate duce la creşteri ale
ratei medii de procesare a instrucţiunilor de până la 15%.
- capacităţi egale ale TC şi respectiv memoriei cache de instrucţiuni (64 ko,
128 ko) conduc la performanţe cvasioptimale.
- asociativitatea crescută a liniei TC nu pare a conduce la creşteri
spectaculoase de performanţă.
- performanţa globală faţă de o arhitectură echivalentă, dar fără TC, creşte
cu circa 24%, iar rata de fetch a instrucţiunilor a instrucţiunilor crește în medie
cu 92% (practic se dublează).
În continuare, se prezintă o altă tehnică de procesare, legată tot de
reutilizarea dinamică a instrucţiunilor deja executate, posibil a fi folosită în
conjuncţie cu un trace-cache [Vin02, Vin07]. Principalul scop urmărit constă în
paralelizarea execuţiei unor instrucţiuni dependente RAW (sic!), bazat pe
anticiparea (reutilizarea) valorilor rezultatelor produse de aceste instrucţiuni.
Ideea originară aparţine prestigioasei şcoli de arhitectura calculatoarelor de la
Universitatea din Wisconsin – Madison, SUA, mai precis cercetătorilor Avinash
Sodani şi prof. Gurindar S. Sohi (Eckert Mauchly Award) care au introdus în
1997, la conferinţa International Symposia on Computer Architecture (ISCA
’97) ţinută la Denver, SUA, conceptul de reutilizare dinamică a instrucţiunilor –
bazat pe o nouă tehnică microarhitecturală, non-speculativă, menită să
exploateze fenomenul de repetiţie dinamică a instrucţiunilor, reducând deci
cantitatea de cod-maşină necesar a fi executat, cu beneficii deosebite asupra
vitezei de procesare. Iată deci cum un concept fundamental şi fertil în actuala
inginerie a calculatoarelor, anume acela de reutilizare, migrează practic din
software (vezi tehnicile de reutilizare a rezultatelor funcțiilor din program,
exploatate încă din anii 60 ai secolului trecut – memoization techniques) şi
înspre hardware (vertical migration). Autorii arată, în primul rând, că
reutilizarea unor instrucţiuni sau secvenţe de instrucţiuni este relativ frecventă şi
se datorează modului compact de scriere a programelor (bucle, recursivități etc.)
precum şi caracteristicilor intrinseci ale structurilor de date generice prelucrate.
O instrucţiune dinamică este reutilizabilă dacă ea operează asupra aceloraşi
intrări şi produce aceleaşi rezultate precum o instanţă anterioară a aceleiaşi
instrucţiuni. Ideea de bază este că dacă o secvenţă de instrucţiuni se reia în

322
acelaşi “context de intrare”, atunci execuţia sa nu mai are sens, fiind suficientă o
simplă actualizare a “contextului de ieşire”, în concordanţă cu unul precedent,
memorat în anumite structuri de date implementate în hardware, de tip look-up
tables. Se reduce astfel numărul de instrucţiuni executate dinamic, putându-se
acţiona direct chiar asupra dependenţelor de date între instrucţiuni. Aşadar,
instrucţiunile reutilizate nu se vor mai executa din nou, ci pur şi simplu
contextul procesorului va fi actualizat, în conformitate cu acţiunea acestor
instrucţiuni, bazat pe istoria lor memorată.
Autorii acestei idei analizează mai întâi dacă gradul de reutilizare a
instrucţiunilor dinamice este semnificativ şi se arată că răspunsul este unul
afirmativ. Astfel, mai puţin de 20% din numărul instrucţiunilor statice dintr-un
program obiect, generează peste 90% dintre instrucţiunilor dinamice repetate. În
medie armonică, măsurat pe benchmark-urile SPEC ’95 (Standard Performance
Evaluation Corporation), circa 26% dintre instrucţiunile dinamice sunt
reutilizabile (adică au mai fost executate în trecut și au produs, cel puțin o dată,
același rezultat cu cel curent). În opinia autorilor, există în acest sens două cauze
calitative: în primul rând, faptul că programele sunt scrise în mod generic, ele
operând asupra unei varietăţi de date de intrare organizate în structuri de date
specifice, iar în al doilea rând, aceste programe sunt scrise într-un mod concis –
aceasta semnificând menţinerea unei reprezentări statice compacte a unei
secvenţe dinamice de operaţii – în vederea obţinerii rezultatelor dorite (în acest
sens, structurile de tip recursiv, “buclele” de program, polimorfismele din
programarea obiectuală etc., sunt reprezentative).
În vederea scopului propus, pe parcursul procesării instrucţiunilor, se
construiesc în mod dinamic aşa-numite seturi de instrucţiuni. O instrucţiune "i"
se adaugă unui set notat cu S dacă "i" depinde RAW de cel puţin una dintre
instrucţiunile setului S. În caz contrar, instrucţiunea "i" va fi prima aparţinând
unui nou set. Practic, construcţia acestor seturi implică generarea grafului
dependenţelor de date ataşat programului, ca în secvenţa de mai jos (vezi Figura
4.31).

323
Figura 4.31. Construcţia seturilor în vederea reutilizării codurilor
După procesarea instrucţiunilor, seturile rezultate sunt înscrise în vederea
reutilizării într-un buffer de reutilizare a rezultatelor instrucțiunilor, numit și
TDIS (Table of Dependent Instruction Sequences). O intrare în TDIS conţine
trei părţi principale:
- partea IN, care memorează valorile operanzilor de intrare, adică aceia
neproduşi prin secvenţa respectivă, ci preluaţi din afara acesteia.
- partea INSTRUCTION, conţine adresele instrucţiunilor inserate în seturi.
- partea OUT, care conţine numele regiştrilor destinaţie aferenţi unui set,
precum şi valorile acestora.
Pentru exemplificare, secvenţa de program anterioară necesită un buffer
TDIS cu două intrări, ca mai jos (Figura 4.32).

Figura 4.32. Structura TDIS la un moment dat


Aşadar, la fiecare aducere a unei noi instrucţiuni, PC-ul acesteia se
compară cu adresa (tag-ul) primei instrucţiuni din fiecare linie a TDIS. Apoi, în
caz de hit, conţinutul actual al regiştrilor procesorului este comparat cu cel al
părţii IN a bufferului de reutilizare TDIS. În caz de hit (valorile sunt identice),

324
secvenţa de instrucţiuni din TDIS poate fi reutilizată cu succes şi cu eludarea
tuturor hazardurilor RAW dintre aceste instrucţiuni. Execuţia acestor
instrucţiuni va însemna doar actualizarea contextului procesorului în
conformitate cu valorile OUT din TDIS. Prin urmare, reutilizarea instrucţiunilor
prin acest mecanism va avea un efect benefic asupra timpului de procesare al
instrucţiunilor. Considerând un procesor superscalar care poate aduce,
decodifica şi executa maximum 4 instrucţiuni / ciclu, secvenţa anterioară se
procesează ca în cele două figuri următoare (4.33, 4.34).

Figura 4.33. Execuţia programului pe un procesor superscalar

Figura 4.34. Execuţia programului pe un procesor cu reutilizarea codurilor


Se observă că bufferul de reutilizare a rezultatelor instrucțiunilor TDIS
determină execuţia secvenţei prin reutilizarea instrucţiunilor în doar 4 cicli faţă
de 7 cicli, câţi ar fi fost necesari în cazul unei procesări clasice. Dacă lărgimea
de bandă a decodorului de instrucţiuni ar fi fost de 6 instrucţiuni în loc de doar

325
4, execuţia secvenţei s-ar fi redus la doar 3 cicli. Teste efectuate pe benchmark-
urile SPEC 95, au arătat că între 17% şi 26% dintre instrucţiunile acestor
programe au putut fi reluate cu succes. Conceptul TDIS implică o procesare
eficientă a instrucțiunilor, întrucât se elimină necesitatea secvenţierii în execuţie
a unor instrucţiuni dependente RAW. Mai mult, în opinia autorului acestei cărți,
dintr-un anume punct de vedere, conceptul reutilizării dinamice a secvenţelor
dependente de instrucţiuni, corectează celebra lege a lui G. Amdahl, întrucât
trece peste secvenţialitatea intrinsecă a programului şi procesează agresiv
paralel, chiar şi în acest caz, prin “updating”. Este fără îndoială posibil ca acest
concept să se cupleze favorabil cu cel de tip "trace cache" anterior prezentat şi
care acţionează favorabil în special asupra limitărilor ratei de fetch a
instrucţiunilor.
Cercetătorii americani A. Sodani şi G. Sohi au propus trei scheme de
reutilizare dinamică a instrucţiunilor, primele două la nivel de instrucţiune, iar
ultima, la nivel de lanţ de instrucţiuni dinamice dependente RAW (Read After
Write). Instrucţiunile deja executate, se memorează într-un mic cache, numit
buffer de reutilizare (Reuse Buffer - RB). Acesta poate fi adresat cu PC-ul
(Program Counter) pe timpul fazei de aducere a instrucţiunilor (IF – Instruction
Fetch) având şi un mecanism pentru invalidarea selectivă a unor intrări, bazat pe
acţiunile anumitor evenimente. Desigur că acest RB trebuie să permită şi un
mecanism de testare a reutilizabilităţii instrucţiunii selectate. Testul de
reutilizare verifică dacă informaţia accesată din RB reprezintă un rezultat
reutilizabil sau nu. Detaliile de implementare ale testului depind de fiecare
schemă de reutilizare folosită. De asemenea, trebuie tratate două aspecte privind
managementul RB, anume: stabilirea instrucţiunii care va fi plasată în buffer (1)
şi respectiv menţinerea consistenţei buffer-ului de reutilizare (2). Decizia privind
modul de inserare a instrucţiunilor în RB poate varia de la una nerestrictivă ("no
policy"), care plasează toate instrucţiunile în buffer după execuția lor, în cazul în
care nu sunt deja prezente, la una mai selectivă, care filtrează instrucţiunile ce
vor fi inserate după probabilitatea statistică de a fi reutilizate (spre exemplu, pot
fi introduse în buffer instrucțiunile cu latențe ridicate de execuție, cu avantaje
evidente). Problema consistenţei are în vedere garantarea corectitudinii
rezultatului instrucţiunii reutilizate din RB.

326
În vederea compatibilizării cu modelul superscalar, care lansează în
execuţie mai multe instrucţiuni independente simultan, RB este în general
multiport, pentru a putea permite reutilizarea mai multor instrucţiuni. Este
evident că gradul de multiportare al RB-ului nu are sens a fi mai mare decât
fereastra maximă de execuţie a instrucţiunilor.

Figura 4.34.1. Bufferul de reutilizare

După cum se observă în Figura 4.34.1, bufferul RB este indexat cu PC-ul


instrucţiunii. El prezintă un mecanism de invalidare selectivă a intrărilor, pe
baza unor anumite evenimente specifice. În cazul reutilizării la nivel de
instrucţiune, o intrare în RB ar putea avea următorul format:

Tag Op1 Op2 Adr Rez Rez_valid Mem_Valid

Tag – ar putea fi reprezentat, în esenţă, de către PC-ul instrucţiunii (biții


mai semnificativi ai acestuia).
Op1, Op2 – reprezintă numele regiştrilor sursă utilizaţi de către
instrucţiune (în alte versiuni, ei pot reprezenta chiar valorile acestor regiştri
sursă [Vin02, Vin07]).
Rez – reprezintă rezultatul actual al instrucţiunii, cel care va fi reutilizat în
caz de “hit” în buffer-ul RB.
Rez_Valid – indică, în cazul instrucţiunilor aritmetico-logice, dacă
rezultatul “Rez” este valid sau nu. În cazul instrucţiunilor Load şi Store,

327
dacă bitul este setat, arată că adresa instrucţiunii este validă în RB şi poate
fi deci reutilizată. Este setat odată cu introducerea (alocarea) instrucţiunii în
RB. Este resetat automat de către orice instrucţiune care scrie într-unul din
regiştrii sursă ai instrucțiunii memorate în RB (Op1, Op2).
Adr – este adresa (reutilizabilă) de acces la memorie, în cazul unei
instrucţiuni Load (citire din memoria de date).
Mem_Valid – indică dacă valoarea din câmpul “Rez” este reutilizabilă, în
cazul unei instrucţiuni Load. Bitul este setat la înscrierea instrucţiunii Load
în RB. Resetarea bitului se face prin orice instrucţiune Store subsecventă
Load-ului, având aceeaşi adresă de acces în execuția efectivă (normal,
pentru că va altera rezultatul memorat în RB).
Rezultă că pentru instrucţiunile aritmetico-logice reutilizarea este asigurată
dacă bitul de stare Rez_Valid=1. De asemenea, Rez_Valid=1 garantează adresa
corectă de acces la memorie, pentru orice instrucţiune Load şi deci scuteşte
procesorul de calculul ei (adresare indexată [Registru + Offset]). În schimb,
rezultatul unei instrucţiuni Load nu poate fi reutilizat decât dacă Mem_Valid=1
şi Rez_Valid=1. Plusul de performanţă datorat reutilizării dinamice a
instrucţiunilor se datorează atât scurt-circuitării unor nivele din structura
pipeline, cât şi reducerii hazardurilor structurale, şi deci, a presiunii asupra
diverselor resurse hardware. Astfel, prin reutilizarea instrucţiunilor se evită
stagnarea instrucțiunilor în staţiile de rezervare (Instruction Window) şi, în
consecinţă, micşorarea timpului de execuţie, rezultatele instrucţiunilor reutilizate
fiind scrise mai repede în buffer-ul de reordonare. Rezultă, de asemenea, o
disponibilizare a unităţilor funcţionale de execuţie, care nu vor mai avea de
procesat instrucţiunile reutilizate, precum şi o deblocare mai rapidă a
instrucţiunilor dependente RAW de cea reutilizată. De remarcat că evacuarea din
RB trebuie să ţină cont de faptul că instrucţiunile invalidate trebuie să aibă
prioritate în acest proces.
În cazul unei scheme care reutilizează un întreg lanţ de instrucţiuni
dinamice dependente, structura unei intrări RB este aceeaşi cu cea precedentă,
doar că aici apar două noi sub-câmpuri, asociate operanzilor sursă, notate
SrcIndex1 respectiv SrcIndex2. Acestea pointează spre adresele instrucţiunilor
din RB care au produs operandul sursă 1, respectiv operandul sursă 2, aferenţi
instrucţiunii curente memorate în RB. În acest caz, instrucţiunile sunt clasificate

328
în trei categorii: sursă – care produc rezultate pentru alte instrucţiuni din lanţ,
numite dependente şi respectiv independente – a căror operanzi sursă nu sunt
produşi în cadrul lanţului de instrucţiuni dinamice considerat. O altă diferenţă
esenţială faţă de schema anterioară constă în faptul că pentru schema de
reutilizare la nivel de lanţ de instrucţiuni (dependente), în cazul modificării unui
operand sursă, sunt invalidate doar instrucţiunile independente care conţin acest
operand. Pentru a înţelege mai bine beneficiile acestei reguli selective de
invalidare, se consideră următoarea secvenţă de instrucţiuni dependente RAW:

I: R1<- 0
J: R2<- R1 + 5
K: R3<- R1 + R2
……….
R: R1<-6

În acest caz, procesarea instrucţiunii R nu va invalida instrucţiunile J şi K


aflate în RB (cum ar face schema anterioară) pentru că acestea sunt dependente
de instrucţiunea I. Mai mult, instrucţiunea R nu va invalida nici chiar
instrucţiunea independentă I, pentru simplul motiv că registrul R1 nu este sursă
în această instrucţiune. Astfel, la o nouă instanţiere a lanţului <I,J,K>, rezultatele
acestuia (R1=0, R2=R3=5) vor fi pur şi simplu reutilizate, nemaifiind necesară
procesarea efectivă a instrucţiunilor respective, cu beneficii evidente asupra
timpului de procesare. Din păcate, în cazul schemei anterioare acesteia (cea cu
reutilizare la nivelul unei singure instrucţiuni), instrucţiunea R ar fi invalidat, în
mod conservator şi inutil, instrucţiunile J şi K din bufferul de reutilizare,
nepermiţând astfel reutilizarea acestora (pentru că ambele îl au pe registrul R1
pe post de operand sursă), deşi ele sunt, în mod evident, reutilizabile. Această
invalidare conservatoare nu ar fi produs rezultate eronate, doar ar fi efectuat
procesări redundante, în cazul instrucțiunilor J și K [Vin07].
Desigur că se pune şi problema integrării instrucţiunilor de ramificaţie
(branch) în aceste scheme de reutilizare dinamică. Considerarea acestor
instrucţiuni conduce la restricţii semnificative în politica de introducere a
instrucţiunilor în RB. Pentru a analiza această problemă, se consideră
următoarea secvenţă de instrucţiuni:

329
I1: R1 <- 1
I2: BRANCH <Cond>, I4; If <Cond>=True, salt la I4
I3: R1 <- 0
I4: R2 <- R1+4

În cazul execuţiei speculative a instrucţiunilor, politica de introducere a


instrucţiunilor în RB, aşa cum a fost ea descrisă anterior, poate fi una incorectă.
Mai întâi, se va considera schema de reutilizare la nivel de instrucţiune, anterior
descrisă. Se presupune că instrucțiunea de salt condiționat I2 este iniţial
predicţionată ca Not Taken. Ar rezulta că instrucţiunile I3 şi I4 se introduc în
RB, ca urmare a execuției lor speculative. În cazul în care, ulterior, se constată
că saltul I2 a fost predicţionat eronat, este necesară refacerea contextului şi
execuţia căii Taken. În consecinţă, este posibilă reutilizarea instrucţiunii I4, care
va genera un rezultat eronat, întrucât operandul sursă R1=0 în bufferul de
reutilizare, aşa cum l-a modificat I3. Pentru rezolvarea acestei anomalii se
impune ca o instrucţiune speculativă să fie inserată în RB numai dacă
instrucţiunile sale sursă sunt nespeculative. Astfel, în exemplul considerat, cum
I3 este speculativă, rezultă că instrucțiunea I4 nu se va mai introduce în RB şi
deci eroarea anterior semnalată nu va mai apare.
În cazul schemei cu reutilizare la nivel de lanţ de instrucţiuni dependente,
situaţia este mai complicată. În acest caz, după introducerea instrucţiunilor I3 şi
I4 în RB, va exista un pointer de la I4 la I3, semnificând faptul că I3 este sursă
pentru I4. Aşadar, când datorită predicţiei eronate se va procesa ramura
alternativă, I4 nu se va mai reutiliza, pentru că legătura sa cu I3 va dispărea, în
acest caz instrucţiunea sa sursă fiind I1. Totuşi, aceeaşi problemă care a apărut la
schema anterioară va apărea şi în acest caz, dacă I4 ar fi fost inserată în RB ca
instrucţiune independentă (spre exemplu, dacă din anumite motive, I3 nu ar mai
fi în RB). Prin urmare, în cazul acestei scheme, se impune ca o instrucţiune
speculativă să fie inserată în RB numai dacă instrucţiunile sale sursă sunt
nespeculative sau dacă toate instrucţiunile sursă sunt prezente în RB.
Figura 4.34.2 ilustrează o microarhitectură superscalară tipică, cu
posibilități de reutilizare a instrucţiunilor. Singura modificare de principiu faţă
de modelul superscalar este dată de apariţia buffer-ului de reutilizare și a

330
algoritmului de gestionare a acestuia, care acționează în fazele de aducere și de
decodificareale instrucțiunilor. În faza de fetch instrucţiune sunt extrase din
cache-ul de instrucţiuni sau din memoria principală instrucţiunile adresate şi
plasate în buffer-ul de prefetch (Instruction Queue). Urmează apoi faza de
decodificare a instrucţiunilor şi redenumire a regiştrilor, în vederea eliminării
conflictelor de nume (dependenţe WAR – Write After Read şi WAW - Write
After Write). În faza de citire operand, valorile operanzilor aferenţi
instrucţiunilor sunt citite, fie din setul de regiştri generali, fie din buffer-ul de
reordonare, funcţie de structura care conţine ultima versiune a valorilor
regiştrilor implicați. Imediat după decodificarea instrucţiunii, în timpul fazei de
citire operanzi, se realizează testul de reutilizare asupra intrărilor citite din RB,
pentru a şti dacă rezultatele instrucţiunilor sunt, sau nu sunt, reutilizabile. Dacă
este găsit un rezultat reutilizabil în RB, instrucţiunea aferentă nu mai trebuie
procesată în continuare, acesta fiind transmis direct buffer-ului de reordonare dar
și stațiilor de rezervare care îl așteaptă (instrucțiunilor dependente de acest
rezultat). Instrucţiunile Load evită fereastra Instruction Window doar dacă
rezultatele ambelor micro-operaţii (calculul adresei şi accesarea memoriei de
date) sunt reutilizabile. Testarea reutilizării poate dura unul sau mai mulţi cicli,
având în vedere că este un proces secvenţial, care depinde de numărul de
instrucţiuni dependente din lanţ.
În cazul predicţiei eronate a unei instrucţiuni de ramificaţie, mecanismul de
refacere a contextului va trebui să fie suficient de selectiv, astfel încât să nu
invalideze în RB acele instrucţiuni situate imediat după posibilul punct de
convergenţă al ramificaţiei şi care ar putea fi reutilizate. Astfel, reutilizarea este
exploatată la maximum şi în acest caz, cu beneficii evidente asupra
performanţei.
Rezumând, se desprind câteva avantaje importante introduse de tehnica de
reutilizare dinamică a instrucţiunilor şi anume:
• Scurt-circuitarea unor nivele (stagii de procesare) din structura pipeline de
către instrucţiunile reutilizate, reducând presiunea asupra resurselor hardware
(staţii de rezervare, unităţi funcţionale de execuție, porturi ale cache-urilor de
date etc.) necesare altor instrucţiuni aflate în aşteptare.

331
• La reutilizarea unei instrucţiuni, rezultatul său devine cunoscut mai devreme
decât în situaţia în care s-ar procesa normal, permiţând în consecinţă altor
instrucţiuni dependente de aceste rezultat, să fie executate mai rapid.
• Reduce penalitatea datorată predicţiei eronate a adreselor destinaţie în cazul
instrucţiunilor de salt, prin reutilizarea, fie şi parţială, codului succesor
punctului de convergenţă (squash reuse) [Vin07].
• Comprimarea dependenţelor de date prin reutilizare, determină îmbunătăţirea
timpului de execuţie al instrucţiunilor, crescând gradul de paralelism al
arhitecturii.
• Procentajul de reutilizare al instrucţiunilor dinamice, calculat pe benchmark-
urile SPEC ’95, este semnificativ, ajungându-se la valori maxime de 76%.
• Accelerarea obţinută faţă de modelul superscalar echivalent pe aceleaşi
programe de test nu este la fel de pronunţată ca şi procentajul de reutilizare
(medii de 7-15%), valoarea maximă atinsă fiind de 43%.

Figura 4.34.2. Microarhitectură superscalară cu buffer de reutilizare

O altă tehnică hardware, şi ea relativ recent dezvoltată (1996, 1997), care


urmăreşte, oarecum similar cu tehnica anterioară, exploatarea redundanţei
existente în programe prin comprimarea dinamică a dependenţelor de date, o
reprezintă predicţia valorilor instrucţiunilor (Value Prediction) [Vin02, Vin07].
Deşi ambele tehnici urmăresc reducerea timpului de execuţie al programelor,
prin eliminarea constrângerilor legate de dependenţele de date ale fluxului de

332
instrucţiuni, există totuşi diferenţe, chiar majore, privind unele aspecte, legate de
modul de interacţiune al fiecărei tehnici în parte cu celelalte caracteristici
microarhitecturale. De asemenea, se pun probleme relativ la modul de
determinare speculativă (predicţia valorii) sau non-speculativă (reutilizarea
dinamică a instrucţiunilor) a redundanţei în programele de uz general, avantajele
şi dezavantajele implicate de fiecare tehnică, cantitatea de redundanţă captată de
fiecare tehnică în parte etc. Înainte de a face cunoscute şi a înţelege diferenţele
existente între predicţia valorilor şi reutilizarea instrucţiunilor, se vor descrie, în
mod sintetic, câteva caracteristici şi aspecte legate de conceptul de localitate
(mai corect, vecinătate) a valorii (value locality) şi respectiv predicţie a
valorilor.
Localitatea (“vecinătatea“) valorii reprezintă o a treia dimensiune a
conceptului statistic de localitate (pe lângă cea temporală şi respectiv spaţială,
frecvent întâlnite în programele de uz general), descriind probabilitatea statistică
de referire a unei valori anterior folosite şi stocată în aceeaşi locaţie de memorie
sau registru CPU. Conceptul de localitate a valorii – introdus în premieră de Dr.
M. Lipasti – este strâns legat de calculul redundant (repetarea execuţiei unor
operaţii/instrucțiuni cu aceiași operanzi sursă și aceleași valori ale acestora).
Diferenţa de esenţă între localităţile temporale şi spaţiale şi respectiv localitatea
valorilor instrucțiunilor constă în faptul că primele două sunt focalizate pe
adresele instrucțiunilor (PC), în timp ce ultima este centrată pe rezultatele
produse de instrucțiuni. Mai precis, localitatea temporală se referă la
probabilitatea ca o anumită adresă de memorie – conţinând o “instrucţiune” sau
o “dată” – să fie referită din nou în viitorul “apropiat” de către procesor, în timp
ce localitatea valorii se referă la faptul că rezultatul unei instrucţiuni care este
din nou procesată, să se repete. Așadar, nu numai că instrucțiunea adusă se va
aduce din nou (conform principiului statistic temporal locality), dar va produce
un rezultat situat în vecinătatea temporală a celorlalte rezultate produse de
aceeași instrucțiune (deci aparținând mulțimii anterioarelor valori produse de
aceeași instrucțiune, value locality). Exploatarea localităţilor spaţiale şi
temporale ale instrucțiunilor și datelor se face, în principal, prin sisteme
ierarhizate de memorii cache, care, practic, reduc latenţa de acces a CPU la
memoria principală, în timp ce localitatea valorilor implică posibilitatea
predicţiei markoviene a acestora, în vederea execuţiilor speculative ale

333
instrucţiunilor. Având în vedere semnificaţia şi tradiţia noţiunii de “vecinătate”
în literatura științifică românească dedicată analizei matematice şi teoriei
mulţimilor (dar și graficii, spre exemplu), exprimăm şi noi opinia că aceasta ar
fi, poate, mai potrivită decât termenul de “localitate”, care s-a cam impus din
păcate la noi, în mod abuziv din punct de vedere lingvistic, datorită traducerii –
nu tocmai potrivite în acest context – cuvântului englezesc “locality”.
Convingerea că "vecinătatea valorilor" instrucțiunilor există, are la bază
rezultate statistice obţinute prin simulare la nivel de execuţie a instrucţiunilor pe
benchmark-urile SPEC ’95 dar și pe altele mai noi. Cu o "adâncime" a istoriei
rezultatelor anterioare produse de o anumită instrucțiune de 1 (regăsirea
aceleiaşi valori în resursa asignată ca şi în cazul precedentei procesări a
instrucțiunii respective), programele de test exprimă o localitate a valorii de
aprox. 50%, în timp ce extinzând verificarea în spaţiul valorilor aferente
ultimelor 16 rezultate produse de către instrucțiunea respectivă, se obţine o
localitate de cca. 80%. Rezultatele subliniază deci că majoritatea instrucţiunilor
statice aferente unui program exprimă o variaţie redusă în timp, a valorilor pe
care le produc pe parcursul execuţiei.
Exploatarea conceptului de localitate a valorilor instrucțiunilor se face prin
tehnici de predicţie run-time a acestor valori. Tehnica Load Value Prediction
(LVP), prima implementată de către cercetători (Lipasti și colaboratorii),
predicţionează rezultatele instrucţiunilor de tip Load (încărcare din memoria de
date) la expedierea spre unităţile funcţionale de execuţie, exploatând corelaţia
dintre adresele respectivelor instrucţiuni şi valorile citite din memorie de către
acestea, permiţând deci instrucţiunilor Load să se execute înainte de calculul
adresei lor de acces şi îmbunătăţind astfel performanţa. Conceptul de localizare
a valorilor se referă practic la o corelaţie dinamică între numele unei resurse
(registru, locaţie de memorie, port I/O) şi valoarea stocată în acea resursă. Dacă
memoriile cache convenţionale se bazează pe localitatea temporală şi spaţială a
datelor pentru a reduce timpul mediu de acces la memoria principală, tehnica
LVP exploatează vecinătatea valorilor instrucțiunilor Load, prin predicţia
acestor valori, reducând atât timpul mediu de acces la memoria principală cât şi
necesarul de lărgime de bandă al acesteia și al magistralei de legătură CPU-
memorie (se efectuează mai puţine accese la memoria centrală), asigurând astfel

334
un câştig de performanţă considerabil. Desigur că toate aceste avantaje se obţin
simultan cu reducerea considerabilă a presiunii asupra memoriilor cache.
În Figura 4.34.3 se prezintă o schemă bloc generică de predictor contextual
(markovian) de valori, bazat desigur pe principiul localității (vecinătății)
valorilor instrucțiunilor. Schema este în mod evident integrată în CPU.
Contextul din tabela VHT (Value History Table) este adresat cu PC-ul
instrucţiunii (biții cei mai puțin semnificativi), pe timpul fazei de aducere a
acesteia. Acest context reprezintă, în general, o istorie comprimată, în mod
frecvent prin intermediul unei funcţii de dispersie, a ultimelor valori generate de
către respectiva instrucţiune. Fireşte că acesta poate conţine şi alte informaţii
considerate relevante. Acest context adresează tabela de predicţie VPT (Value
Prediction Table). Cuvântul adresat din VPT conţine două câmpuri: Val – ultima
sau ultimele valori generate de către respectiva instrucţiune în instanțele sale
anterioare, respectiv Confid – grade de încredere ataşate valorilor din vectorul
Val. Aceste grade de încredere se implementează uzual sub forma unor
numărătoare saturate, care sunt incrementate/decrementate, după cum predicţia
valorii instrucțiunii a fost, sau nu a fost, corectă. În general, valoarea
predicţionată este cea care are gradul de încredere cel mai mare, în ipoteza în
care acest grad de încredere depăşeşte o valoare de prag (threshold), anterior
aleasă de către proiectant. Se remarcă similitudinea între aceste scheme de
predicţie a valorilor şi schemele de predicţie adaptivă pe două niveluri a
ramificaţiilor de program (Two Level Adaptive Branch Prediction), anterior
prezentate în Capitolul 3. Practic, schema predicţionează pe baza probabilităţii
statistice ca o anumită valoare să urmeze unui anumit context dinamic
(reprezentat, spre exemplu, de ultimele k valori anterior produse), comportându-
se excelent pentru secvenţe statistic-repetitive de valori. Schema implementează,
practic, un predictor Markov simplificat. Prin particularizări ingenioase ale
acestei scheme generice de predicție a valorilor se pot obține variațiuni deosebit
de eficiente [Vin07].

335
Figura 4.34.3. Predictor contextual generic de valori (preluată din [Vin02])

Ca şi consecinţă a predicţiei valorilor se reduc şi efectele defavorabile ale


dependenţelor RAW, prin reducerea aşteptărilor instrucţiunilor dependente
ulterioare. Dacă instrucţiunile predicţionate se află pe calea critică a
programului, cea mai mare consumatoare de timp, execuţia acesteia se
comprimă în mod considerabil, invalidând astfel chiar legea lui Amdahl (vezi în
continuare). Din păcate puține sunt cercetările care să selecteze pentru predicție
instrucțiunile predicționate a aparține căii critice de program.
În opinia autorului acestui curs universitar, predicţia şi consecinţa ei,
execuţia speculativă a instrucţiunilor, vor fi procese esenţiale în cadrul
microarhitecturilor viitoare. De aceea credem că este bine să ne “împrietenim”
cu ele încă de pe acum, încercând să le înţelegem esenţa, dincolo de jargoanele
şi instrumentele teoretice și practice sofisticate, folosite actualmente în
cercetările de profil. Cercetările din domeniul microarhitecturilor cu paralelism
la nivel de instrucţiuni s-au concentrat până de curând în vederea depăşirii
latenţei memoriilor principale, hazardurilor structurale, a dependenţelor de date
de tip WAR (Write After Read) şi WAW (Write After Write) şi respectiv a

336
limitărilor impuse de către instrucţiunile de ramificaţie (branch). Acestea din
urmă se rezolvă, după cum am arătat în detalii, în principal prin predicţie
dinamică, proces care asigură execuţia speculativă a instrucţiunilor prin
paralelizarea unor basic-block-uri distincte [Vin00]. Dependenţele de date
imuabile, de tip RAW (Read After Write), erau – până nu demult – considerate
ca reprezentând o limitare fundamentală, ce nu poate fi depăşită, datorată unei
secvenţialităţi intrinseci a programului şi care reducea dramatic paralelismul la
nivel de instrucţiuni. Cu alte cuvinte, graful dependenţelor de date asociat unui
program, constituia “până mai ieri”– prin calea sa critică – o barieră de netrecut
în calea procesării paralele. Predicţia dinamică a valorilor instrucţiunilor (ca și
reutilizarea dinamică a instrucțiunilor de altfel) reprezintă o tehnică relativ
recentă, care permite execuţia speculativă a instrucţiunilor dependente RAW,
prin predicţia rezultatelor acestora, reducându-se astfel în mod semnificativ
latenţa de execuţie a căii critice a programului. Impactul acestei tehnici este unul
enorm, căci, în opinia autorului acestui tratat, după cum am mai arătat, chiar
celebra lege a lui G. Amdahl ar trebui uşor reformulată în cazul acestor
microarhitecturi speculative, sau a celor bazate pe reutilizarea dinamică a
instrucţiunilor, căci noţiunea de secvenţialitate intrinsecă a unui program ar
trebui revăzută pe această bază.
Localitatea valorilor este justificată de câteva observaţii empirice, de natură
statistică, desprinse din programele de uz general, medii şi sisteme de operare
diverse. Dintre acestea, se amintesc aici:
• Redundanţa datelor – seturile de intrări de date pentru programele de uz
general suferă mici modificări (Exemple: matrici rare, fişiere text cu spaţii
libere şi, oricum, cu multe simboluri repetitive, celule libere în foile de calcul
tabelar etc.)
• Verificarea erorilor – tehnica LVP poate fi benefică în gestionarea tabelelor
de erori ale compilatoarelor, în cazul apariţiei unor erori repetate.
• Constante în program – deseori este mult mai eficient ca programele să
încarce constante situate în structuri de date din memorie, decât sub forma
unor constante imediate, situate în chiar corpul instrucțiunii, ceea ce este
exploatat favorabil prin tehnica LVP.
• Calcululul adreselor instrucţiunilor de salt – în situaţia instrucţiunilor case
(switch în C) compilatorul trebuie să genereze cod care încarcă într-un

337
registru adresa de bază pentru branch, care este o constantă (predicţia
adreselor destinaţie pentru instrucţiunile de salt).
• Apelul funcţiilor virtuale sau polimorfismele din programele obiectuale – în
acest caz compilatorul trebuie să genereze cod care încarcă un pointer de
funcţie, care este o constantă în momentul rulării.
Localitatea valorii aferentă unor instrucţiuni Load statice dintr-un program,
poate fi afectată semnificativ de optimizările compilatoarelor: loop unrolling,
software pipelining, tail replication etc. [Vin00], întrucât aceste optimizări
creează instanţe multiple ale instrucţiunilor Load, după cum s-a arătat chiar în
această lucrare. Ca şi metrică de evaluare, localitatea valorii pentru un
benchmark este calculată ca raport dintre numărul de instrucţiuni Load dinamice
care regăsesc o aceeaşi valoare în memorie ca şi precedentele k accese şi
respectiv numărul total de instrucţiuni Load dinamice existente în benchmark-ul
respectiv. O istorie de localizare pe k biţi semnifică faptul că o instrucţiune Load
verifică dacă valoarea citită din memorie se regăseşte printre ultimele k valori
anterior încărcate. O problemă importantă de proiectare care se pune la ora
actuală este: cât de "multă istorie" să fie folosită în procesul de predicţie a
valorilor instrucțiunilor ? Compromisurile actuale care se fac, oscilează între o
istorie redusă – reprezentând o acurateţe de predicţie joasă, dar cost scăzut sau –
o istorie bogată de predicţie – implicând în general o acurateţe ridicată de
predicţie dar costuri şi complexitate hardware ridicate.
Se remarcă aici o similitudine între problema predicţiei valorilor şi
problema - actuală şi ea - a predicţiei adreselor destinaţie aferente instrucţiunilor
de salt indirect, problemă subliniată de altfel şi în capitolul precedent dar și în
lucrarea noastră [Vin00]. Structurile de date implementate în hardware pentru
ambele procese de predicţie, au acelaşi principiu de funcţionare şi anume:
asocierea cvasi-bijectivă a contextului de apariţie al instrucţiunii respective
(Load sau JUMP/CALL indirect registru) cu data / adresa de predicţionat, în
mod dinamic, odată cu procesarea programului. Iată deci că problematica
predicţiei (în latină prae – înainte, dicere – a spune) în microprocesoarele
avansate, tinde să devină una generală şi, ca urmare, implementată pe baza unor
principii teoretice mai generale şi mai elevate. Aceasta are drept scop principal
şi imediat, execuţia predictiv-speculativă agresivă a instrucţiunilor, cu beneficii
evidente în creşterea gradului mediu de paralelism.

338
Se pune problema dacă reutilizarea dinamică a instrucțiunilor nu este
superioară predicției dinamice a valorilor instrucțiunilor, prin faptul că este
nespeculativă, însemnând că testul de reutilizare nu poate greși decizia (în
schimb predicția, da, ar putea fi ulterior invalidată). În realitate, reutilizarea
dinamică a instrucțiunilor poate exploata doar 1-Value Locality, pentru că dacă o
instrucțiune este reutilizabilă, valoarea ei actuală este egală cu valoarea produsă
de anterioara sa instanță. În schimb, predicția dinamică a valorilor poate,
evident, exploata localități ale valorilor de ordin superior, adică k-Value
Locality, k>1. Desigur că acest avantaj este compensat de faptul că, spre
deosebire de reutilizarea dinamică a instrucțiunilor, unde validarea
reutilizabilității este certă, în cazul predicției, validarea acesteia poate conduce la
un insucces, urmat de restaurarea contextului corect al procesorului etc.
La ora actuală, influenţa predicţiei dinamice a valorilor şi, cu atât mai puţin
a reutilizarii dinamice a instrucţiunilor, sunt departe de a fi înţelese în cadrul
sistemelor SMT sau multiprocesor [Vin09]. Acest fapt este valabil chiar şi la
nivel teoretic-principial, după cum vom arăta în continuare, printr-un exemplu
simplu. Este binecunoscută legea lui Amdahl’s pentru un sistem multiprocesor
cu N procesoare:
1
S(N) =
(1 − f)
f+
N
O formula corecta pentru accelerarea obținută prin predicția valorilor în cazul
unui sistem multiprocesor, notată cu Svp(N), trebuie să ţină cont că:

• f(1-p) [%] din program este procesat în acelaşi timp în care ar face-o o
maşină secvenţială (SISD) echivalentă.
• fp [%] din program s-ar procesa teoretic instantaneu, din cauza predicţiei
perfecte a valorilor instrucţiunilor.
• (1-f) [%] din program s-ar procesa, din punct de vedere pur teoretic, de N
ori mai rapid decât pe maşina secvenţială.

In aceste condiţii, formula corectă a accelerării este:

339
1
Svp(N) =
(1 - f)
f(1 - p) +
N
Pentru N=1 rezultă:
1
Svp(1)= ≥ 1.
1 − fp
Particularizând f=1 în formula precedentă, în perfect acord cu [Vin07], rezultă:
1
S= .
1− p
Pentru cei care doresc să aprofundeze asemenea probleme de actualitate,
precum și probleme referitoare la optimizarea unor sisteme de calcul avansate,
recomandă studiul referințelor bibliografice [Vin05], [Gel09], [Gel12], [Rad13],
[Jah15], [Vin15].
În [Vin02] se prezintă un model analitic, ceva mai elaborat decât
precedentul, în vederea determinării creşterii de viteză aduse de o arhitectură cu
predicţie a valorilor, faţă de una superscalară clasică, echivalentă. Se consideră
că probabilitatea ca o anumită instrucţiune să fie corect predicţionată, este p,
fiind egală cu acurateţea medie de predicţie a valorilor instrucţiunilor. Pentru
simplificare, se consideră că o instrucţiune predicţionată corect se execută în
mod instantaneu, în caz contrar, ea executându-se în timpul T (un ciclu CPU).
Modelul ia în considerare doar calea critică a programului şi consideră, pentru
simplitate, că întregul program se află memorat în resursele interne ale
procesorului. O reprezentare grafică simplificată aferentă procesării
instrucţiunilor pe modelul propus este dată în Figura 4.34.b.

Figura 4.34.b. Model predictiv-speculativ de procesare a instrucţiunilor

340
Semantica figurii este următoarea: dacă instrucţiunea Ik este predicţionată
corect (p), atunci execuţia ei este practic instantanee; în caz contrar (1-p),
aceasta se execută în timpul T. Aşadar, orice tranziție între două instrucțiuni
consecutive are asignate două componente: o probabilitate de execuţie predictiv-
speculativă şi respectiv un timp de execuţie a instrucţiunii procesate normal
(nespeculative).
În acest caz, timpul total de execuţie al programului (Execution Time – ET)
este:

ET (n) = T + Tn −1 , unde Tn-1 reprezintă timpul de execuţie al celor (n-1)


instrucţiuni următoare instrucţiunii I1, deci timpul aferent instrucţiunilor I2, …,
In. Putem scrie acum probabilitatea ca Tn-1 să dureze k cicli CPU:

Prob(Tn-1 = kT)=C kn−1 (1-p)kpn-1-k, ∀k ∈ {0,1,2,…,n-1} – distribuție binomială


de probabilitate

Avem îndeplinită şi relaţia de normalizare (echilibru):

n −1

∑ Pr ob(T
k =0
n −1 = kT ) = 1

Rezultă că timpul total de execuţie este:

n −1 n −1
ET (n) = T + T ∑ k Pr ob(Tn −1 = kT ) = T + T ∑ kC nk−1 (1 − p ) k p ( n−1− k )
k =1 k =1

Se arată imediat că este îndeplinită identitatea recurentă între Cnk−1 și Cnk−−21 :

kC nk−1 = (n − 1)C nk−−21

Luând în considerare această ultimă identitate şi ţinând cont de ultima


expresie a lui ET(n), putem scrie succesiv egalităţile:

341
n −1 n −1
Tn−1 = T ∑ (n − 1)C nk−−21 (1 − p ) k p n −1− k = T (n − 1)(1 − p )∑ C nk−−21 (1 − p ) k −1 p n −1− k =
k =1 k =1
n−2
= T (n − 1)(1 − p )( p + (1 − p )) = T (n − 1)(1 − p )

În ultima relație s-a exploatat formula binomului lui Newton. În concluzie,


timpul total de execuţie este:

ET (n) = T + T (n − 1)(1 − p )

Rezultă că accelerarea S(n) (Speed-up) faţă de un procesor echivalent, dar


fără predictor de valori, este următoarea (acesta execută calea critică de program
în minimum n cicli CPU):

nT n
S ( n) = = ≥1
ET (n) 1 + (n − 1)(1 − p )

În fapt, având în vedere că numărul de instrucţiuni dinamice executate este


practic infinit, accelerarea este neliniară (hiperbolică), anume:

1
S (∞) = lim S (n) =
n →∞ 1− p

Desigur, având în vedere multiplele simplificări operate, în realitate


accelerarea este mai mică decât S (∞) obţinut, dar şi aşa rezultatul este unul
important şi deosebit de sugestiv. Spre exemplu, la o probabilitate medie de
predicție corectă a instrucțiunilor p=80%, accelerarea este S=5! (Pentru ca
cititorul să se asigure că a înțeles acest demers matematic elementar, autorul îi
propune să rezolve o problemă izvorâtă chiar din demonstrația anterioară, fiind
practic echivalentă cu aceasta, doar prezentată într-o altă formulare. Așadar, se
n
consideră șirul: S (n) = n−1 , ∀ p ∈ R-{1}, n=3,4, … Să se
∑ kC
k =1
k
n −1 (1 − p ) p
k n −1− k

calculeze: lim
n→∞
S (n) .)

342
O prezentare sintetică și accesibilă a acestor arhitecturi predictiv-
speculative, la nivelul marelui public, făcută de autorul acestei lucrări, este
disponibilă pe You Tube la adresa
https://www.youtube.com/watch?v=2mx1Qpiw9jk (2013) respectiv
https://www.youtube.com/watch?v=HLAVV7hbnRc.
În contextul următoarei generaţii arhitecturale de microprocesoare de înaltă
performanţă, se întrevede, de asemenea, implementarea unor mecanisme de
aducere de tip Out of Order a instrucţiunilor, în plus faţă de cele deja existente
în execuţia instrucţiunilor. Astfel, spre exemplu, în cazul unei ramificaţii dificil
de predicţionat, pe durata procesului de predicţie, procesorul ar putea să aducă
anticipat instrucţiunile situate începând cu punctul de convergenţă al ramurilor
de salt, dacă acesta există. Aceste instrucţiuni, dacă sunt independente de
condiţia de salt, pot fi chiar lansate în execuţie, în mod speculativ. Când
predicţia se va fi realizat sau pur şi simplu când adresa destinaţie a ramificaţiei
va fi cunoscută, procesorul va relua aducerea instrucţiunilor de la adresa
destinaţie a ramificaţiei.
În viitorul apropiat, unitatea de execuţie va trebui să lanseze spre unităţile
funcţionale între 16 şi 32 de instrucţiuni, în fiecare tact. Evident, execuţia
instrucţiunilor se va face Out of Order, pe baza dezvoltării și rafinării unor
algoritmi de tip Tomasulo. Staţiile de rezervare aferente unităţilor de execuţie,
vor trebui să aibă capacităţi de peste 2000 de instrucţiuni. Pentru a evita falsele
dependenţe de date (conflictele de nume de tip WAR, WAW),
microprocesoarele vor avea implementate mecanisme eficiente de redenumire
dinamică a regiştrilor logici. Desigur, tehnicile de scheduling static vor trebui
îmbunătăţite radical, pentru a putea oferi acestor structuri hardware complexe
suficient paralelism. Se estimează atingerea unor rate medii de procesare de 12-
14 instrucțiuni / tact, considerând că se pot lansa în execuţie maximum 32
instrucțiuni / tact. La ora actuală, cele mai avansate procesoare, cu un potenţial
teoretic de 6 instrucțiuni / tact, ating în realitate doar 1.2 - 2.3 instr. / tact.
Aceste rate mari de procesare, impun execuţia paralelă a cca. 8 instrucţiuni
Load/ Store. Aceasta implică un cache de date primar de tip multiport şi unele
secundare, de capacitate mai mare, dar cu porturi de acces mai puţine. Miss-urile
pe primul nivel, vor accesa al 2-lea nivel ș.a.m.d. Pentru a nu afecta perioada de
tact a procesorului, este posibil ca memoria cache din primul nivel să fie

343
multiplicată fizic. De asemenea, în vederea îmbunătăţirii performanţei, viitoarele
microprocesoare vor predicţiona adresele de acces la memorie ale instrucţiunilor
Load, asemenea predicţiilor salturilor, permiţând acestora să se execute înainte
de calculul adresei.
Aceste noi arhitecturi, care execută trace-uri ca unităţi de procesare, vor
putea permite procesarea mai multor asemenea trace-uri la un moment dat, ceea
ce conduce la conceptul de procesor multithreading, cu thread-uri (așa numite
trace-uri) care nu sunt puse în evidență în mod explicit, la nivelul limbajului
HLL, ci la nivelul sistemului hardware de procesare sau chiar al compilatorului.
Aşadar, paralelismul la nivel de instrucţiuni (ILP- Instruction Level Parallelism)
va fi înlocuit cu unul mai masiv, de un nivel semantic superior (pentru că este
evidențiat la nivelul programului de nivel înalt/mediu), constituit la nivelul
thread-urilor unei aplicaţii (TLP - Thread Level Parallelism). În acest scop,
arhitectura va trebui să conţină mai multe unităţi de procesare a thread-urilor /
trace-urilor, interconectate. La aceasta, se adaugă o unitate de control "high-
level", în vederea partiţionării programului în thread-uri de instrucţiuni
independente. Se poate ajunge astfel la o rată de procesare de mai multe trace-
uri / tact, faţă de instrucţiuni / tact, metrica de performanţă obişnuită a
procesoarelor superscalare actuale. Aceste procesoare cu paralelism la nivelul
firelor de execuție acoperă "semantic-gap"-ul existent între paralelismul la nivel
de instrucţiuni şi respectiv cel situat la nivelul programelor, mult mai masiv. O
altă paradigmă relativ nouă, oarecum asemănătoare, devenită de prin 2004 o
realitate comercială, o constituie multiprocesoarele integrate într-un singur
circuit (multicore), ca soluţie în vederea exploatării paralelismului masiv
("coarse grain parallelism"). Aceasta a fost încurajată şi de anumite limitări, în
special tehnologice, care au afectat dezvoltarea arhitecturilor uniprocesor (v. în
continuare).
Toate aceste idei arhitecturale agresive, au putut fi implementate într-un
microprocesor real, abia atunci când tehnologia a permis integrarea "on-chip" a
800 milioane - 1 miliard de tranzistori, adică începînd cu anul 2010 (conform
"Semiconductor Industry Association"). La acest nivel de dezvoltare tehnologică
este deja posibilă, de asemenea, integrarea "on-chip" a memoriei DRAM, la un
timp de acces de cca. 20 ns. Ideea este atrăgătoare, pentru că la aceeaşi suprafaţă
de integrare, o memorie DRAM poate stoca de cca. 30-50 de ori mai multă

344
informaţie decât o memorie SRAM pe post de cache. Un DRAM integrat de 96
MB necesită circa 800 milioane de tranzistori ocupând cca 25% din suprafaţa
circuitului (vezi pentru detalii http://iram.cs.berkeley.edu/)
În orice caz, se pare că pentru a continua şi în viitor creşterea exponenţială
a performanţei microprocesoarelor, sunt necesare idei noi, revoluţionare chiar,
pentru că, în fond, paradigma actuală este, din punct de vedere conceptual,
veche de circa 15-20 de ani. Ar fi poate necesară o abordare holistică, integrată,
care să îmbine eficient tehnicile de scheduling software, cu cele dinamice, de
procesare hardware. În prezent, după cum a rezultat din cele prezentate până
acum, separarea între cele două abordări este, poate, prea accentuată. În acest
sens, programele ar trebui să expliciteze paralelismul intrinsec (concurența) într-
un mod mai clar. Cercetarea algoritmilor ar trebui să ţină seama cumva şi de
concepte precum, de exemplu, cel de cache, în vederea exploatării localităţilor
spaţiale ale datelor prin chiar algoritmul respectiv. Topologia de interconectare
în cazul sistemelor multicore este, de asemenea, esențială în acest sens.
Cunoaştem puţine lucruri despre ce se întâmplă cu un algoritm când avem
implementate ierarhii de memorii pe maşina fizică sau când avem topologii
sofisticate de interconectare a nucleelor multiple de procesare. Spre exemplu,
complexitatea unui algoritm de sortare are în vedere numărul de interschimbări
între entitățile (numerele) care trebuie sortate, neținând însă cont de locul unde
sunt memorate aceste variabile, adică de localizarea acestora (în regiștrii interni
ai CPU, în sub-sistemul de cache, în memorie, pe disc, în alt procesor din cadrul
sistemului de calcul, în cloud etc.) Desigur, această remarcă nu înseamnă că
programatorul va trebui să devină expert în arhitectura computerelor, dar nu o va
mai putea neglija total, dacă va dori performanţă în programare. În noua eră
“post PC” în care suntem deja, centrată pe Internet și pe sisteme de calcul
omniprezente (ubiquitous computing), fiabilitatea, disponibilitatea, consumul de
putere şi scalabilitatea vor trebui să devină criterii esenţiale, alături de
performanţa în sine, ceea ce implică iarăşi necesitatea unei noi viziuni pentru
arhitectul de computere.
De asemenea, se poate predicţiona o dezvoltare puternică, în continuare, a
procesoarelor multimedia. Aplicaţiile multimedia, spre deosebire de cele de uz
general, nu impun în mod necesar complicate arhitecturi de tip superscalar sau

345
VLIW. Aceste aplicaţii sunt caracterizate, în principal, de următoalele aspecte,
care le vor influenţa în mod direct arhitectura:
-structuri de date regulate, de tip vectorial, cu tendinţe de procesare identică
a scalarilor componenţi, care impun caracteristici de tip SIMD (Single
Instruction Multiple Data), de natură vectorială deci, a acestor procesoare;
-necesitatea procesării şi generării răspunsurilor în timp real;
-exploatarea paralelismului la nivelul thread-urilor independente ale
aplicaţiei (codări / decodări audio, video etc);
-localizare pronunţată a instrucţiunilor, prin existenţa unor mici bucle de
program şi nuclee de execuţie care domină timpul global de procesare.
“Convergenţa procesării informaţiei cu tehnicile de comunicaţii, ilustrată
elocvent în ultimii ani, mai ales prin dezvoltarea exponenţială a Internet-ului, a
determinat apariţia unor enorme cantităţi de date, informaţii şi cunoştinţe
reprezentate în forme dintre cele mai diverse și nestructurate. Această cantitate
de date va fi sporită nu doar de dezvoltarea în continuare a Internet-ului, dar şi
de apariţia unor tehnologii emergente, precum sistemele de calcul dedicate,
sistemele mobile şi respectiv sistemele omniprezente de prelucrare a informaţiei
(paradigma „Ubiquitous Computing”). Generalizarea Internetului – inclusiv prin
modele de tip Internet of Things, sistemele de achiziții de date organizate în
rețele de senzori, sateliți, drone, camere video, rețele de socializare etc.,
accentuează semnificativ această tendință. Ca urmare, aplicațiile software de
astăzi au un caracter computațional intensiv, dar și data-intensiv. În acest
context devine clară necesitatea extragerii eficiente de informaţii relevante şi
cunoştinţe din aceste masive de date puternic distribuite și eterogene. În
asemenea scopuri au fost dezvoltate metodele de Data Mining, reprezentând
tehnicile de extracţie a informaţiilor şi cunoştinţelor existente în date, informaţii
necunoscute apriori şi de un folos potenţial pentru utilizatori.
Un alt concept emergent și conex este acela de Big Data, aflat în strânsă
legătură cu cel numit Data Deluge – reprezentând faptul că astăzi datele noi se
genereaza într-un ritm exponențial, superior ca rată de dezvoltare celui dat de
legea lui Gordon Moore (co-fondator Intel), în virtutea căreia capacitatea
circuitelor de memorie se dublează la fiecare interval de cca. 18 luni. Big Data
nu este datorat doar cantității uriașe de date disponibile, ci și eterogenității
acestora (caracterului lor oarecum anarhic), respectiv vitezei lor de modificare.

346
Aceste caracteristici influențează negativ capacitatea de stocare și de prelucrare
a acestor date, în special în condiții restrictive de timp real. Algoritmii din
domeniul învăţării automate (Bayes-ieni, neuronali - supervizați sau
nesupervizați, bio-inspiraţi, evoluţionişti, tip Support Vector Machine –SVM
etc.) s-au dovedit extrem de utili în dezvoltarea unor metode eficiente de Data
Mining. (Algoritmii SVM sunt algoritmi adaptivi, care pot clasifica – inițial în
două clase, apoi generalizat – în mod eficient date care nu sunt separabile, în
spațiul lor de reprezentare, printr-un hiper-plan. În acest caz, algoritmii SVM
reprezintă, doar teoretic, aceste date într-un spațiu de ordin superior, unde liniar
separabilitatea ar fi posibilă și astfel se poate face o clasificare clară a
exemplelor pozitive respectiv negative. Au fost dezvoltați inițial de Vapnik în
lucrarea sa, Vapnik, V. - The Nature of Statistical Learning Theory, Springer
New York, 1995). Modelele de programare existente în calculul paralel și
distribuit scalabil, precum MapReduce spre exemplu (v.
https://en.wikipedia.org/wiki/MapReduce), pot fi deosebit de utile în vederea
implementării eficiente a unor asemenea metode complexe de Data Mining,
având în vedere că astăzi sistemele multi-core / many-core eterogene sunt
ubicue.
Față de acum 10-15 ani, aplicațiile informatice au caracteristici noi, fiind
mult mai inteligente și mai conectate la diverse sisteme și aplicații, în general
prin intermediul Internetului. În plus, în mod frecvent, aceste aplicații
controlează mediul, lumea digitală (virtuală) influențând direct lumea fizică
(Cyber-Physical Systems). Mai mult, pe lângă nevoia de performanță, au apărut
cerințe non-funcționale suplimentare, relativ noi, ale aplicațiilor software,
precum consumul limitat de putere/energie (esențial într-o lume a dispozitivelor
mobile), fiabilitate, robustețe, securitate etc., care trebuie asigurate încă din faza
de proiectare. Toate acestea pun o presiune fantastică pe specialiștii din
domeniul tehnologiei informației, care trebuie să se adapteze continuu la
asemenea schimbări majore de paradigmă.” (citat din lucrarea autorului, L. N.
VINŢAN - Educația universitară în ingineria calculatoarelor: spre o abordare
cultural-științifică (Academic Education in Computer Engineering: Towards a
Cultural-Scientific Approach), Revista de politica științei și scientometrie – serie
nouă, ISSN-L 1582-1218, Vol. 4, No. 3, pg. 204-208, septembrie 2015)

347
Notă. Algoritmii de învățare bayes-ieni se bazează pe teorema lui Bayes
din teoria probabilităților. Se știe că: , relația [1].
, relația [1bis]. Din identitățile (1) și (1bis)
rezultă imediat teorema lui Bayes: . Într-o notație mai
P(D/h)P(h)
consacrată, P(h/D)= (Teorema lui Bayes), unde:
P(D)
h=eveniment numit ipoteză (hypo-thesis); Probabilitatea P(h) este cunoscută din
istorii statistice.
D=eveniment apriori, pentru care există date statistice.
P(h/D)=probabilitatea ca să se întâmple evenimentul h, dacă (în condițiile în
care) D este îndeplinit.
P(D/h)= probabilitatea ca să se întâmple evenimentul D, dacă evenimentul h este
îndeplinit (există date statistice, în sensul calculării acestei probabilități.)

Exemplu (preluat și adaptat din Mitchell, T., Machine Learning, McGraw Hill
Publishers, 1997 [Mit97].)
h=un individ are cancer; din istorii statistice se știe că P(h)=0.008 P(-
h)=0.992 (probabilitatea ca un individ să nu aibă cancer.)
D=testul de cancer generează rezultat pozitiv (+), rezultând deci că pacientul ar
fi canceros; Notăm P(D)=P(+).
Se știe din evaluări statistice anterioare că: P(+/h)=0.98, reprezentând
probabilitatea ca un canceros să fie diagnosticat pozitiv (că are cancer) cu testul
considerat (diagnoză corectă).
P(-/h)=0.02, probabilitatea ca un canceros să fie diagnosticat negativ (că nu
are cancer), cu testul considerat (diagnoză eronată în fapt).
De asemenea, se știe din evaluari statistice anterioare că: P(-/-h)=0.97,
reprezentând probabilitatea ca un necanceros să fie diagnosticat negativ (că nu
are cancer) cu testul considerat (diagnoză corectă).
P(+/-h)=0.03, probabilitatea ca un necanceros să fie diagnosticat pozitiv (că
are cancer) cu testul considerat (diagnoză eronată).
Intrebare: Care este probabilitatea ca un pacient nou, pentru care testul de
cancer a fost pozitiv, să aibă într-adevar cancer?

348
P(h/D)=P(h/+)=P(un pacient nou are cancer/testul a arătat că are
P(+ /h)P(h)
cancer)= =0.98*0.008:P(D)=0.0784:P(D)=0.0784:P(+)=? (*)
P(D)
Dar cât este probabilitatea P(D)? Aceasta poate fi calculată astfel:
• P(+/h)P(h)=0.98*0.008=0.0784; pacientul este canceros și este
diagnosticat pozitiv (ca fiind canceros).
• P(+/-h)P(-h)=0.03*0.992=0.298; pacientul nu este canceros și este
diagnosticat pozitiv (ca fiind canceros! O eroare regretabilă, desigur...)

P(D)=P(+)=P(+/h)P(h)+P(+/-h)P(-h)=0.0784+0.298=0.3764
Din ecuația (*) rezulta P(pacient nou are cancer/testul a arătat că are
cancer)=0.0784:0.3764=0.208, deci aproximativ 21%.

Având în vedere cele expuse anterior, în următorul paragraf se prezintă


succint, caracteristicile procesării vectoriale, cea care stă la baza acestor
procesoare multimedia şi a altor procesoare şi co-procesoare specializate.

4.11. PROCESAREA VECTORIALĂ

Alături de procesarea pipeline, procesarea vectorială este o tehnică deosebit


de performantă, nelipsită în implementarea procesoarelor componente ale
supercalculatoarelor actuale (High Performance Computers). În esenţă,
procesarea vectorială reprezintă prelucrarea informaţiei numerice reprezentată
sub formă de vectori. Ca şi tehnica pipeline pe care o înglobează (în special în
procesarea datelor sub formă de vectori), procesarea vectorială urmăreşte, în
principiu, creşterea ratei de procesare la nivelul programului, fiind deci o tehnică
de exploatare a paralelismului la nivelul datelor din program. De multe ori
aceste arhitecturi vectoriale se întâlnesc în literatură sub numele de sisteme
SIMD (Single Instruction Multiple Data, în clasificarea profesorului Michael
Flynn). În continuare, se vor prezenta pe scurt câteva elemente arhitecturale
specifice acestui concept, precum şi problemele implicate de către acestea. Se va
insista, ca şi până acum, pe problemele interfeţei hardware-software, necesare

349
înţelegerii acestor arhitecturi de către utilizator, în vederea exploatării lor prin
software. Dacă precedentele modele de procesare nu afectau viziunea
programatorului, manifestată în limbaje de nivel mediu sau înalt (HLL), modelul
vectorial modifică această viziune "independentă de maşină" într-un mod
semnificativ, similar cu cel al sistemelor multiprocesor.
Prin noţiunea de vector se înţelege un tablou (şir) constituit din elemente
scalare de acelaşi tip. În general, elementele vectorilor sunt numere reprezentate
în virgulă mobilă (în formatul semn, mantisă - bază, caracteristică - exponent) pe
mai mulţi octeţi (de regulă pe 4, 8 sau 10 octeţi conform standardelor IEEE).
Prin lungimea vectorului înţelegem numărul de elemente scalare pe care le
conține acesta. După cum vom arăta, eficienţa maximă a procesării se obţine
atunci când numărul unităţilor de prelucrare din cadrul mașinii vectoriale,
coincide cu lungimea vectorului. O altă caracteristică a unui vector îl constituie
pasul vectorului, adică diferenţa numerică dintre valorile adreselor de memorie
la care se află două elemente scalare succesive din punct de vedere logic (V(i),
V(i+1)) ale unui vector. În fine, lungimea scalarului, adică lungimea în octeţi a
unui element scalar inclus în vector, are importanţă întrucât afectează
performanţa unităţilor scalare de execuţie.
În conformitate cu tipul operanzilor pe care îi prelucrează şi respectiv cu
tipul rezultatului pe care îl generează, se obişnuieşte clasificarea instrucţiunilor
vectoriale (I), în patru categorii de bază şi anume:
- instrucţiuni de tipul I1: V -> V, V= mulţimea operanzilor vectori. Spre
exemplu, o instrucţiune de complementare a valorilor unui vector.
- instrucţiuni de tipul I2: V -> S, S= mulţimea operanzilor scalari. Spre
exemplu, determinarea elementului scalar minim / maxim al unui vector (se
returnează de obicei indexul/indecșii acestuia/acestora, respectiv).
- instrucţiuni de tipul I3: VxV -> V, spre exemplu instrucţiuni aritmetico /
logice cu doi operanzi vectori
- instrucţiuni de tipul I4: VxS -> V, spre exemplu înmulţirea unui vector cu
un scalar.
Notă. Prezentăm succint, în continuare, în mod simplificat și intuitiv,
noțiunea de spațiu vectorial D-dimensional euclidian, care este, de fapt,
spațiul RD peste care se definesc operațiile de sumă vectorială (+) și produs
scalar a doi vectori (*), dar și de înmulțire a unui vector X cu un scalar (aX).

350
Motivul este de a sesiza cititorul că procesarea vectorială este inspirată tocmai
din noțiunea matematică de spațiu vectorial, de mare fertilitate în știință dar și în
tehnologie.
Definim:
• Înmulțirea unui vector X cu un scalar aX={ax1, ax2, …,axD}, ∀ a ∈ R.
• Suma vectorială între vectorii X și Y ∈ RD, X+Y={x1+y1, x2+y2, …,
xD+yD}.

Vectorul nul 0 ∈ RD are proprietatea X+0=X, ∀X ∈ RD. Sunt îndeplinite


proprietățile:
• X+Y=Y+X (comutativitatea adunării vectorilor)
• a0=0; 0=vectorul zero (0), a ∈ R
• 0X=0; 0=scalarul zero, X=vector
• a(X+Y)=aX+aY, ∀ a ∈ R, X, Y ∈ RD

Se definește o normă (distanță) ca fiind o funcție g: RD R, cu proprietățile:


• ∀X ∈ RD, g(X) ≥ 0.
• ∀X ∈ RD și g(X)=0 X=0.
• g(aX)= a g(X).
• ∀X , Y ∈ RD, g(X+Y) ≤ g(X)+g(Y) (inegalitatea triunghiului).

Un exemplu de normă este norma euclidiană a unui vector, definită ca fiind


D
g(X)= ∑ Xd
d =1
2
. Reprezintă modulul vectorului X.
D
Produsul scalar a doi vectori este XY= ∑ XdYd . Unghiul dintre doi vectori
d =1

este dat de formula următoare, care derivă din definiția produsului scalar a doi
vectori:
D

X ⋅Y ∑ XdYd
cos( X , Y ) = = d =1
,
D D
X Y
∑ Xd ∑ Yd
d =1
2

d =1
2

unde X reprezintă modulul (norma) vectorului X. Cosinusul anterior


reprezintă o măsură a similarității celor doi vectori. Cos (V1 ,V2 ) = 1 este echivalent

351
cu V2 =k V1 (vectori coliniari, unghiul dintre aceștia având măsura 0).
Cos (V1 , V2 ) = 0 în cazul unor vectori ortogonali. Se folosește în măsurarea
similarității documentelor, spre exemplu, unde un document se reprezintă ca un
vector de cuvinte, fiecare cuvânt având anexată o frecvență normalizată a
apariției sale în document. Astfel, două documente identice au măsura
cosinusului dintre ele 1, ceea ce rezultă și din formula anterioară, pentru X=Y.
Operațiile de adunare și înmulțire (produs scalar) a vectorilor au proprietățile:

XY=YX (comutativitate)
X(Y+Z)=XY+XZ (distributivitatea înmulțirii față de adunare, pe RD)

De remarcat că o singură instrucţiune vectorială specifică o multitudine de


operaţii scalare executate în paralel, pe datele din vectori, fiind deci echivalentă
cu o buclă de program executată de un microprocesor scalar. Întrucât, în
principiu, o buclă de instrucţiuni scalare poate fi înlocuită cu o singură
instrucţiune vectorială (dacă numărul de iterații al acesteia este egal cu lungimea
vectorilor fizici, disponibili în registrele mașinii vectoriale), rezultă că astfel se
pot elimina multe dintre problemele generate de hazardurile specifice
arhitecturilor scalare/superscalare pipeline (RAW-uri, ramificaţii, analiză
antialias etc.) O altă caracteristică a maşinilor vectoriale constă în faptul că
acestea deseori pipeline-izează procesarea pe elementele scalare ale vectorilor.
De asemenea, nu este greu de sesizat că procesarea vectorilor implică arhitecturi
de memorie cu acces întreţesut ("interleaving"), ceea ce implică avantajul că
latenţa memoriei va afecta la nivel de operare vector şi nu la nivelul de acces al
fiecărui scalar. O astfel de arhitectură de memorie cu acces întrețesut este
performantă, spre exemplu, atunci când se citesc mai multe cuvinte succesive
(situate în blocuri de memorie succesive), începând cu o adresă putere a lui 2.
Întrucât operează asupra vectorilor compuşi din scalari reprezentaţi în virgulă
mobilă, performanţa absolută a maşinilor vectoriale se exprimă, în general, în
MFLOPS (Mega FLotant Operation Per Second), spre deosebire de perfomanţa
procesoarelor scalare / MEM, exprimată uzual în MIPS (Mega Instruction Per
Second). Așadar, primele se focalizează pe exploatarea paralelismului la nivelul
datelor, pe cînd ultimele se concentrează pe paralelismul la nivelul

352
instrucțiunilor sau chiar al (micro)firelor de execuție concurente, după cum s-a
arătat.

Figura 4.35. Structura unei maşini vector - registru

Există maşini vectoriale de tip vector - registru şi respectiv memorie -


memorie. Maşinile vector - registru au caracteristic faptul că toate operaţiile cu
vectori, mai puţin cele cu referință la memorie, de tipul LOAD / STORE, se
execută între regiştrii vectoriali interni. Aceste maşini, reprezintă deci o
arhitectură analoagă arhitecturii LOAD / STORE din cadrul procesoarelor
MEM, cu set de instrucțiuni de tip RISC. Maşinile vectoriale de tipul memorie -
memorie, mai puţin frecvente decât primele, se caracterizează prin faptul că
toate operaţiile între vectori se execută memorie - memorie. O arhitectură
vectorială tipică, de tip vector-registru, este prezentată în Figura 4.35.
Regiştrii vectoriali sunt de lungime fixă, notată cu M, fiecare registru
conţinând un singur vector. Unităţile funcţionale sunt unităţi de execuţie, în
general pipeline-izate, care operează concurent, la nivelul scalarilor componenți,
asupra operanzilor vectori. Numărul de astfel de unităţi este diferit de la un
procesor la altul. Evident că este necesară şi o unitate de control specială, pentru
detecţia şi controlul hazardurilor din cadrul unităţilor de execuţie pipeline.

353
Unitatea LOAD / STORE accesează vectorii din memoria principală. Se
urmăreşte atingerea unei rate de transfer CPU - memorie de un element / ciclu
CPU, după depăşirea latenţei iniţiale a memoriei principale cu acces întreţesut.
În fine, mai există şi setul de regiştri scalari, necesari pentru execuţia proceselor
scalare.

Probleme în vectorizarea programelor.


Prin noţiunea de vectorizare a unui program scris într-un limbaj de nivel
înalt, se înţelege posibilitatea ca respectivul program să fie compilat utilizându-
se instrucţiuni maşină vectoriale. Cum instrucţiunile vectoriale se pretează, în
general, în compilarea buclelor de programe scrise în limbaje de nivel înalt, se
pune îndeosebi problema vectorizării buclelor de program. Aşadar, pentru ca o
maşină vectorială să fie eficientă, trebuie ca programul compilator să recunoască
dacă o buclă de program este, sau nu este, vectorizabilă, iar dacă este
vectorizabilă, atunci să genereze codul obiect corespunzător. Este evident că
există bucle de program nevectorizabile, de exemplu buclele recurente cu
dependențe intrinseci, precum secvenţa de mai jos:
for i = 1 to 100
X(i+1) = X(i) + X (i+1);
Desigur, aceeași buclă poate fi sau nu poate fi vectorizabilă, dependent de
modul în care este exprimată. Spre exemplu, datorită faptului că iterația i
depinde de iterația i-1, bucla de mai jos nu este vectorizabilă.
for i = 1 to N-1
Z(i) = X(i) + Z(i-1);
În schimb, bucla de program următoare este evident vectorizabilă.
for i = 0 to N-1
Z(i) = X(i) + Y(i);
Se pune deci problema, cum poate detecta compilatorul dacă o buclă de
program este, ori nu este, vectorizabilă ? Dintre multele posibile răspunsuri la
această problemă, în continuare prezentăm doar unul, deosebit de simplu. Prin
urmare, există o dependenţă de date care face o buclă nevectorizabilă dacă există
doi indici de iteraţii j şi k în limitele buclei, aşa încât se scrie un element cu
indicele (a*j+b) într-un vector şi apoi se citeşte acelaşi element din vector, de
astă dată cu indicele (c*k+d), unde a, b, c, d aparţin mulțimii Z. Altfel spus, o

354
buclă este nevectorizabilă dacă c.m.m.d.c. (a, c) divide (b - d). Prin prisma
acestui criteriu, de exemplu, rezultă imediat că bucla de mai jos este
vectorizabilă (pentru că 2 nu îl divide pe 3):
for i = 1 to 100
X(4i+3) = X(2i) + Y(i);
Lungimea fizică a regiştrilor vectori este fixă şi deci fatalmente limitată
(<=M). De multe ori în software se lucrează cu vectori de lungime mai mare
decât M (lungimea regiştrilor vectori) sau se lucrează cu vectori de lungime
necunoscută în momentul compilării, ca în exemplul buclei SAXPY (Single
Precision A*X+Y) de mai jos:
for i = 1 to n
Y(i) = a * X(i) + Y(i);
În astfel de cazuri, bucla se rescrie utilizând tehnica "strip mining", care
reprezintă o tehnică care permite generarea de cod program în urma compilării,
astfel încât fiecare operaţie vectorială să se efectueze pe vectori de lungime mai
mică decât M, sau egală cu acesta. În urma aplicării acestei tehnici, bucla
SAXPY devine:

start = 1
lungvec = (n mod M) ;
for j = 0 to (n / M) do ; întreg trunchiat
for i = start, start + lungvec -1 do
Y(i) = a * X(i ) + Y(i); VECTORIZABILĂ!
enddo
start = start + lungvec;
lungvec = M;
enddo

De remarcat că în prima iteraţie (j = 0) se operează pe vectori de lungime (n


mod M<M), iar în celelalte iteraţii (j = 1, 2, ...) se operează asupra unor vectori
de lungime maximă admisă, anume M în acest caz. Rezultă deci că ar fi necesară
o resursă specială (registru), care să controleze lungimea vectorilor asupra cărora
se operează la un moment dat.

355
Să considerăm acum o secvenţă de program destinată înmulţirii a două
matrici pătratice, A = B x C, de ordinul (100x100), ca mai jos.

for i = 1 to 100 do
for j = 1 to 100 do
A(i,j) = 0
for k = 1 to 100 do
A(i,j) = A(i,j) + B(i,k) * C(k,j)
enddo
enddo
enddo

Figura 4.36. Moduri de memorare ale matricilor în memorie

După cum se ştie, există două moduri de memorare a unei matrici în


memoria principală, anume: modul "row major" şi respectiv modul "column
major", ca în figura anterioară (Fig. 4.36).
În cazul buclei de program anterioare, în ipoteza memorării matricilor după
regula "column major", rezultă pas//C//=1 şi pas//B//=100. În ipoteza memorării
după cealaltă regulă, ar rezulta pas//C//=100 şi pas//B//=1. Nicicum nu este
optim din punct de vedere al accesului rapid al vectorilor linie / coloană.
Vectorii cu pasul 1 se pot încărca din memorie într-un singur ciclu, după cum se
arată în continuare. Acest fapt nu este valabil, prin memorii întrețesute clasice,
pentru vectori având un pas diferit de 1 (v. în continuare). Oricum, rezultă clar
necesitatea unor instrucţiuni mașină de încărcare / memorare care, pe lângă
adresa de bază şi lungimea vectorului, să aibă ca parametru şi pasul acestuia.
După cum am precizat, vectorii cu pasul diferit de 1 cauzează complicaţii în
accesul la memoria întreţesută. Pentru exemplificare, să considerăm 16 module

356
de memorie cu acces întreţesut, având fiecare timpul de acces ta. Timpul necesar
pentru a încărca un vector cu 64 elemente, având pasul 1, este 4*(Tciclu + ta)
unde Tciclu = durata unui ciclu din structura pipeline a procesorului vectorial.
La cealaltă extremă, timpul necesar pentru a încărca un vector de aceeaşi
lungime, având însă pasul 32, este 64*(Tciclu + ta), deci mult mai mare. Pentru
eliminarea problemelor implicate de pasul vectorului (latenţa mare a memoriei
pentru pasul diferit de 1) se cunosc mai multe soluţii, bazate pe îmbunătăţirea
arhitecturilor de memorii întreţesute. Spre exemplu, o matrice de 8 x 8 poate fi
memorată în 8 blocuri de memorie cu acces întreţesut ca în Figura 4.37.

Figura 4.37. Memorarea unei matrici într-o memorie cu acces întreţesut

Se observă că pasul unui vector "rând" este 1, iar pasul unui vector
"coloană" este 9 (dar 9modulo8=1). Cum două elemente succesive ale unei
coloane se află memorate în module succesive de memorie, printr-o proiectare
adecvată a adresării modulelor, bazat pe pasul vectorului accesat, se poate obţine
în acest caz o accesare optimală a vectorilor "coloane", la fel de rapidă ca şi
accesarea unui vector "rând", care are pasul =1. În general, dacă pasul vectorului
este relativ prim cu numărul de module de memorie, se pot elimina latenţele
ridicate de acces, prin organizări similare. Datorită frecvenţei accesării, în cazul
unor algoritmi numerici diverşi, a vectorilor diagonali, se impune şi optimizarea
accesului la aceşti vectori. Dacă numărul modulelor de memorie este o putere a
lui 2, nu poate fi optimizat simultan accesul la vectorii coloană şi respectiv la cei

357
diagonali. În acest caz se utilizează p module de memorie, unde p este un număr
prim. O asemenea arhitectură de memorie a fost implementată în cazul
supercomputerului BSP (Burroughs Scientific Processor - compania Unisys,
unul dintre primele calculatoare nanoprogramate, după cum am arătat în
articolul meu: VINȚAN L. - Microarhitectura nanoprogramata, PC-Report
Calculatoare Personale, nr. 23, Editura Hot-Soft, ISSN 1220-9856, Tg. Mures,
1994). În figura de mai jos (4.38) se prezintă o astfel de structură cu 5 module,
având memorată o matrice de 4x4. Pasul vectorilor rând este 1, cel al vectorilor
coloană este 6, iar cel al vectorilor diagonali este 7. De remarcat că accesul
optimizat simultan la cele trei tipuri de vectori este posibil datorită faptului că
numărul de module ale memoriei (5) este relativ prim cu paşii vectorilor rând,
coloană respectiv diagonali (1, 6, 7).
În acest caz, alinierea ar fi fost efectivă dacă s-ar fi accesat, spre exemplu,
vectorul diagonal 00 11 22 33. Evident că selecţia elementelor, citirea şi
alinierea acestora se fac comandate de un automat, în baza adresei de bază,
lungimii şi pasului vectorului accesat (citit în acest caz).

Figura 4.38. Optimizarea memorării vectorilor linie, coloană şi diagonali


Să considerăm acum o arhitectură vectorială de tip SIMD (Single
Instruction Multiple Data) ca în Figura 4.39. Această arhitectură SIMD se mai
numeşte şi procesor matricial, deoarece este constituită din mai multe
procesoare identice, relativ simple, care execută acelaşi program simultan,
asupra unor date locale diferite. Putem considera aceste structuri de calcul ca
făcând trecerea la arhitecturile de tip MIMD (Multiple Instruction Multiple
Data).

358
Figura 4.39. Arhitectură SIMD
De precizat că o astfel de structură de calcul execută un singur program
asupra mai multor operanzi scalari, în mod simultan (ALU-uri multiple) sau
acelaşi program este executat în paralel de mai multe procesoare (UE), operând
asupra propriilor seturi de date memorate în resursele locale de memorie (Single
Program Multiple Data). Nu intrăm aici în amănunte asupra topologiei şi
caracteristicilor diverselor tipuri de reţele de interconectare, prin intermediul
cărora se realizează schimbul de date între unităţile locale de execuţie, acestea
sunt arhicunoscute şi prezentate în toate cărţile generale de arhitectura
sistemelor de calcul.
Să considerăm acum o aplicaţie care însumează 128.000 de elemente pe un
sistem SIMD, având 128 unităţi de execuţie distincte. Pentru început, un
procesor specializat de I/O (front computer) plasează câte un subset de 1000 de
elemente în memoria locală a fiecărei unităţi de execuţie (UE). Apoi fiecare UE
procesează câte o secvenţă de program astfel:
sum = 0;
for (i = 0, i < 1000; i = i + 1)
sum = sum + Al(i);
Al(i) reprezintă o tabelă locală, conţinând 1000 de scalari, câte una în
fiecare UE. Rezultă la final faptul că fiecare dintre cele 128 de sume parţiale va
fi situată în altă UE. Programul SIMD pentru însumarea sumelor parţiale
distribuite ar fi următorul:

359
limit = 128;
half = 128;
repeat
half = half/2
if(Pn >= half && Pn < limit) send (Pn-half;sum); Message Passing
if(Pn < half) sum = sum + receive( );
limit = half;
until (half = = 1);

Funcţia send(x,y) are parametrii x = adresă procesor destinaţie, y =


variabilă emisă. Precizăm că am notat prin Pn numărul unităţii de excuţie, deci
Pn ∈ {0, 1, 2,....,127}. Send / Receive sunt funcţii emisie / recepţie prin / din
reţeaua de interconectare a unităţilor de execuţie aferente SIMD. Suma finală se
va obţine în UE0. Timpul de execuţie este de ordinul O(log2N), în cazul nostru
concret 7 pași, unde N=numărul de UE, spre deosebire de un sistem
convenţional tip SISD care ar realiza adunarea sumelor parţiale într-un timp
O(N). În prima iterație, a 2-a jumătate din mulțimea procesoarelor trimite
rezultatele parțiale celor din prima jumătate. Apoi, al 2-lea sfert din prima
jumătate trimite rezultatele procesoarelor din primul sfert ș.a.m.d., în mod
iterativ. Secvenţa anterioară poate fi scrisă mai elegant, prin eliminarea
variabilei half, ca mai jos:

limit = 128;
repeat
if(Pn >= limit/2 && Pn < limit) send (Pn-limit/2;sum); Message Passing
if(Pn < limit/2) sum = sum + receive( );
limit = limit/2;
until (limit = = 1);

Buclele condiţionate implică şi ele anumite cerinţe în procesarea vectorială.


Spre exemplu, să considerăm secvenţa următoare :

for i = 1 to 100 do

360
if(X(i) diferit de 0) then
X(i) = X(i) + Y(i);
endif
enddo

Pentru ca secvenţa să fie vectorizabilă, rezultă necesitatea existenţei unui


registru vector de mascare, de lungime egală cu lungimea regiştrilor vectori.
Acest registru va conţine elemente pe '0' sau pe '1', în conformitate cu testul
făcut asupra unui vector pe o anumită condiţie. Operaţiile vectoriale se vor
executa numai asupra acelor elemente din vector cărora le corespunde un
element pe '1' în registrul de mascare. Considerând adresele de bază ale
vectorilor X şi Y memorate în regiştrii Rx respectiv Ry, bucla anterioară este
echivalentă cu următoarea secvenţă de instrucţiuni maşină vectorială:

LV V1, Rx; V1<-MEM/Rx


LV V2, Ry; V2<-MEM/Ry
LD F0,#0; instr. scalară F0<-0
SNESV F0, V1; setează elementele reg. de mascare aferent
elementelor V1(i) diferit de 0, i = 1-100
ADDV V1, V1, V2; V1<- (V1)+(V2)
SETVM; setează toate elementele reg. de mascare VM
SV Rx, V1; V1 -> MEM/Rx

Există bucle de program nevectorizabile integral, datorate unor dependenţe


de date "ascunse", generate de existenţa unor variabile scalare, ca mai jos :

x = 0;
for i = 1 to 100 do
x = x + A(i) * B(i); 200 de operații:100 +, 100 *
enddo

Această buclă nevectorizabilă se poate transforma în următoarea secvenţă


semi-vectorizabilă, obţinută prin transformarea scalarului în vector şi care
paralelizează cele 100 de înmulţiri care se executau serial în secvenţa anterioară:

361
y = 0;
for i = 1 to 100 do
X(i) = A(i) * B(i); vectorizabil (o singură operație de *)
enddo
for i = 1 to 100 do
y = y + X(i); nevectorizabil (100 +)
enddo

Multe dintre supercomputerele actuale reprezintă multiprocesoare, formate


din mai multe procesoare vectoriale care procesează în paralel, având deci un
accentuat paralelism spaţial. În acest context să considerăm bucla de program:

for i = 1 to n do
X(i) = Y(i) + Z(i); (1)
Y(i+3) = X(i + 4); (2)
enddo

Un compilator inteligent din cadrul unui sistem vectorial multiprocesor,


trebuie să sesizeze potenţialul paralelism de date din cadrul acestei secvenţe,
precum şi dependenţele de date dintre relaţiile (1) şi (2). Secvenţa de mai jos va
fi transformată astfel:

for j=1 to n step 3 do


for i=j to j+2 doall
X(i)= Y(i)+Z(i); (1')
Y(i+3)=X(i+4) ; (2')
enddoall
enddo

Se observă că s-au obţinut de fapt două bucle vectorizate și deci s-au


eliminat dependenţele de date (i=1,3; i=4,6; i=7,9;…). Instrucţiunea "doall"
determină execuţia paralelă, pe procesoare distincte, a secvenţelor (1') şi (2'),
acum perfect vectorizabile şi independente.

362
Desigur că generarea de cod vectorial sau cod vectorial-paralel, prin
intermediul unor compilatoare pe limbaje convenţionale, nu poate exploata
optimal procesarea vectorială respectiv multiprocesarea vectorială. Actualmente,
se lucrează intens, cu deosebire în Statele Unite ale Americii dar și în Europa (v.
spre exemplu rețeaua de excelență în cercetarea sistemelor de calcul avansate,
finanțată de Comisia Europeană, numită HiPEAC – v. https://www.hipeac.net/),
pentru elaborarea de noi limbaje specifice programării vectoriale / concurente
(IBM Parallel Fortran, Cedar Fortran, limbajul VPC - un supraset vectorial /
concurent al limbajului C etc.). Cert este că odată cu avansul tehnologiei VLSI,
preţul acestor microprocesoare vectoriale va scădea şi ele vor pătrunde tot mai
masiv pe piaţă. Încă din 1988 firma Ardent a realizat un CPU în jurul unui
microprocesor RISC + un coprocesor vectorial integrat, la preţul de numai 180$.
Ca şi procesarea paralelă la nivelul instrucţiunilor, procesarea vectorială a
produs un impact puternic, atât asupra proiectării hardware, cât şi asupra
compilatoarelor. Oricum, dezvoltarea compilatoarelor reprezintă provocarea
esenţială şi în aceste arhitecturi paralele.
În concluzie, am prezentat aici câteva idei relative la procesarea vectorială,
întrucât reprezintă un concept arhitectural de referinţă în proiectarea unui
procesor performant (MMX - MultiMedia eXtension, Multiple Math eXtension
sau Matrix Math eXtension) chiar dacă nu intră strict în categoria arhitecturilor
cu paralelism la nivelul instrucţiunilor sau a firelor de execuție, care se
abordează cu precădere în această lucrare.

4.12. SISTEME MULTIPROCESOR

În intervalul de timp 1986 – 2002 /2003, performanța microprocesoarelor


în sine, a crescut în medie, cu o rată de cca. 52% pe an. Din anul 2002 însă,
creșterea de performanță a fost mai mică, de doar 20% pe an [Hen11]. Pentru a
menține în continuare o creștere semnificativă a performanțelor
microprocesoarelor, marii producători au schimbat strategia și au trecut la
proiectarea și producția de sisteme multiprocesor integrate pe același cip sau în
cadrul unei capsule unice (multicore). Un sistem multicore combină două sau

363
mai multe procesoare (nuclee, cores) pe o singură pastilă de siliciu. În acest caz,
latențele miss-urilor cache to cache sunt semnificativ mai mici decât la clasicele
sisteme multiprocesor realizate pe plăci separate. De asemenea, în acest caz
lărgimea de bandă a bus-urilor sau rețelelor de interconectare este mai mare,
datorită integrării etc. Evident că un astfel de sistem cu N procesoare ar fi
utilizat la maximum, dacă acestea ar procesa simultan N fire de execuție.
Compania Intel, cel mai puternic proiectant și producător de
microprocesoare de uz general, a început această schimbare paradigmatică încă
din anul 2004, în principal datorită faptului că nu mai puteau asigura creșteri de
performanță spectaculoase pe modelele mono-core. Sistemele multicore sunt de
fapt sisteme de calcul paralel, în care procesoarele componente rulează în paralel
firele de execuție ale task-ului curent, puse în evidență printr-un limbaj de
programare concurentă, în scopul accelerării vitezei de procesare (sau task-uri
concurente, desigur). Evident că sistemele multicore accelerează și procesările
de tip multiuser /multitasking, implementate la nivelul sistemelor de operare.
Se pune în mod natural întrebarea: ce a determinat această schimbare de
strategie - numită “sea change in computing” (cf. Paul Otellini, Președinte Intel,
2005) - în domeniul microprocesoarelor? Câteva dintre motivele trecerii la
microprocesoare de tip multicore sunt următoarele:

o Consumul de putere (statică și dinamică) ridicat (Power wall), care


implică o densitate de putere nerezonabilă. Astfel, la o frecvență de
lucru a procesorului de 3 GHz, tipică pentru calculatoarele
obișnuite, se ajungea la densități de putere de cca. 200W/cm2,
considerate foarte mari. Așadar, la asemenea frecvențe, disipația
termică implicată era una enormă. Reamintim faptul că puterea
dinamică Pd=kCV2F, unde k = constantă, C= reactanța capacitivă a
procesorului, V=tensiunea de alimentare și F=frecvența de lucru.
De aici rezultă în mod clar că frecvența de lucru a CPU nu mai
putea crește. Calcule ale specialiștilor arătau că, la nivelul
tehnologiei electronice a anului 2004, un microprocesor la 15 GHz
ar fi ajuns la o densitate de putere egală cu cea dintr-un reactor
nuclear! Acesta a fost motivul principal care a determinat trecerea
la tehnologia multicore.

364
o Prăpastia de comunicație procesor-memorie (Memory wall) din
sistemele uniprocesor, determinată, după cum am mai arătat în
acest curs, de viteza prea mare a microprocesorului în raport cu
latența memoriei principale (esențialmente DRAM). Având în
vedere că sistemele multiprocesor procesează în mod predilect
programe concurente, cu fire multiple de execuție, evident că
acestea vor masca latența memoriei principale, după cum am arătat
chiar în acest curs (vezi paragraful intitulat Microprocesoare
multithread)
o Exploatarea ILP şi-a cam atins limitele ideatice (ILP wall), ideile de
exploatare a paralelismului la nivel de instrucțiuni prin metode de
scheduling dinamic / static, nu mai conduceau la inovații
semnificative din punct de vedere al eficienței.
o În rezumat, Memory wall + ILP wall + Power wall = Brick wall
(limitare 3-dimensională)

Așadar, companiile producătoare de sisteme multicore au sperat, prin


această trecere, că vor reuși să continue creșterea spectaculoasă a performanței
microprocesoarelor, mizând pe faptul că aceasta va fi direct proporțională cu
numărul de procesoare. Astfel, nu mai este necesară creșterea frecvenței de tact
(esențială în creșterea performanțelor sistemelor uniprocesor), dimpotrivă chiar,
aceasta putând fi chiar redusă, cu beneficii asupra consumului de putere și a
dispației termice. Cum performanța globală a sistemelor multicore poate fi
îmbunătățită fără creșterea în continuare a frecvenței de tact, aceste sisteme
oferă un raport mai bun performanță/Watt decât un sistem monoprocesor cu
performanțe similare. De remarcat faptul că și în lumea sistemelor dedicate
(incorporate) tehnologia multicore câștigă tot mai mult teren [Wol07].
Ceea ce companiile producătoare de procesoare multicore au evitat să
sublinieze în mod apăsat, este faptul că, la nivelul programatorului, utilizarea
eficientă a acestor sisteme paralele necesită limbaje de programare concurentă,
care să poată crea fire multiple de procesare, care să fie asignate în timpul rulării
taskului, diferitelor procesoare, în vederea exploatării paralelismului TLP.
Desigur că la nivelul sistemelor de operare această chestiune era rezolvată,
acestea având deja posibilitatea de a rula multiuser / multitasking, dar și

365
multithreading, ceea ce este benefic pentru sistemele multicore, întrucât ele pot
paraleliza concurența expusă la nivelul programului.
Așadar, un sistem multiprocesor este un calculator paralel constituit dintr-
un set de procesoare (sau calculatoare), care cooperează și comunică între ele
pentru a rula o aplicație mai repede decât pe un sistem de calcul mono-procesor,
prin paralelizarea firelor sale de execuție [Cul99]. Procesoarele comunică prin
intermediul unei rețele de comunicații. Concurența este pusă în evidență de către
ultilizator, la nivelul limbajului de programare, prin “spargerea” aplicației în mai
multe fire concurente de execuție. Evident că această concurență explicitată la
nivelul limbajului HLL nu are legătură cu numărul de procesoare și alte detalii
hardware. Maparea efectivă a concurenței (a firelor de execuție aferente
taskului) pe procesoarele hardware, se numește paralelism și este făcută de
sistemul de operare, în timpul rulării taskului. Cu alte cuvinte, concurența este o
caracteristică statică intrinsecă a aplicației (problemei), iar paralelismul
reprezintă maparea dinamică (run-time) a concurenței pe diferitele unități
hardware de procesare. Calculatoarele paralele pot profita de avantajul utilizării
mai multor procesoare simple, ieftine, de tip low power, dar ele pot conține și
procesoare heterogene, incluzând procesoare superscalare cu execuții out of
order ale instrucțiunilor, SMT, DSP, multimedia etc.
Se au în vedere următoarele caracteristici importante referitoare la sistemele
multiprocesor:

• Scalabilitate – posibilitatea de a dezvolta sistemul prin adăugarea de noi


procesoare, pentru a crește corespunzător performanța globală
• Toleranța la defectări, bazat în principal pe redundanța hardware
intrinsecă sistemului multi-microprocesor
• Performanță globală mai mare decât a unui sistem monoprocesor
echivalent (în complexitate)

Referitor la toleranța la defectări, uneori sistemele multiprocesor se


implementează tocmai în acest scop, în domenii în care funcționarea corectă
este absolut necesară (spre exemplu, în industria de aeronave). În aceste
cazuri, deseori se implementează sisteme redundante cu modularitate n (n-
modular redundancy). Spre exemplu, pentru n=3 (Triple Modular

366
Redundancy - TMR), informația de intrare intră simultan în cele 3 module,
fiecare dintre acestea generând o anumită ieșire Ok. Cele 3 ieșiri intră într-un
circuit de tip voter. Considerând că cel mult o ieșire ar putea fi eronată,
voterul generează o ieșire globală în formă normală disjunctivă, anume
O=O1O2 + O1O3 + O2O3, unde prin + s-a notat operația SAU logic. Așadar,
se merge pe principiul corectitudinii majorității.
Metricile de performanță cele mai uzitate pentru sistemele multiprocesor sunt
următoarele:

- Accelerarea performanței (N procesoare) = Timp rulare (1


procesor)/Timp rulare (N procesoare) = Performanța (N
procesoare)/Performanța (1 procesor)
- Latența – timpul necesar procesării unei operații sau tranzacții
[secunde per operație]
- Lărgimea de bandă a rețelei de interconectare – numărul de operații
/ tranzacții care se pot procesa în paralel [operații per secundă]
- Raportul Performanță/Complexitate (Cost) – impactul creșterii de
performanță asupra complexității sistemului.

Ideal ar fi ca un sistem multi-microprocesor (SMM) dotat cu N procesoare


să proceseze un program de N ori mai rapid decât un sistem monoprocesor
echivalent, cerinţă numită scalabilitate completă sau ideală. În realitate, acest
deziderat de scalabilitate nu se realizează, din multiple motive. În privinţa
scalabilităţii, aceasta este mai uşor de realizat pe un sistem cu resurse distribuite
(slab cuplate), decât pe unul având resurse centralizate (strâns cuplate), întrucât
acestea din urmă constituie un factor de “strangulare” a activităţii, mai ales prin
busul (busurile) de legătură, după cum se va arăta în continuare.
Dintre cauzele care stau în calea unei scalabilităţi ideale, se amintesc
următoarele:

1. Gradul de secvenţialitate intrinsecă a algoritmului executat. Aşa, spre


exemplu, există în cadrul unui algoritm operaţii secvenţiale dependente RAW
de date şi deci imposibil de partajat, în vederea procesării lor paralele pe mai
multe procesoare. Exemplu:

367
n = y + z; 
 scalar secvential dependent de RAW 100% = secvenţial
a = n + b; 
------------------------------------------------------------------------------------------
for i = 1 to 10 
 paralelizabil pe 10 procesoare, complet
A(i) = B(i) + C(i); 
(1-f)×100%=paralelizabil
------------------------------------------------------------------------------------------

Accelerarea S (Speed-up) pentru un SMM cu N procesoare este dată de


următoarea formulă, prin definiţie:
Ts
S= ≥1
TN

unde:
Ts = timpul de execuţie pentru cel mai rapid algoritm secvenţial care
rezolvă problema pe un mono-procesor (SISD – Single Instruction Single Data,
conform taxonomiei lui Flynn)
TN = timpul de execuţie al algoritmului paralel executat pe un SMM cu N
µprocesoare.
Dacă notăm cu f = fracţia (procentajul, fracția - ratio) din algoritm care are
un caracter eminamente secvenţial, f∈[0,1], putem scrie:
(1 − f) ⋅ Ts
TN = f ⋅ Ts + ,
N

Ts
adică S=
(1 − f) ⋅ Ts
f ⋅ Ts +
N

1 1
sau: S= ≤ Legea lui Eugene (Gene) Amdahl, 1≤S≤N
(1 − f) f
f+
N
(scalabil)
Legea lui G. Amdahl (un mare proiectant de supercalculatoare) sugerează
că un procentaj (fx100%) oricât de scăzut de calcule secvenţiale (dependente

368
RAW de date), impune o limită superioară a accelerării (dată de 1/f) care poate
fi obţinută pentru un anumit algoritm paralel pe un SMM, indiferent de numărul
N al procesoarelor din sistem, de tipul acestora, de topologia rețelei de
interconectare etc. Spre exemplu, pentru un f=0.2 (20%) S ≤ 5. Funcția S(f)
este evident neliniară (hiperbolică). Se definește ca metrică de performanță și
S
eficiența E (n) = , semnificând accelerarea per unitate de procesare (CPU).
n
Motivele concrete ale acestei limitări semnificative (neliniare) de
performanță sunt, în principal, următoarele:

1. Timpul consumat cu sincronizarea şi comunicarea între procesele (firele


de execuție) rezidente pe diversele (µ)procesoare din sistem.
2. Imposibilitatea sau neputința balansării optimale – de către sistemul de
operare și sistemul hardware – a activităţii procesoarelor din sistem
(Load Balancing). Frecvent, nu se poate evita situaţia în care anumite
procesoare să fie practic inactive sau cu un grad scăzut de utilizare.
3. Planificarea sub-optimală, din punct de vedere al performanței globale, a
proceselor din punct de vedere software (activare proces, punere în
aşteptare a unui proces, schimbarea contextului în comutarea proceselor
etc.)
4. Operaţiile de I/O, în cazul nesuprapunerii lor peste activitatea de execuţie
a task-ului de către procesor (overlapping).

Un parametru important, care influenţează direct performanţa unui SMM,


este dat de granularitatea algoritmului de executat, adică de dimensiunea medie
(din punct de vedere al numărului de instrucţiuni, timpului de execuţie etc.) a
unei unităţi secvenţiale de program (USP). Prin USP se înţelege o secvenţă de
program în cadrul algoritmului paralel în cadrul căreia nu se execută operaţii de
sincronizare sau de comunicare de date cu alte procese (fire). Deci o USP se
execută doar pe un singur procesor. Se mai defineşte şi un alt parametru
important, numit timp mediu de comunicaţie între două task-uri nerezidente pe
acelaşi procesor.
Preluat din [Hen11], să considerăm acum un sistem multiprocesor cu 100
procesoare omogene simple (in order, fără paralelism la nivelul instrucțiunilor,

369
low power etc., deci care nu exploatează paralelismul la nivel de instrucțiuni din
cadrul unui USP) care accelerează corespunzător 90% din programul dinamic.
Se consideră că restul, de doar 10% din codul dinamic, este secvențial. În acest
caz, accelerarea este:
1
S= = 1 / (0.1 + 0.9/100) = 9.2
(1 − f)
f+
N
Accelerarea obţinută pe un sistem neomogen, de complexitate echivalentă
cu cel anterior, având 90 de nuclee simple şi un nucleu complex (superscalar,
SMT etc., deci care exploatează paralelismul la nivel de instrucțiuni din cadrul
unui USP) care ar reduce la jumătate timpul de rulare a codului secvențial, prin
exploatarea ILP, este superioară:
S’= 1 / (0.1/2 + 0.9/90) = 16.7>S=9.2
Din acest exemplu simplu devine evident că un sistem multicore de uz
general trebuie să fie unul de tip neomogen, care să conțină procesoare de
diverse tipuri (superscalare out of order, DSP, VLIW, multimedia etc.), pentru
că, în fond, aplicațiile moderne sunt neomogene (deseori conțin, deopotrivă,
procesare de uz general, algoritmi numerici, procesare multimedia – text, voce,
imagini, procesare de semnal etc.) Desigur că, în anumite aplicații dedicate,
precum spre exemplu cele de procesare de imagini, procesoarele pot conține
doar nuclee simple, precum procesoarele GPU (Graphical Processing Unit),
care conțin mii de nuclee simple și care, pe timpul rulării, filtrează porțiuni
diferite ale imaginii respective. Acestea ating performanțe la vârf de câțiva
GFLOPS – anul 2015)
Din punct de vedere al raportului între granularitatea algoritmului (G,
durata de procesare medie a unei USP) şi timpul mediu de comunicaţie între
firele de execuție (C), se citează în literatura de specialitate două tipuri de
algoritmi:

a. Coarse grain parallelism (paralelism intrinsec masiv), caracterizaţi de


un raport G/C relativ “mare”. Aceşti algoritmi se pretează, cu bune
rezultate, la procesare pe sisteme paralele de tip SIMD (vectoriale,
Single Instruction Multiple Data) sau MIMD (multiprocesoare, Multiple
Instruction Multiple Data). Plastic spus, în acest caz o USP (thread) este

370
“mare”, adică are un număr mare de instrucțiuni, procesând un timp
îndelungat deci. Evident, cantitatea medie de comunicații între aceste
USP-uri va fi mai mică.
b. Fine grain parallelism (paralelism intrinsec redus), caracterizaţi de un
raport G/C relativ mic. În acest caz nu este recomandabilă
multiprocesarea, din cauza granularităţii fine a algoritmului precum şi a
timpilor mari de comunicaţie/ sincronizare între procesoare (threads),
elemente ce vor reduce dramatic accelerarea SMM. În acest caz, se
recomandă exploatarea paralelismului redus prin tehnici mono-procesor
de tipul procesare pipeline a instrucțiunilor și “Instruction Level
Parallelism”, prin procesări speculative ale instrucțiunilor (procesoare
superscalare cu execuții out of order ale instrucțiunilor, procesoare SMT
etc.)
O problemă majoră, de un mare interes practic la ora actuală, o constituie
scrierea de software concurent pentru SMM. Sarcina unui sistem de operare și a
unui compilator SMM este dificilă, întrucât trebuie să determine o metodă de a
executa mai multe operaţii (programe, fire), pe mai multe procesoare, în
momentele de timp neprecizabile. Apoi, trebuie optimizat raportul G/C printr-o
judicioasă partiţionare – în general statică, dar și dinamică – a algoritmului de
executat.

GRANULARITATE ŞI COMUNICARE. MODELE ANALITICE DE


ESTIMARE A PERFORMANŢEI

Acest paragraf a fost preluat, cu revizuiri și minore adaosuri, din cartea autorului
[Vin00b].

1. Un model “pesimist”

371
Se consideră, pentru început, un sistem biprocesor care trebuie să execute
un program (proces, aplicaţie) ce conţine M task-uri (sau fire de execuție, mai
exact). Se presupune că fiecare astfel de task se execută în "G" unităţi relative de
timp şi că oricare două task-uri nerezidente pe acelaşi procesor, consumă în
medie "C" unităţi relative de timp pentru intercomunicaţii (schimburi de date +
sincronizări). Se va considera că "K" task-uri (fire de execuție) se vor executa pe
un procesor, iar restul de (M-K) task-uri, pe celălalt procesor, (∀) k=1,2,...,M.
“Pesimismul” modelului constă în faptul că se va considera că nu este
posibilă nici o suprapunere între execuţia unui task şi comunicarea acestuia cu
celelalte task-uri nerezidente pe acelaşi procesor (overlapping). În acest caz,
timpul de execuţie (ET) aferent aplicaţiei considerate este dat de relaţia:

ET = G ⋅ Max(M − K,K) + C ⋅ (M − K) ⋅ K

Particularizând acum pentru M=50, G/C=10 (granularitate "mică"), G=1


(un task se execută într-o singură unitate de timp), se obţine:

ET(K) = Max (50-K, K) + 0.1 K(50-K)

Primul termen al sumei precedente conține timpul efectiv de procesare pe


un microprocesor, iar al 2-lea termen semnifică timpul de comunicații inter-
procesoare. În acest caz, rezultă Koptim = 0, adică alegerea optimă ar fi ca toate
cele 50 de taskuri să ruleze pe un singur procesor (monoprocesare!) Dacă am
considera granularitatea aplicaţiei G/C = 50 ("mare"), se va obţine:

ET(K) = Max (50-K, K) + K(50-K)/40

În acest caz, Koptim = M/2 = 25, adică o distribuire uniformă a numărului de


task-uri pe cele două procesoare ("coarse grain parallelism") ar fi optimală.
Reprezentarea grafică a acestei funcții ET(n) este prezentată în figura următoare
(cu ajutorul programului Wolfram Alpha):

372
Figura 3.12. Reprezentarea timpului de execuție ET(K) funcție de
distribuția celor 50 de task-uri pe cele două procesoare

În concluzie, pentru un sistem biprocesor în condiţiile date, strategia


optimală în vederea obţinerii performanţei (ET) maxime este:

G M
a) Dacă ≤ ⇒ Koptim = 0 (monoprocesare)
C 2
G M
b) Dacă > ⇒ Koptim = M/2 (procesare paralelă omogenă)
C 2

Generalizare pentru N procesoare (N>2)


Se consideră pentru început un SMM cu trei procesoare P1, P2, P3, care are
de executat o aplicaţie având M task-uri sau fire de execuție, în cazul unui
program concurent. Se presupune că P1 execută k1 task-uri (thread-uri), P2
execută k2 task-uri, iar P3 execută k3 task-uri, astfel încât k1+k2+k3=M.

Figura 4.40. 3 procesoare interconectate


Rezultă imediat că timpul de comunicaţii (ETc) inter-task-uri va fi:

373
3
ETc =
C
[(k 2 + k 3 ) ⋅ k1 + (k1 + k 3 ) ⋅ k 2 + (k1 + k 2 ) ⋅ k 3 ] = C ∑ k i (M − k i )
2 2 i =1

Prin urmare, pentru un sistem cu N procesoare avem:

C N
ET = G ⋅ Max(k i ) + ∑ ki (M − ki )
2 i =1
sau:

C  2 N 2 
ET = G ⋅ Max(k i ) + M − ∑ k i , (∀) i = 1, N
2  i =1

În ipoteza - nu neapărat optimală - unei alocări uniforme a task-urilor


pentru toate cele N procesoare, avem prin particularizare în formula
precedentă:  k i =
M

 N

G⋅M C⋅M 2 C⋅M2


ET N = + −
N 2 2N

Timpul de execuţie al algoritmului pe un singur procesor este:

ET1 = G×M

"Punctul critic" se atinge când ETN = ET1, adică:

G⋅M C⋅M2 C⋅M2


+ = +G⋅M , sau:
N 2 2N
 1  C⋅M  1
G 1 −  = 1 −  , adică:
 N 2  N
G M
= (prag de performanţă identic, pentru monoprocesoare-multiprocesoare)
C 2

În concluzie:

374
G M
a) Dacă ≥ , este indicată multiprocesare omogenă (coarse grain)
C 2
G M
b) Dacă < , monoprocesarea este mai indicată (fine grain)
C 2

2. Un model mai "optimist"

Să presupunem acum că execuţia task-urilor se poate suprapune cu


comunicaţia inter-procesoare. În acest caz avem timpul de procesare (ET):

 C N 
ET = MaxG ⋅ Max(k i ), ∑ k i ⋅ (M − k i )
 2 i =1 

Optimul s-ar obţine atunci când suprapunerea celor două componente ar fi


perfectă, adică:

G⋅M C⋅M 2 C⋅M2 G C⋅M  1


= − ⇒ =  1− 
N 2 2N 2 N 2  N

Pentru un număr "mare" de procesoare (N) avem:

G C⋅M 2 G
≅ ⇒ N optim = ⋅
N 2 N C

Obs. 1. N creşte liniar cu granularitatea (G/C) aplicaţiei.


Obs. 2. Aparent paradoxal, Noptim este invers proporţional cu numărul de task-uri
(thread-uri) paralele (M) în care s-a împărţit aplicaţia. Acest fapt nu este
totuși paradoxal pentru că creşterea lui M, determină creşterea timpului
de comunicaţii interprocesor (ETc).

3. Un model cu costuri liniare de comunicaţie

Să considerăm acum un model de SMM în care costurile de comunicaţie să


fie liniare cu numărul de procesoare N şi nu cu numărul de task-uri asigurate
fiecărui procesor (Pi), ca în modelul precedent.

375
În acest caz, putem scrie că timpul de procesare este:

ET N = G ⋅ Max(k i ) + C ⋅ N

Să determinăm în continuare, până la ce "N", avem că ETN ≥ ETN+1, adică


să determinăm în acest caz un număr optim de procesoare ( ∆ETN =0 Nopt) care
să proceseze cele M task-uri.
Pentru simplificare, se consideră o distribuţie uniformă de task-uri/procesor
 M
 ki =  , adică:
 N

G⋅M
ET N = +C⋅N
N
1 1 
⇒ ∆ETN = ETN − ETN +1 = G ⋅ M  −  − C =0, adică:
 N N +1
G⋅M G N ( N + 1)
∆ETN = −C = 0 ⇔ =
N ( N + 1) C M
G
Rezultă deci N optim ≅ ⋅M .
C

Aşadar Noptim nu creşte proporţional cu numărul de task-uri (M), ci


proporţional cu M şi, de asemenea, proporţional cu rădăcina pătrată a
G
raportului . Pentru un N > Noptim, performanţa SMM cu costuri liniare de
C
comunicaţie se degradează datorită costurilor comunicaţiilor interprocesor.

Observație: Toate aceste modele sunt, totuși, simpliste, generând rezultate


discutabile, chiar şi contradictorii. Complexitatea procesării SMM este prea
mare pentru abordări analitice de referinţă. Pentru o adecvată evaluare și
optimizare se impun şi aici simulări / optimizări complexe, pe benchmark-
uri concurente specifice.

Aplicații specifice pentru sistemele de calcul paralel

376
Unele dintre cele mai specifice aplicații pentru sistemele paralele de calcul
sunt cele stiințifice, numerice. Acestea se caracterizează prin numeroase calcule
numerice, având un grad semnificativ de concurență. Printre aplicațiile
particulare care se pretează la procesări paralele, amintim aici probleme
referitoare la științele computaționale sau la științele inginerești, precum:
modelarea schimbărilor în climatul global, evoluția galaxiilor, structura atomică
a materialelor, modelarea proceselor de combustie în motoarele termice,
decodificarea genomului uman, circulația apelor în oceane, calculul dinamicii
fluidelor, modelarea super-conductivității, aplicații multimedia, criptografie,
procesare de semnale, de voce etc. Printre benchmarkurile de calcul paralel cele
mai cunoscute și utilizate putem aminti LINPACK benchmarks, extrem de
folosite pentru evaluarea aplicațiilor numerice (Millions of Floating Point
Operations Per Second, MFLOPS), PARSEC, SPLASH 2 etc.
Există însă concurență semnificativă și în aplicații de tip comercial.
Performanțele sistemelor de calcul paralel se referă și la capacitatea acestora de
a procesa tranzacții on-line (OLTP - On-Line Transaction Processing)
Benchmarkurile specifice măsoară numărul mediu detranzacții efectuate per
minut (tpm) pentru o aplicație specifică.
Modelul de programare (programming model - PM) reprezintă
conceptualizarea semantică a mașinii virtuale pe care programatorul o utilizează
în codarea aplicațiilor (concurente în cazul acestui paragraf). În particular, un
PM concurent specifică modul în care firele de execuție (părți ale programului)
rulează în paralel, respectiv comunică între ele și se sincronizează, pentru a
rezulta o coordonare globală eficientă a întregii procesări. Comunicațiile
interprocesoare se fac atunci când data scrisă de un procesor este citită de către
un altul. Așadar un PM concurent reprezintă o intefață între aplicația concurentă
dezvoltată de către programator și implementarea concretă a acesteia pe
arhitectura paralelă hardware. În mod uzual, modelul de programare este
independent de numărul de procesoare al sistemului, de modul de interconectare
între acestea și de alte detalii ale implementării hardware a sistemului paralel.
În continuare, vom prezenta succint două modele de programare nativ
concurente sau două categorii de mașini paralele de tip multicore.
Modelul de SMM cu memorie globală partajată sau cu adrese de
memorie partajate (Shared address) – se bazează pe o memorie logică globală,

377
posibil a fi partajată de către toate procesoarele (thread-urile) componente. O
anumită locație din această memorie are aceeași adresă și același înțeles pentru
toate procesoarele (firele concurente). Activitățile individuale pe fiecare
procesor pot fi coordonate bazat pe planificarea firelor de execuție. Scopul
general este acela de a permite diferitelor procesoare din sistem să execute în
paralel diferite părți ale programului, numite fire de execuție, pentru a-l procesa
mai repede. Așadar procesoarele (nucleele) componente vor coopera, în vederea
paralelizării programului concurent. Cooperarea și schimbul de informații între
procesoare, în acest model multiprocesor cu memorie globală partajată, se face
în mod implicit, prin intermediul unor instrucțiuni de citire / scriere din/în
memoria partajată, dar și prin utilizarea unor sincronizări, în vederea controlului
acceselor la variabilele partajate.
Cheia sincronizării proceselor în SMM este data de implementarea unor
aşa-zise procese atomice (în sensul în care un astfel de proces nu poate fi divizat
în sub-procese componente, la nivelul unei cereri de bus). Așadar un proces
atomic (secțiune critică de program – critical section) reprezintă un proces
software care, odată iniţiat, nu mai poate fi întrerupt sau accesat de către niciun
alt proces. De asemenea, o astfel de secțiune critică nu poate fi procesată, la un
anumit moment dat, decât de un singur proces. Spre exemplu, să considerăm că
pe un SMM se execută o aplicaţie care calculează o sumă globală, prin nişte
sume locale calculate anterior pe fiecare procesor, ca mai jos:

LocSum=0;
for i=1 to Max
LocSum=LocSum+LocTable[i]; secvenţa executată în paralel de
; către fiecare procesor!
Proces LOCK
atomic GlobSum=GlobSum+LocSum; secțiune critică, Read-Modify-
Write
UNLOCK

Procesul LOCK/UNLOCK este atomic, în sensul că numai un anumit


procesor poate executa acest proces (thread) la un moment dat (“Make updates
to a location appear atomic”), neputând fi întrerupt de un altul, care să

378
proceseze aceeași secvență critică. În caz contrar, s-ar putea obţine rezultate
hazardate pentru variabila globală "GlobSum". Astfel, spre exemplu, P1 citeşte
GlobSum = X, P2 la fel, apoi P1 scrie GlobSum = GlobSum + LocSum1, iar
apoi P2 va scrie GlobSum = GlobSum + LocSum2 = X + LocSum2 (incorect!)
Așadar, în loc ca variabila finală să aibă valoarea GlobSum = X + LocSum1 +
LocSum2, aceasta va avea valoare finală GlobSum = X + LocSum2. Este deci
necesar ca și structura hardware să poată asigura atomizarea unui proces
software, adică să implementeze directivele LOCK/UNLOCK (numite pragmas
în limbajul Open MP). O soluţie constă în implementarea în cadrul µprocesoarelor
actuale a unor instrucţiuni atomice de tip "Read - Modify - Write" (pe variabile
partajate), neinteruptibile la nivelul unei cereri de bus BUSREQ (venite din partea
unui CPU sau DMA).
Cu precizarea că o variabilă globală rezidentă în memoria comună
("GlobSum") deţine un octet "Semafor" asociat, care indică dacă aceasta este sau
nu este ocupată, se prezintă un exemplu de implementare a procesului atomic
precedent (LOCK UNLOCK), pe un SMM cu µprocesoare (compatibile ISA cu
arhitectura) INTEL - x86:

MOV AL,01
WAIT:LOCK XCHG AL, <semafor>; instrucţiune atomică "Read -
; Modify - Write"
TEST 01,AL ; resursa “GlobSum” este liberă?
JNZ WAIT ; dacă nu, repetă
MOV AX, <GlobSum> ; AX ← (GlobSum)
ADD AX,BX ; GlobSum = (GlobSum)+(LocSum)
MOV <GlobSum>, AX ; AX → GlobSum
XOR AL,AL; AL ≡ 0
MOV <semafor>, AL ; eliberare resursă

Observație: S-a presupus că fiecare proces local a depus rezultatul "LocSum" în


registrul BX al fiecărui procesor.
Așadar, o secțiune critică de cod (critical section) desemneaza o secţiune
de cod care poate fi controlată (procesată) la un moment dat, numai de către un
singur proces. În Figura 4.42 se prezintă un sistem multiprocesor simetric, de tip
UMA (Uniform Memory Access), cu memorie comună partajată, centralizată (și

379
din punct de vedere) fizic. Se observă că rețeaua de interconectare (RIC) precum
și memoria comună reprezintă potențiale “gâtuiri” ale performanței
(bottlenecks). Aceste sisteme multiprocesor se mai numesc și sisteme strâns
cuplate (în jurul memoriei comune). Se consideră că accesul oricărui procesor la
memoria comună este cvasi-egal din punct de vedere al latenței de acces (costul
de acces este practic același pentru orice procesor). Aceste sisteme
multiprocesor de tip UMA dețin, în general, câte un cache de nivel 1 pentru
fiecare CPU și un cache de nivel 2 global (acesta din urmă este uneori împărțit
în module – bank-uri fizice diferite, pentru mărirea lărgimii de bandă la
accesarea lor). În Figura 4.43 se prezintă un sistem multiprocesor simetric, de tip
UMA (Uniform Memory Access), cu memorie comună partajată, distribuită însă
din punct de vedere fizic (mai multe module fizice). Această distribuire fizică
are avantajul de a permite accese multiple, simultane, ale procesoarelor, la
modulele fizice de memorie comună (desigur, numai dacă și RIC are o lărgime
de bandă suficient de mare). În Figura 4.51 se prezintă un sistem multiprocesor
nesimetric, de tip NUMA (Non Uniform Memory Access), cu memorie comună
centralizată și partajată din punct de vedere logic, dar distribuită sub forma mai
multor memorii fizice (distributed shared-memory architecture). Această
distribuire s-a făcut ca să existe șansa, pentru un anumit procesor, să adreseze
memoria mai apropiată („locală”), care nu necesită accesarea RIC. Evident că o
asemenea accesare “norocoasă”, ajutată de localitatea favorabilă a datelor (prin
program și prin compilator), se face cu o latență mai mică decât accesarea unui
modul fizic de memorie mai îndepărtat, care necesită și accesarea RIC, printr-o
arbitrare prealabilă. Este sarcina controlerului local de memorie, să determine pe
baza adresei emise de către procesorul său, dacă adresa aceasta necesită, sau nu,
ocuparea RIC. Așadar, timpul de acces al procesoarelor la memoria comună este
unul neuniform (nu este același). Desigur, pentru ca o arhitectură SMM de tip
NUMA să fie utilizată corespunzător, este necesară exploatarea localității
spațiale a datelor aplicației procesate, în special de către compilator (dar și
programatorul poate facilita acest fapt, printr-o scriere corespunzătoare a
programului.)
Un alt model de sistem paralel de calcul este cel compus din
calculatoare distribuite (practic, rețele de calculatoare), care comunică prin
transmiterea / recepționarea de mesaje (message passing), în mod explicit, prin

380
funcții specifice de tip send/receive. Un mesaj conține câmpurile: sursă,
destinație, tip mesaj, datele (conținut mesaj), informație control mesaj (CRC).
Aceste evenimente coordonează practic întreaga activitate de procesare paralelă
a firelor de execuție pe aceste sisteme paralele, numite și slab cuplate. Aceste
sisteme multicore speciale sunt implementate și pe un singur cip, sub denumirea
de Network on Chip (NoC). Ele au avantajul, față de precedentele (cu memorie
comună), de a fi mai scalabile din punct de vedere hardware. În schimb,
sincronizarea explicită, prin transmiterea recepția de mesaje, complică
(îngreunează) programarea lor, făcând-o mai puțin productivă. Reamintim că la
sistemele multicore strâns cuplate sincronizarea era implicită, prin scrieri/citiri
ale variabilelor globale partajate de către fiecare thread (procesor). Aici,
memoria este doar locală pentru fiecare procesor. În consecință, o aceeași adresă
virtuală este localizată diferit pentru procesoare diferite, variabila pointată de
această adresă având înțelesuri diferite pentru calculatoarele componente. O
schemă a acestui tip de sistem multicore este prezentată în Figura 4.51 (sic!).
Această figură poate semnifica atât un sistem multiprocesor de tip NUMA, după
cum am mai arătat, dar și unul distribuit, de tip message passing (rețea de
calculatoare).
Așadar, nodurile sunt calculatoare independente în acest model.
Comunicațiile între procesoare se fac explicit, prin primitive de tip send/receive,
care transmit respectiv recepționează mesaje. Comunicațiile sunt integrate în
nivelul de I/O, ca în rețelele de calculatoare.
Funcția Send specifică un buffer de memorie locală, din care se citesc
datele de transmis, un proces receptor (situat pe un alt calculator-proces din
sistem) și un tag, prin care se identifică transmițătorul. Funcția Receive specifică
procesul emițător, un buffer de memorie locală, unde se vor memora datele
recepționate precum și un algoritm de verificare a tagului recepționat.
Programele paralele bazate pe arhitecturi de tip message passing sunt
structurate. În mod frecvent, toate nodurile procesează copii identice ale
programului, având același cod și variabile private (SPMD – Single Program
Multiple Data). Modelul message passing operează pe un set de procese (fire de
control), fiecare având un spațiu privat de memorie. De asemenea, fiecare proces
poate să comunice cu alte procese, în mod explicit. Funcțiile send/receive
operează asupra spațiului local de adrese, dar și asupra spațiului global de adrese

381
al procesului. Fiecare pereche send/receive specifică o operație de sincronizare
specifică, de tip punct la punct (point-to-point).
Topologiile rețelelor de interconectare sunt de tip inel, grid (2D, 3D) –
inclusiv toroidale, hipercub, rețele cu conectare totală (fully connected) etc.
Ne dorim un SMM de tip shared memory, pentru faptul că procesele de
comunicație sunt implicite, naturale, ceea ce facilitează programarea concurentă,
dar, pe de altă parte, ne dorim și un sistem multiprocesor de tip message
passing, datorită scalabilității sale hardware.
În continuare vom analiza mai detaliat sistemele multicore cu memorie
partajată din punct de vedere logic.
În acest caz, toate procesoarele partajează un singur spațiu logic global de
adrese de memorie. Proprietatea esențială a SMM cu memorie partajată constă
în faptul că procesoarele comunică între ele în mod implicit, prin instrucțiuni
Load/Store convenționale, pentru a accesa variabilele partajate din acest spațiu
comun de memorie. Fiecare procesor poate accesa, din punct de vedere teoretic,
orice locație fizică de memorie. Sincronizările se implementează prin hardware
(LOCK/UNLOCK) sau prin software, cu ajutorul semafoarelor.
Un proces (task) reprezintă o instanță a unei aplicații active (secvența
dinamică de execuție a unui program static) având un spațiu virtual de memorie
și unul sau mai multe fire de control (threads), care partajează zone al acestui
spațiu virtual. Rezultă deci că o anumită locație de memorie virtuală partajată de
mai multe threaduri este mapată la o aceeași adresă fizică. Evident că sistemul
de operare alocă fiecărui task spațiu de memorie (cod, date, stivă) dar și resurse
de CPU, pentru rulare. În mod uzual, variabilele partajate au aceeași adresă de
memorie și același înțeles pentru toate thread-urile. Segmentul de memorie
privată a unui thread conține în mod tipic zone de cod, stivă și date.
Thread-urile reprezintă o modalitate explicită prin care un program se
împarte pe sine în două sau mai multe sub-taskuri concurente, care se pot deci
procesa în paralel. Ele sunt naturale numai în paradigma programării concurente.
Taskurile (procesele) din cadrul unui sistem de operare cu procesări multitasking
sunt în mod normal independente, având fiecare informații de stare, spații de
date separate (ca și cele de cod și de stivă de altfel). Acestea interacționează
numai prin intermediul unor mecanisme de comunicații inter-procese, puse la
dispoziție de către sistemul de operare. Pe de altă parte, thread-urile multiple de

382
execuție partajează starea procesului, zona de date a acestuia precum și alte
resurse, în mod direct. Ele pot interacționa prin intermediul zonei de date
partajate. Această interacțiune este controlată prin operații specifice de
sincronizare (spre exemplu, prin secțiuni critice, după cum deja am arătat.) Din
punct de vedere al sistemului de operare, un thread este secvențial (nu există
paralelism în cadrul acestuia) și atomic, în sensul că este cea mai mică entitate
căreia acesta îi acordă resurse. Totuși, din punct de vedere al procesării
hardware, un thread nu este neapărat secvențial, pentru că procesorul sau
compilatorul pot exploata paralelismul la nivel de instrucțiuni din cadrul
acestuia, după cum am explicat în detalii în această lucrare. Cooperarea și
coordonarea pe timpul rulării firelor de execuție multiple se face prin citirea /
scrierea variabilelor partajate și a pointerilor care referă adrese partajate.

Figura 4.41. Partiționarea spațiului de date comun de către firele de execuție

În Figura 4.41 se prezintă modul de partiționare a spațiului de date comun


de către firele de execuție. Acest model de programare cu memorie partajată
asumă faptul că unul sau mai multe thread-uri partajează o zonă comună de date.
Desigur, acestea pot avea și zone private de date pentru variabilele locale.
Așadar, esenţa principială a thread-ului este aceea că execută o procedură,
în cadrul aceluiaşi proces, în paralel cu alte thread-uri. Contextul şi zonele de

383
date ale procesului sunt utilizate în comun de către toate thread-urile
componente. Contextul procesului conţine informaţiile de localizare în memoria
internă şi informaţiile de stare aferente execuţiei sale. Singurul spaţiu de
memorie ocupat exclusiv de către un thread este spaţiul de stivă. Fiecare thread
îşi întreţine propriul context, cu elemente comune contextului procesului părinte
al thread-ului.
Mai jos, se prezintă două scheme tipice pentru un SMM cu memorie
globală partajată. Evident că această memorie poate fi distribuită din punct de
vedere fizic, astfel încât să conțină mai multe module fizice, în vederea
exploatării unor accese concurente ale procesoarelor la memoria partajată, după
cum am arătat.

Figura 4.42. Sistem multiprocesor UMA (simetric) cu memorie comună


partajată centralizată fizic

384
Figura 4.43. Sistem multiprocesor UMA cu memorie comună partajată
distribuită fizic (MFk=modul fizic memorie)

Rețele de interconectare (Network on Chip - NoC)


“Computer power increases by the square of the number of nodes on the
network.” (Legea lui Robert Metcalf)

Atât sistemele multiprocesor strâns cuplate, cât și cele slab cuplate, dețin
o rețea de interconectare, prin intermediul căreia pot accesa memoria partajată
respectiv pot comunica prin mesaje explicite. În cazul celor strâns cuplate, orice
procesor poate accesa orice locație de memorie. Având în vedere legea empirică
a lui Gordon Moore în dezvoltarea microprocesoarelor, procesoarele (deseori
neomogene, de tip DSP, multimedia, ASP etc.) se interconectează pe același cip,
prin intermediul unor rețele de inteconectare. Aceste sisteme multicore speciale
sunt numite Network on Chip (NoC). În mod uzual aceste NoC-uri sunt
heterogene (prin procesoarele interconectate) și interconectează nuclee de
procesoare, spre deosebire de rețelele de calculatoare care interconectează
calculatoare de tip PC, tablete sau telefoane mobile, fiind deci oarecum mai
omogene.

385
Sarcina principală a NoC este de a transfera informația de la orice nod
sursă, la orice nod destinație. Performanța înseamnă latență mică și lărgime de
bandă mare (posibilitatea mai multor transferuri concurente prin rețea).
Un NoC se compune din link-uri (magistrale, legături între procesoare,
prin cabluri sau wireless) și switch-uri. Un link reprezintă, în esență, o legătură
de fire conductoare sau fibră optică care transportă semnale analogice [Cul99].
Desigur că sunt necesare conversii analog-numerice și numeric-analogice pentru
transmiterea și recepția datelor din/în procesoarele numerice implicate. Fibrele
optice transmit datele numerice sub forma unor impulsuri de lumină (LED,
LasEr Diode). O conexiune de tip full duplex (emisie/recepție simultane)
necesită două astfel de fibre optice. Diametrul cablului este limitat la lungimea
de undă a semnalului luminos. Fibrele optice necesită conversii optic-electric și
reciproc. Transmițătorul, link-ul și receptorul formează un așa numit canal de
comunicație (communication channel). Protocolul (algoritmul) de emisie –
recepție la nivelul magistralei de legătură (link) segmentează șirul de simboluri
care reprezintă mesajul respectiv și care traversează canalul de comunicație, în
unități logice, numite pachete, deosebit de utile în implementarea algoritmilor de
rutare prin rețea. Așadar, mesajul codificat sub forma unei succesiuni de pachete
reprezintă informația trimisă între procesoare, prin intermediul unei rețele de
interconectare (RIC) sau a unei NoC. Un switch (comutator de rețea) se
compune dintr-un set de porturi de intrare, un set de porturi de ieșire, o rețea
internă de interconectare cu intrări-ieșiri (crossbar), buffere interne de tip FIFO
pentru memorarea temporară a pachetelor și o logică de control, în vederea
stabilirii unei conexiuni intrare-ieșire, de fiecare dată când este nevoie.
Figura următoare prezintă o arhitectură generică de tipul de Network-on-
Chip. Suprafața cipului este împărțită în secțiuni regulate. Fiecare astfel de
secțiune conține un procesor (core, nucleu) și un ruter de rețea. Procesoarele
sunt în general heterogene. Astfel, pot exista procesoare superscalare/SMT de uz
general (CPU 1, 2, 3, 4), procesoare dedicate de tip ASIC (Application Specific
Integrated Circuits), procesoare de tip Digital Signal Processors, module de
memorie și I/O etc.

386
rout er

CPU 1 buffer
CPU 3
DSP 1

NA
IP core link
CPU 4
CPU 2
ASIC 2

ASIC 1 I/O DSP 2

Figura 4.43.b. Sistem de tip NoC

Graful numit Application Characterization Graph (APCG) arată modul de


mapare a firelor unei aplicații concurente, pe procesoarele heterogene din NoC.
Arcele semnifică faptul că aceste fire comunică între ele și că sunt dependente
(entitatea indicată de săgeată este dependentă în procesare, de cea anterioară.)
Firele de control aferente grafului APCG vor trebui apoi mapate optimal pe
nodurile NoC. Optimalitatea se referă la o performanță maximă, consum de
energie electrică minim etc. Această problemă a mapării este una de
complexitate mare, factorială, necesitând în general algoritmi euristici. Desigur
că această mapare este dependentă de algoritmul de rutare utilizat [Rad11].

387
Figura 4.43.c. Problema mapării aplicației pe NoC

Rețelele de comunicație se clasifică în rețele directe respectiv rețele


indirecte. Într-o rețea directă fiecare nod are două funcții principale: procesare
de date respectiv transmisie/recepție de date. Într-o rețea indirectă fiecare nod
are doar o singură funcție principală, anume aceea de procesare de date sau
(exclusiv!) transmisie/recepție de date.
In vederea trimiterii / recepționării de mesaje între două calculatoare este
necesar un așa numit protocol de transfer. Spre exemplu, următorii pași esențiali
ar fi necesari pentru a trimite un mesaj:

• Copiază data din memoria procesorului (nodului) într-un buffer dedicat de


transmisie, al sistemului de operare (SO).
• Sistemul de operare calculează o sumă de control a datelor utile (spre
exemplu un cod detector și corector de erori de tip CRC) pe care o include
în coada din formatul mesajului / pachetului (tail). După aceasta, SO
startează un timer de time-out (pentru a evita un transfer indefinit de
lung).
• Sistemul de operare trimite mesajul prin intermediul unor componente
hardware-software dedicate.

388
În vederea recepției unui mesaj sunt necesari următorii pași importanți:

• Sistemul de operare copiază mesajul din interfața rețelei, într-un buffer


FIFO de recepție, special dedicat. Dacă acest buffer este cumva ocupat,
receptorul va trebui să-i transmită emițătorului un mesaj corespunzător.
• Suma de control recepționată este comparată cu cea calculată, pe baza
datelor utile recepționate. Dacă această comparație a avut succes,
receptorul notifică acest fapt emițătorului, printr-un mesaj special de
recunoaștere (acknowledge). În consecință la acest mesaj special,
emițătorul va reseta timerul de time-out. În caz contrar (sumele de control
nu coincid), receptorul șterge mesajul primit, urmând ca emițătorul să-l
retrimită, imediat după ce timerul de time-out s-a resetat.
• Dacă pasul 2 se termină cu succes, sistemul de operare transferă mesajul
în memorie.

Headerul mesajului (pachetului) poate conține numărul de secvență al


pachetului în vederea posibilității unei asamblări corecte a mesajului format din
mai multe pachete (acestea pot ajunge la destinație out of order), informații de
rutare etc. Câteva dintre cele mai importante caracteristici ale unei rețele de
interconectare sunt următoarele:

• Întârzierea - Latency, adică timpul de transmitere pentru un singur cuvânt


(pachet, mesaj)
• Lărgimea de bandă (bandwidth, throughput), adică ce trafic de mesaje
poate suporta reţeaua în unitatea de timp. Cu alte cuvinte, această lărgime
de bandă reprezintă rata maximă de propagare a datelor prin rețea și se
măsoară în numărul de biți / secundă.
• Sender overhead (SO) – reprezintă timpul necesar transferului unui mesaj
din memoria CPU la bufferul FIFO al transmițătorului (înainte de
startarea transferului propriu-zis, v. TF mai jos).
• Time of flight (TF) – timpul de transmisie necesar pentru ca primul bit al
mesajului să ajungă la receptor.

389
• Transmission time (TT) – durata de transmisie reprezintă perioada de timp
din momentul în care primul bit al mesajului a ajuns la receptor, până în
momentul în care ultimul bit al mesajului a ajuns la receptor.
• Receiver overhead (RO) – reprezintă timpul necesar pentru ca mesajul
recepționat să fie transferat din bufferul FIFO al receptorului până în
memoria CPU.
• Așadar, latența totală a rețelei este = SO+TF+TT+RO
• Gradul de conectivitate, adică numărul de vecini direcţi pentru fiecare nod
• Costul hardware, adică ce fracţie din costul total al hardului reprezintă
costul RIC
• Fiabilitatea şi funcţionalitatea (arbitrare, întreruperi etc.)

O rețea de interconectare sau o structură de tip NoC este caracterizată de


următoarele aspecte importante:

• Topologia rețelei, care este determinată de structura fizică a rețelei de


interconectare, adică de graful corespunzător acesteia, care poate fi
regulat sau neregulat.

• Algoritmul de rutare – acesta determină ruta (calea) pe care mesajul o


folosește pentru traversarea rețelei (inclusiv NoC), de la sursă, la
destinație. Un algoritm de rutare este ne-adaptiv, dacă ruta utilizată de
mesaj este determinată doar de sursă și destinație și nicidecum de traficul
prin rețea; așadar, în acest caz, pachetul va urma o anumită cale
prestabilită, indiferent dacă un link de pe această cale este blocat. Rutele
minimale pot supra-încărca rețeaua în mod frecvent, generând blocaje
(exact ca în traficul auto). În consecință, s-au dezvoltat algoritmi adaptivi
de rutare, tocmai pentru a contracara asemenea dezavantaje. Acești
algoritmi permit ca ruta aleasă pentru un anumit pachet să depindă de
traficul pe care acesta îl întâlnește pe parcursul rutării. Totuși, chiar și in
acest caz pot apărea congestii care să conducă la anumite blocaje. Un
astfel de blocaj este cunoscut în literatura de specialitate sub denumirea de
deadlock și poate apărea atunci când un pachet așteaptă după un

390
eveniment care nu poate să apară. Așadar, într-un asemenea caz, pachetul
respectiv nu mai poate face progrese în vederea înaintării sale spre
destinație (spre exemplu, bufferele de emisie / recepție sunt pline și
fiecare dintre ele așteaptă după celălalt, ca să devină disponibile.) Uneori,
poate să apară un tip de deadlock numit indefinite postponement, cu
semnificația că un pachet de date așteaptă după un eveniment care, deși ar
putea să apară din punct de vedere teoretic, totuși, în contextul dat, nu va
apărea niciodată. Un alt blocaj tipic se numește livelock și poate să apară
atunci când un pachet nu va ajunge niciodată la destinație prin intermediul
algoritmului de rutare implementat. Așadar deadlock livelock, dar
reciproca nu este valabilă întotdeauna. O posibilă soluție pentru reducerea
congestiilor în rețea ar consta în interzicerea intrării unor noi pachete în
rețea până când traficul nu se reduce.

• Strategia de switching determină modul în care un mesaj ajuns într-un


switch (comutator de rețea) este trimis la o ieșire din cadrul acestuia.
Evident că această strategie de switching face parte din strategia mai
generală de rutare a pachetului (mesajului). Scopul principal al acestei
strategii este îmbunătățirea lărgimii de bandă a rețelei. Ea permite mai
multor perechi de noduri sursă-destinație să comunice simultan. În
comutarea de circuite (circuit switching), calea dintre sursă și destinație
este stabilită și rezervată până când mesajul este transferat prin circuit,
ceea ce este neoptimal. Ideea a venit din vechile centrale telefonice, care
funcționau pe acest principiu. O soluție alternativă, de o mai largă
utilizare, constă în comutarea de pachete (packet switching), în care
mesajul este împărțit într-o secvență de pachete de date. Un pachet
conține, pe lângă datele propriu zise, un header (care la rându-i conține
informații precum sursă, destinație, informație de secvențiere și rutare,
informație de control etc.) și un așa numit trailer (coduri detectoare și
corectoare de erori). Comutarea de pachete permite o mai bună utilizare a
resurselor rețelei (comparativ cu comutarea de circuite), datorită faptului
că link-urile și bufferele vor fi ocupate exclusiv pe durata de timp în care
sunt traversate de pachetul de date. În așa numitele rutări bazate pe sursă,
mesajul conține în mod static calea de urmat. În cazul alternativ al

391
rutărilor bazate pe destinație, headerul mesajului conține adresa
destinației, după cum am mai menționat. În acest caz, fiecare switch
trebuie să comute pachetul la o ieșire a sa, astfel încât să ajungă
finalmente la destinație. Este deci necesară existența unei tabele de rutare
pentru fiecare pachet, în vederea deciziei de rutare a acestuia. Există două
strategii principale de switching: store-and-forward respectiv cut-through
routing numită și wormhole routing [Cul99]. În cazul strategiei store-and-
forward switchul așteaptă să recepționeze întregul pachet (mesaj), înainte
de a-l trimite mai departe la următorul switch de pe rută. Strategia cut-
through routing este una alternativă, prin care mesajul este pipeline-izat
prin intermediul secvenței de switch-uri aparținând căii de rutare stabilite.
Prin urmare, strategia cut-through obține latențe mai mici, prin alegerea
automată a căii urmate de primul flit (FLow control digITs), numit și head
flit (flit=unitate date/control care divide un pachet), din cadrul pachetului
(mesajului) care este divizat în unități mai mici, numite flits. Ultimul flit
din pachet (mesaj) va dezactiva calea de rutare (canalul de comunicație)
rezervată. De asemenea, această strategie reduce necesarul de buffere din
fiecare nod, comparativ cu strategia store-and-forward. Ca dezavantaj,
crește probabilitatea statistică de blocare, în special în condiții de trafic
mare.

• Mecanismul de control al fluxului de date determină momentul în care


pachetul se transmite efectiv pe linkul din cadrul rutei sale. Este necesar
ori de câte ori două sau mai multe mesaje doresc să utilizeze aceeași
resursă a rețelei, în mod simultan (un analog al hazardurilor structurale
din cadrul structurilor pipeline). În acest caz, unele mesaje vor fi ținute în
buffere sau deturnate pe alte rute alternative. O coliziune este detectată
atunci când două sau mai multe noduri trimit mesaje pe același link, în
mod simultan. Interfețele rețelei pot detecta orice posibilă coliziune prin
“ascultarea” canalului (carrier sensing – reprezentând de fapt un protocol
probabilistic de acces la mediu – Media Access Control, prin care se
verifică mai întâi absența traficului prin lipsa semnalului purtător, iar apoi,
în caz pozitiv, se startează transmisia pe canalul de comunicație). În acest
caz defavorabil, fiecare nod implicat așteaptă un timp aleator, înainte de a

392
încerca o retrimitere a mesajului. O abordare alternativă constă în pasarea
unui așa numit jeton (token), care să circule între noduri. Un nod poate
transmite un mesaj numai când a recepționat jetonul (v. în continuare
pentru detalii).

Modelul de trafic într-o rețea caracterizează modul de variație al traficului


în timp. Deseori, interesează numărul de evenimente care ar putea să apară în
unitatea de timp într-o rețea de comunicație. Dacă notăm cu r numărul mediu de
apariții ale unui eveniment în intervalul de timp dat, atunci probabilitatea ca să
apară un număr mediu de x evenimente observabile în intervalul de timp este
dată de formula lui Poisson, care exprimă de fapt probabilitatea unei variabile
aleatoare de tip Poisson (X), astfel [Wol07]:
r x e −r
P( X = x) = , x=0,1,2...
( x)!
Structura rețelelor de interconectare poate varia de la o simplă magistrală
(care permite un singur master la un moment dat), la rețele sofisticate, care
permit transferuri simultane de mesaje între perechi de noduri. Dintre multiplele
topologii de rețele amintim doar câteva, mai utilizate:

• Switch de tip Crossbar – permite o lărgime de bandă ridicată, dar din


păcate, nu este scalabilă. Costul, din păcate, crește cu pătratul numărului
de porturi (v. în continuare).

• Interconexiuni cu stagii multiple (Multistage interconnections) – costurile


cresc mai puțin agresiv cu numărul de porturi decât în cazul precedent. Pe
de altă parte, lărgimea de bandă este mai mică, iar latența este mai mare
decât în cazul crossbar.

• Arbore “gras” (Fat tree) – reprezintă un arbore de comunicație cu o


lărgime de bandă mai mare în nivelele superioare, astfel încât lărgimea de
bandă între fiecare două nivele (adiacente) din arbore să fie constantă
(într-un arbore normal, lărgimea de bandă se reduce odată cu avansarea
spre nivelele superioare – cele mai apropiate de radăcină.)

393
• Rețele cu interconectare totală (oricare două noduri pot comunica direct,
2
implicând deci C N legături), inele, rețele liniare, hyper-cuburi, plase 2D
sau 3D (mesh), inclusiv toroidale etc.

• Conexiune de tip bus (magistrală unică) – permite un singur master activ


la un moment dat. Evident că are cea mai mică lărgime de bandă, în
comparație cu topologiile precedente, dar costă mai puțin. Din păcate nu
este scalabilă, saturându-se, după anumite studii, la un număr maxim de
32 de procesoare conectate la bus.

Figura 4.44. Topologii derețele de interconectare

În continuare vom detalia câteva dintre aceste topologii de rețele.

1. SMM pe bus comun (RIC statică)

Caracterizată de faptul că RIC este un simplu bus comun partajat în timp de


către µprocesoare (magistrală comună de acces). Este cea mai simplă rețea de
interconectare a procesoarelor, dar conflictele potenţiale ale procesoarelor
(masterilor) pe busul comun, pot fi multiple. Desigur, există un singur master
activ pe bus la un moment dat, rezultat în urma unui proces de arbitrare la

394
nivelul cererilor de bus. Busul comun şi memoria globală (slave) sunt partajate
în timp de către masteri, pe parcursul procesării. Resursele locale ale masterilor -
memorii cache și memorii locale, dar și porturi de I/O - au rolul de a diminua
traficul pe busul comun și de a permite procesarea paralelă de către masteri.
Accesul pe bus se face prin intermediul unui arbitru de priorităţi, centralizat sau
distribuit.
Arhitectura implică dificultăţi tehnologice legate de numărul maxim de
masteri cuplabili (în practică până la 32), reflexii şi diafonii ale semnalelor pe
bus. Cum capacităţile şi inductanţele parazite cresc proporţional cu lungimea
busului, rezultă că acesta trebuie să fie relativ scurt.
Există arhitecturi standardizate de SMM pe bus comun (VME – dezvoltat
de compania Motorola pentru familia de µp MC680X0, MULTIBUS –
compania Intel pentru procesoare I - 80X86) și altele mai noi (ex. CAN
automotive sau, ceva mai recent, FlexRay [Wol07]), mai ales în lumea
sistemelor dedicate (incorporate).

2. SMM în inel (token – ring) – reţea statică

Figura 4.45. Reţea de tipul Token-Ring

Arhitectura este standardizată conform standardelor IEEE 802.5. Este


utilizată cu precădere în sistemele slab cuplate (reţele locale). Protocolul de
comunicaţie are la bază trimiterea unei informaţii binare speciale, numită jeton
(token), de la un procesor la celălalt, în mod secvenţial. Un procesor Pi, nu poate
să trimită un mesaj decât dacă a recepţionat jetonul. Dacă un procesor doreşte să
trimită un mesaj la un alt procesor, va aştepta recepţionarea jetonului, apoi va
modifica un bit din jeton, iar după această modificare va transmite mesajul cu

395
jetonul modificat pe post de antet. Din acest moment, nu mai circulă jeton prin
structură şi deci toate emisiile de mesaje sunt inhibate. După ce procesorul
destinaţie a recepţionat mesajul (date sau comenzi), îl va trimite mai departe,
spre procesorul sursă. Conform standardului, procesorul sursă va insera din nou
jetonul în structură în momentul în care şi-a terminat de transmis mesajul şi a
recepţionat “începutul” propriului mesaj emis.
Rezultă de aici că eficienţa scade proporţional cu numărul procesoarelor
din reţea.

3. SMM cu interconectare “crossbar” (reţea dinamică)

Această arhitectură deţine complexitatea cea mai ridicată dintre rețelele de


interconectare utilizate în SMM, în schimb conflictele procesoarelor la resursele
de memorie comună partajată sunt minime. Comunicaţia între orice pereche
procesor – memorie este întârziată în nodul de conexiune aferent. De remarcat
că pot avea loc simultan până la N accese ale procesoarelor la memorie, în
ipoteza în care nu există două procesoare care să acceseze acelaşi modul fizic de
memorie comună (v. Figura 4.46). Practic se implementează N! funcţii bijective
de comunicaţie, definite pe mulţimea celor N procesoare, cu valori în mulţimea
celor N module fizice de memorie. Uneori însă, chiar și în acest caz favorabil,
pot exista conflicte, prin faptul că mai multe procesoare doresc să adreseze
simultan același modul de memorie. Pentru evitarea conflictelor de acest gen, se
încearcă atunci când este posibil, “împrăştieri” favorabile ale informaţiei în
modulele fizice de memorie globală. Spre exemplu, în aplicaţiile pe vectori,
dacă aceştia au pasul 1, atunci scalarii succesivi sunt situaţi în blocuri succesive
de memorie, rezultând minimizări ale conflictelor. Cade în principal în sarcina
compilatorului să exploateze în mod favorabil localitatea spațială a datelor
aplicației, ținând cont de caracteristicile acestei rețele de interconectare cu o
lărgime de bandă ridicată.

396
Figura 4.46. Reţea de interconectare de tipul crossbar

Deşi cea mai performantă, arhitectura devine practic greu realizabilă pentru
un număr N relativ mare de procesoare, din cauza costurilor ridicate (sunt
necesare N2 comutatoare de comunicație).

4. SMM cu reţele de interconectare dinamice multinivel

Reprezintă un compromis performanță / cost între SMM cu rețele de


interconectare de tip unibus şi cele de tip crossbar. Elementul principal al RIC îl
constituie în acest caz comutatorul complex (complex switch), care este compus
din 4 switch-uri elementare, ca în Figura 4.47. În general sunt folosite
comutatoare cu două intrări şi două ieşiri. Aceste comutatoare pot lucra “direct”
sau în “cruce”, adică (A→C, B→D) respectiv (A→D, B→C), prin acțiunea
corespunzătoare a celor 4 switchuri componente.

Figura 4.47. Comutator de reţea

397
Se prezintă mai jos, ca exemplu, un SMM având o reţea de interconectare
pe trei niveluri, într-o topologie numită BASELINE, cu opt procesoare (P0, P7) şi
opt module fizice de memorie (M0, M7).

Figura 4.48. Interconectare Baseline

Cu precizarea că Ckj = 0 semnifică faptul că switch-ul Ckj lucrează în


“linie”, iar Ckj = 1 faptul că acesta lucrează în “cruce”, k={1,2,3} (coloane) iar
j={1,2,3,4}(rânduri), în continuare se prezintă grafurile de comunicație “totală”
procesoare – memorii pentru reţeaua BASELINE.

398
Figura 4.49. Grafurile de comunicaţie pentru rețeaua Baseline

399
Exemplu: F(100) reprezintă graful asociat reţelei de interconectare
procesoare – memorii, considerând că switchurile C0 lucreează în cruce, iar C1
și C2 lucrează în linie.
Spre deosebire de rețeaua de interconectare de tip crossbar, în cazul acestor
RIC – uri, nu este posibilă implementarea oricărei funcţii bijective de
comunicaţie f:{P0, P1,..., P7}→{M0, M1,..., M7}, dintre cele 8! funcţii bijective
posibile, ci doar a celor 8 funcţii de mai sus (v. Figura 4.49).
De remarcat însă că în acest caz, complexitatea comutatoarelor este mai
mare decât în cazul crossbar, în schimb sunt mai puţine. Mai precis, RIC de tip
crossbar deține N2 comutatoare elementare, în timp ce o astfel de reţea deține
N
doar 4 × log 2 N × = 2Nlog 2 N < N 2 comutatoare elementare (48 în cazul exemplului
2
considerat). În schimb, o conexiune procesor-memorie este întârziată aici pe 3
nivele de comutatoare elementare şi nu pe unul singur, ca în cazul crossbar.
Un dezavantaj important al acestor arhitecturi, zise uneori şi “arii de
procesoare”, îl constituie posibilitatea unei căi de comunicare procesor –
memorie de a bloca alte căi necesare. Spre exemplu, în cazul rețelei BASELINE,
calea P0 – M7 blochează interconectarea simultană a oricărora dintre următoarele
conexiuni: P1 – M4, P1 – M5, P1 – M6, P1 – M7, P2 – M6, P3 – M6 etc. Rezultă deci
că o asemenea RIC este mai puţin potrivită pentru transferuri prin comutare de
circuite. În cazul unei reţele cu comutare de pachete, blocarea constă în
aşteptarea pachetului într-un buffer asociat comutatorului, până când se va ivi
posibilitatea trimiterii sale spre următorul nivel de comutatoare. Desigur, există
şi alte topologii de reţele multinivel (BANYAN, DELTA etc.)

5. SMM interconectate în hipercub (statică)

400
Figura 4.50. Interconectare hipercub (4-dimensional)

În hipercubul k – dimensional (binary k-cube) există N = 2k noduri


(procesoare), fiecare de gradul k, adică având legături directe cu alte k
procesoare. Dacă tratăm fiecare etichetă a nodurilor ca pe o valoare binară,
semnificând adresa acestui nod, nodurile conectate direct diferă printr-o singură
coordonată (adiacent codificate, codurile adiacente diferă printr-un singur bit).
Altfel spus, cei k vecini ai unui procesor Pj au etichete binare adiacente cu cea a
lui Pj. Pentru dimensiuni mai mari decât 3 ale lui k, diagrama de interconectare
devine mai laborioasă, dar ideea rămâne aceeaşi. Considerând un nod sursă
având adresa binară S și un nod destinație având adresa binară D, este definită
așa numita adresă relativă ca fiind: R(S, D)=S XOR D (XOR= conectorul logic
SAU EXCLUSIV). Această adresă relativă R(S, D) reprezintă dimensiunile care
trebuie parcurse pentru a ajunge de la nodul sursă S, la nodul destinație D, astfel
încât distanța să fie minimă. Ruta nu este neapărat unică. Lungimea rutei
(rutelor) este egală cu numărul biților de unu logic din funcția R(S, D). Ca un
exemplu, să considerăm un simplu cub (k=3), iar în cadrul acestuia, nodul sursă
S=2=0102 iar nodul destinație D=7=1112. În acest caz, R(S, D)=010 XOR 111 =
101. Cele două valori de unu logic obținute în rezultat (101) arată care biți
trebuie schimbați pentru a ruta mesajul din S în D. Aceasta înseamnă că, în cazul
particular considerat, sunt posibile două rute minimale, anume:

401
S=010 011 D=111 sau S=010 110 D=111. Câteva companii incluzând
INTEL, NCUBE, FPS etc. au studiat şi au implementat maşini în această reţea.

6. Arbore “gras” (Fat tree)

Rețeaua de interconectare de tip Fat tree este, după cum am mai arătat, un
arbore avînd lărgimea de bandă mai mare în nivelele superioare, astfel încât
lărgimea de bandă între oricare două nivele (consecutive) este constantă și deci
lărgimea de bandă nu se reduce odată cu trecerea spre nivelele superioare, ca
într-un arbore obișnuit. Spre exemplu, într-un arbore normal de aritate N, avem
că BW(Lk)=N*BW(Lk+1), unde BW(Lk) reprezintă lărgimea de bandă
(bandwidth) în nivelul k al arborelui. (Un arbore are aritatea N dacă orice nod
deține maximum N copii.) Într-o rețea de tip fat tree de aritate N avem însă
BW(Lk)=BW(Lk+1), ceea ce reprezintă un avantaj major. Un arbore are o
adâncime (număr de nivele) logaritmică. Într-o astfel de arhitectură de
comunicații, a adăuga un nou nivel în top implică dublarea numărului de noduri
(Figura 4.50b). Între două noduri (procesoare) ale unui fat tree pot exista
multiple căi de comunicație. Acesta reprezintă un avantaj, în vederea evitării
congestiilor și implementării unor facilități de toleranță la defecte (prin
redundanța existentă). Această rețea se întâlnește în implementarea unor
supercalculatoare, dintre care amintim binecunoscutul CM-5 supercomputer
(Thinking Machine Co, 1991), care interconecta 1024 de procesoare sau
cunoscutul supercomputer numit Mare Nostrum, de la Universitatea Politecnica
Catalunya din Barcelona (Barcelona Supercomputing Center) care
interconectează peste 10.000 de procesoare.

402
Figura 4.50b. Interconectare de tip Fat Tree (preluare de la ClusterDesign.org)

Se definește diametrul unei NoC ca fiind lungimea maximă a căii cu


lungimea (costul) minimă, între oricare două noduri. Distanța de rutare (routing
distance) între o pereche de noduri este dată de numărul de link-uri traversate de
ruta respectivă; în mod evident, aceasta nu coincide în mod necesar cu cea mai
scurtă cale.

Figura 4.51. Sistem multiprocesor NUMA, nesimetric (cu memorie distribuită


logic și fizic) sau rețea de calculatoare

Problema coerenței memoriilor cache în SMM

403
Problema apare în cazul scrierii unei date partajate (shared) în cache-ul
unuia dintre procesoarele componente. Pentru a asigura coerența acestei date în
toate cache-urile celorlalte procesoare care o dețin, este necesară garantarea
faptului că următoarele citiri ale acestei date de către alte procesoare vor aduce
noua dată (copia cea ma recentă) și nicidecum o copie replicată mai veche,
desuetă, a acestei date. Iată că redundanța datelor în cache-uri (replicarea
acestora), nu doar că reduce comunicațiile și sincronizările inter-procesoare, dar
poate crea și eventuale probleme de incoerență.
Cu alte cuvinte, coerența cache-urilor în sistemele multicore înseamnă, în
principiu, că o citire a unei variabile partajate va returna valoarea ultimei copii
scrise a acestei date, așa cum este definită aceasta de ordinea operațiilor cu acces
la memorie din program (dată de ordinea dependențelor de date), într-o execuție
validă a acestuia. Niciun procesor nu trebuie să poată citi din propriul cache o
valoare desuetă a unei variabile partajate. Coerența memoriilor cache în SMM
poate fi încălcată de operații de tipul read-modify-write pe variabile partajate,
după cum se va arăta, în mod concret, în continuare. Pentru a asigura coerența
cache-urilor în SMM este necesar ca actualizările variabilelor partajate să fie
atomice.
O definiție mai riguroasă a coerenței cache-urilor în SMM ar putea fi
următoarea:

Un SMM este coerent dacă sunt îndeplinite următoarele trei condiţii


simultane:
1. Un procesor P scrie variabila X. Dacă după un timp, P va citi variabila X
şi dacă între cele două accese la memorie ale lui P nici un alt procesor nu
a scris în X, atunci P va citi aceeaşi valoare a lui X cu cea scrisă.
2. Un procesor Pi scrie variabila X. Dacă după un timp, un alt procesor Pj
va citi variabila X şi dacă între timp niciun alt procesor nu a scris în X,
atunci Pj va citi aceeaşi valoare ca cea scrisă de către Pi. Condiţia nu este
triviala, având în vedere exemplul de mai jos (CPU1 scrie "0", CPU2
citeşte "1").
3. Scrierile la aceeaşi locaţie (X) trebuie serializate, prin arbitrare. De
exemplu, dacă P1 scrie 1 la adresa X şi apoi P2 scrie 2 la aceeași adresă,
niciodată un procesor nu va putea citi întâi X=2 şi apoi X=1.

404
O nerespectare a unuia dintre cele trei principii enuntate mai sus, poate
conduce la incoerenţa sistemului de memorie.

O soluție de asigurare a coerenței: strategia Write-invalidate

Ideea de bază a acestei strategii de asigurare a coerenței este aceea de a


invalida toate copiile din alte cache-uri ale datei respective (bitul V=0), înainte
de a scrie această dată în memoria cache locală a procesorului care dorește să o
scrie. Pentru aceasta este necesar ca semnalul de invalidare, precum și adresa de
acces la memoria comună, să se pună pe busul comun de acces. Toate cache-
urile vor “spiona” („asculta”, monitoriza) acest bus (snooping) și, în consecință,
vor acționa în mod corespunzător (dacă dețin acea copie, o vor invalida în
cache-ul local). Se va considera un exemplu care arată cum două procesoare pot
"vedea" două valori diferite pentru aceeaşi locaţie (X) de memorie globală, adică
un caz tipic de incoerenţă a unei valori globale de tip partajat (shared).

Pas Eveniment Conţinut Conţinut Conţinut


cache cache Memorie
CPU1 CPU2 globală (X)
0  1
1 CPU1 citeşte X 1 1
2 CPU2 citeşte X 1 1 1
3 CPU1 scrie 0 în X 0 1 0
(Write Through) 0 1 1
Write Back

S-a presupus că, iniţial, nici una din cele două cache-uri nu conţine
variabila globală X şi că aceasta are valoarea 1 în memoria globală. De
asemenea, s-au presupus cache-uri de tip WriteThrough - WT (un cache
WriteBack - WB ar introduce o incoerenţă asemănătoare). În pasul 3, CPU 2
(dar și memoria comună în cazul scrierii WT) are o valoare incoerentă a
variabilei X.

405
Fiecare dintre cache-urile care conţin copia unui bloc din memoria globală,
conţine, de asemenea, și "starea" acelui bloc, din punct de vedere al procesului
de partajare (partajat - "read-only", exclusiv - "read/write", invalid). Aşadar, nu
există o centralizare a informaţiei de stare a blocului, în acest caz. Fiecare
controller de cache monitorizează permanent busul comun, pentru a determina
dacă cache-ul respectiv conţine, sau nu conţine, o copie a blocului cerut pe busul
comun (snooping protocol). În cadrul acestui protocol de monitorizare, există
două posibilităţi de menţinere a coerenţei, funcţie de ceea ce se întâmplă la o
scriere:

1) Write Invalidate (WI). Procesorul care scrie determină ca toate copiile


din celelalte memorii cache să fie invalidate (se pune bitul de validare
V=0 în cadrul blocului respectiv din cache ⇒ orice acces la acel bloc va
fi cu MISS), înainte însă ca el să-şi modifice blocul în cache-ul propriu.
Respectivul procesor va activa pe busul comun un semnal de invalidare
bloc şi toate celelalte procesoare vor verifica, prin monitorizare, dacă
deţin o copie a blocului respectiv; dacă DA, trebuie să invalideze blocul
care conţine acel cuvânt. Evident că un anumit bloc invalidat în cache,
nu va mai fi scris în memoria globală la evacuare. Astfel, strategia de
coerență WI permite mai multe citiri simultane ale blocului, dar o
singură scriere în bloc, la un anumit moment dat. Este foarte des
implementată în SMM, împreună cu strategia de scriere în cache-uri de
tip Write Back.
2) Write Broadcast (sau Write Update) - WBC. Procesorul care scrie pune
data de scris pe busul comun, spre a fi actualizate toate copiile din
celelalte cache-uri. Pentru aceasta, este util să se ştie dacă un cuvânt este,
sau nu este, partajat (conţinut în alte cache-uri decât cel al procesorului
care scrie). Dacă nu este partajat într-un anumit cache din cadrul SMM,
evident că actualizarea (updating) sa în acel cache este inutilă. Acest
protocol de coerență este mai puţin utilizat în SMM actuale.

Observație: Ambele strategii de menţinere a coerenţei cache-urilor (WI, WBC)


pot fi asociate cu oricare dintre protocoalele de scriere în SMM (Write
Through respectiv Write Back).

406
Pas Activitate Activitate pe Loc. Loc.X Loc. X
procesor bus comun X cache Memorie
cach CPU2 globală
e
CPU
1
0 0
1 CPU1 citeşte X Cache Miss 0 0
(X)
2 CPU2 citeşte X Cache Miss 0 0 0
(X)
3 CPU1 scrie ‘1’ Invalidare X 1 INV. 0
în X
4 CPU2 citeşte X Cache Miss 1 1 0
(X)

Coerenţa prin protocol WI

În pasul 4 din tabelul anterior, CPU1 abortează ciclul de citire al lui CPU2
din memoria globală şi pune pe busul comun valoarea lui X ("1", copie
exclusivă). Apoi, scrie (actualizează) valoarea lui X în cache-ul lui CPU2 dar şi
în memoria globală, iar X devine o variabilă aflată în stare partajată (shared).
Mai jos, se prezintă un exemplu de protocol de coerenţă WBC, bazat pe un
protocol de scriere în cache de tip "Write Through":

Pas Activitate Activitate pe Loc. Loc.X Loc. X


procesor bus comun X cache Memorie
cach CPU2 globală
e
CPU
1
0   0

407
1 CPU1 citeşte X Cache Miss 0  0
(X)
2 CPU2 citeşte X Cache Miss 0 0 0
(X)
3 CPU1 scrie ‘1’ Write update 1 1 1
în X X
4 CPU2 citeşte X  1 1 1
(HIT)

Coerenţa prin protocol WBC

Diferenţele de performanţă între protocoalele de coerenţă WI şi WBC,


provin, în principal, din următoarele caracteristici:

a) Dacă acelaşi CPU scrie de mai multe ori la aceeaşi adresă, fără apariţia
intercalată a unor citiri din partea altor procesoare, sunt necesare scrieri
multiple pe busul comun în cazul WBC, în schimb este necesară doar o
invalidare iniţială, în cazul WI.
b) WBC lucrează pe cuvinte (deci la scrieri repetate în acelaşi bloc
accesează repetat busul comun), în timp ce WI lucrează pe bloc (la
scrieri repetate într-un bloc, determină doar o invalidare iniţială a acelui
bloc din celelalte cache-uri care îl conţin).
c) Întârzierea între scrierea unui cuvânt de către un procesor şi citirea
respectivei valori de către un alt procesor, este în general mai mică într-
un protocol WBC (hit), decât într-unul WI (miss).
d) Strategia WI, prin invalidarea blocurilor din celelalte cache-uri, măreşte
rata de miss. În schimb, strategia WBC măreşte traficul pe busul comun.

Actualmente, strategia de asigurare a coerenței de tip WI este preferată în


majoritatea implementărilor. În continuare, se va considera un protocol de
coerenţă WI şi unul de scriere în cache de tip WB. Pentru implementarea
protocolului WI, procesorul accesează busul comun şi distribuie pe acest bus,
adresa de acces, spre a fi invalidată data aferentă în toate copiile partajate. Toate

408
procesoarele, monitorizează în mod continuu busul comun, în acest scop. Dacă
adresa respectivă este caşată, atunci data aferentă este invalidată.
În plus faţă de procesele de invalidare, este de asemenea necesar în cazul
unui miss în cache, să se localizeze data necesară. În cazul WT este simplu,
pentru că cea mai recentă copie a blocului respectiv se află în memoria globală.
Pentru un cache "Write Back" însă, problema este mai complicată, întrucât cea
mai recentă valoare a datei respective se află într-un cache, mai degrabă decât în
memoria globală. Soluţia constă în următoarea acțiune: dacă un anumit procesor
deţine o copie a blocului accesat, având biţii D=1 (dirty) şi V=1 (valid), atunci el
furnizează pe busul comun data respectivă şi abortează accesul la memoria
globală (vezi pasul 4 în tabelul de mai sus).
În cazul unei scrieri cu MISS (WI, WB), invalidarea blocului în care se
scrie nu are sens să se facă, dacă nici un alt cache nu îl conţine. Rezultă că este
necesar să se ştie starea unui anumit bloc (partajat/nepartajat) în orice moment.
Aşadar, în plus faţă de biţii de stare D şi V, fiecare bloc mai poate deţine un bit
P (partajat sau nu). O scriere într-un bloc partajat (P=1), determină invalidarea
pe bus şi marchează blocul ca "privat" (P=0), adică respectivul cache deţine
copia exclusivă a acelui bloc. Procesorul respectiv se numeşte în acest caz,
proprietar (owner). Dacă ulterior, un alt procesor citeşte blocul respectiv, acesta
devine din nou partajat (P=1).
Un protocol de coerenţă (WI,WB) se implementează printr-un controller de
tip automat finit sincron, dedicat, plasat în fiecare nod. Controllerul răspunde
atât la cererile CPU-ului propriu, cât şi la cererile de pe busul comun, de tip
Read/Write miss etc. Ca urmare a acestor cereri, controllerul modifică "starea"
blocului din cache-ul local şi utilizează busul comun pentru accesarea sau
invalidarea informaţiei. În principiu, un bloc din cache se poate afla într-una din
următoarele trei stări: invalid, partajat sau shared (read only) şi exclusiv sau
modified (read - write). Pentru simplitate, protocolul implementat nu va distinge
între un "write hit" şi un "write miss" la un bloc "partajat" din cache; ambele se
vor trata ca "write miss"-uri. Când pe busul comun are loc un "write miss",
fiecare CPU care conţine în cache o copie a blocului accesat pe bus, o va
invalida. Dacă blocul respectiv este "exclusiv" (există dată corectă doar în acel
cache), se va actualiza ("write back"). Orice tranziţie în starea "exclusiv" (la o
scriere în bloc), necesită apariţia unui "write miss" pe busul comun, cauzând

409
invalidarea tuturor copiilor blocului respectiv, aflate în alte cache-uri. Apariţia
unui "read miss" pe bus, la un bloc exclusiv, va determina punerea lui ulterioară
ca "partajat" (shared) de către procesorul proprietar.
Dacă două procesoare actualizează o variabilă în mod alternativ (primul
care o scrie va genera un miss în cache, data fiind în copie exclusivă în al 2-lea
procesor), data va migra între cele două cache-uri care o conțin, generând un
efect de ping-pong, dar și miss-uri la fiecare scriere, într-un mod oarecum ironic.
De asemenea, protocolul de coerență WI va fi ineficient și dacă un procesor
actualizează o structură de date care este accesată de multe alte procesoare.

Protocoale de coerență de tip Snooping (Write Invalidate, Write Back)

Primul protocol de asigurare a coerenței memoriilor cache în sistemele


multiprocesor, în ordinea apariției, a fost așa numitul protocol MSI. În acest caz,
fiecare bloc dintr-un cache poate să fie într-una dintre stările Exclusive
(Modified), Shared (partajat), Invalid, prin doi biți de codificare, la nivelul
fiecărui bloc din cache. Semnificația acestor stări este următoarea:

• E – Exclusive (Modified): Numai în acest bloc există datele valide,


nicidecum în celelalte cache-uri ale SMM. Totuși, data din bloc ar putea fi
validă și în MP (imediat după o alocarea din MP într-un cache).
• S - Shared: Indică faptul că data conținută în acest bloc mai poate exista
și într-un alt cache al SMM.
• I - Invalid: Arată că datele din bloc sunt invalide, ca urmare a faptului că
au fost scrise (modificate) de către un alt procesor în propriul cache.
În tabelul următor se sintetizează acțiunile controlerului de cache la cereri
din partea propriului CPU, respectiv la cereri venite de la un alt CPU, prin
intermediul busului comun.

Sursă Tip cerere Starea Funcții, explicații, comentarii


blocului
adresat din
cache
*
CPU Read hit Shared (read Citește data din propriul cache. În mod

410
only) sau evident, blocul va tranzita în aceeași
Exclusive stare.
(Owner,
read/write)
Read miss Invalid Plasează semnalul Read_miss pe busul
comun. Blocul va tranzita în starea
Shared.
• Data se va citi din MP (dacă
există cumva și în alte cache-uri,
aducerea de aici ar necesita
arbitrare – nu este permis mai
multor procesoare să furnizeze
data simultan, din motive de
conflict control bus și a altor
complicații implicate) sau dintr-
un cache care-l conține în starea
Modified. (v. în continuare,
starea BUS/Read-
Miss/Exclusive)

Read miss Shared (data Miss datorat interferenței (conflict);


este în MP plasează semnalul Read_miss pe busul
sau/și în alte comun. Blocul cu datele noi va tranzita
cache-uri/alt în aceeași stare Shared (tranziție
cache) trivială)
• Data se va citi din MP (dacă
există cumva și în alte cache-uri,
aducerea de aici ar necesita
arbitrare și alte complicații) sau
dintr-un cache care-l conține în
starea M.
• Fiind în starea Shared, nu este
necesara evacuarea prealabilă.

411
Evacuarea se va face numai dacă
blocul interferent din cache se
află în starea Exclusive (v.
urmatoarea posibilitate)
Read miss Exclusive Conflict miss; Dacă Dirty_bit=1, blocul
va fi scris înapoi în MP (altfel, s-ar
pierde definitiv. Protocolul MESI
rezolvă altfel acest caz.) După această
acțiune, plasează semnalul Read_miss
pe busul comun. Data se va aduce din
MP în cache sau, eventual (alternativ),
dintr-un alt cache, care îl conține în
starea M. Blocul va tranzita în starea
Shared.

Write hit* Exclusive Se va scrie data în blocul din cache.


Acesta va face o tranziție trivială, în
aceeași stare, anume Exclusive.

Write hit* Shared Plasează semnalul Write_miss (sic!) pe


bus (acest semnal va invalida toate
celelalte copii valide, existente în
celelalte cache-uri ale SMM.) Blocul
va tranzita în starea Exclusive.

Write miss Invalid Plasează semnalul Write_miss pe busul


comun. Blocul va tranzita în starea
Exclusive (după ce va fi alocat în cache
din memoria principală sau dintr-un
cache în care blocul dorit se află în
starea M).
Write miss Shared Conflict miss; Nu este necesară
evacuarea blocului (se va face numai
pentru un bloc aflat în starea M);

412
plasează semnalul Write_miss pe bus.
Blocul nou va fi adus (alocat) din MP
sau dintr-un cache care îl deține în
starea M. Blocul va tranzita în starea
Exclusive.

Write miss Exclusive Conflict miss; Blocul adresat se va


evacua în memoria principală (această
operație poate fi evitată în protocolul
MESI – v. în continuare). Apoi, se
plasează semnalul Write_miss pe bus.
Blocul va tranzita în aceeași stare,
Exclusive.
BUS** Read_miss Shared Nicio acțiune. Dacă blocul respectiv ar
pune data pe busul comun, ar putea
rezulta un conflict cu celelalte surse ale
acestei date. Dacă există un cache care
să aibă o copie exclusivă a acestui bloc,
i se va permite acestuia să-l furnizeze
pe bus (deținător unic). În caz contrar
(data dorită nu există în niciun alt
cache), se va permite memoriei
principale să furnizeze data pe busul
comun. Starea următoare va fi aceeași
(Shared)
• Ase vedea protocolul MOESI în
acest caz!
Read_miss Exclusive Blocul adresat, fiind sursa unică a
datei, va furniza data pe bus și va trece
în starea Shared
• De asemenea, blocul va trebui
scris înapoi și în memoria
principală (de acum încolo, MP
va fi sursa datei). Astfel, se evită

413
preluarea ulterioară a acestei date
de la CPU-uri care o vor conține
în starea Shared, printr-o
eventuală arbitrare (complexă)
etc. Data necesară va fi în MP,
care o va și furniza.
Write_miss Shared Blocul adresat se va invalida (V=0).
Tranziție: Shared Invalid.

Write_miss Exclusive Blocul adresat se va evacua în MP și va


fi preluat și de cache-ul (procesorul,
mai exact) care l-a adresat pentru a-l
scrie. Apoi, blocul va trece în starea
Invalid.
*
Pe durata unui acces cu hit, evident că blocul nu poate fi invalid (V bit=0)
**
Read_miss sau Write_miss pentru un bloc Invalid Nicio acțiune

Protocolul MESI adaugă starea Exclusive (E) la protocolul anterior


prezentat (MSI). Această nouă stare (a nu se confunda cu starea Modified în
acest caz!) arată că blocul respectiv există doar în acel cache și în memoria
principală (MP), de unde a fost alocat. Așadar, un bloc în starea E este (încă)
nescris în cache (clonă a copiei din MP). Dacă blocul va fi scris în cache prin
strategie WB, acesta va trece în starea M (blocul este, în această stare M, unicul
deținător al datei corecte; nici măcar MP nu o mai conține acum), dar, evident,
nu vor mai fi necesare invalidări în alte cache-uri (pentru că niciun alt cache nu
mai conține acest bloc). Așadar, în cazul protocolului de coerență numit MESI,
fiecare bloc din cache poate fi într-una dintre următoarele patru stări:

• M - Modified: Blocul este prezent cu date valide numai în cache-ul


curent, fiind scris de procesor (dirty); așadar, prin scrierea de tip Write Back a
procesorului, datele din bloc au fost modificate față de cele existente în MP.
Atunci când blocul va fi evacuat din cache, datele aferente vor trebui scrise în
MP, pentru a nu se pierde. În starea E nu mai este necesară această evacuare în
MP, pentru că cele două copii sunt identice (în cache și MP).

414
• E - Exclusive: Datele din bloc sunt prezente doar în acest cache și în
memoria principală. Blocul a ajuns în cache în această stare (E) din MP, la
alocare (miss în toate cache-urile sistemului multiprocesor). În această stare E, la
evacuare, blocul respectiv nu va trebui scris și în MP, pentru că aceasta deține o
copie identică (clonă). Acest fapt constituie un avantaj față de protocolul MSI,
anterior prezentat. Totodată, dacă un procesor scrie cu hit într-un bloc din cache
aflat în starea E (write back), blocul acesta va trece în starea M, dar, evident, nu
mai este necesară invalidarea altor blocuri memorate în alte cache-uri și deci nici
accesarea busului comun sau a rețelei de interconectare.
• S - Shared: Arată că blocul respectiv există și în alte cache-uri sau în MP.
• I - Invalid: Blocul este invalid (V=0) din cauza unei scrieri anterioare, a
altui procesor.

Așadar, protocolul MESI ar putea fi comparat cu un protocol MSI în care


blocurile din cache au și un bit de Dirty asociat. Protocolul MESI este
implementat în multicore-urile Intel i 7, sub o formă ușor modificată.
Protocolul MOESI adaugă starea Owned (O) la stările MESI, care indică
faptul că data se află doar în cache, copia din memoria principală fiind incorectă.
Dacă ar fi numai această semnificație, starea O s-ar putea confunda cu starea M,
ceea ce nu este adevărat. În protocoalele MSI și MESI, atunci când se încerca
partajarea unui bloc aflat în starea M, acesta trecea în starea S, atât în cache-ul
furnizor cât și în cel care dorea să citească blocul din cache-ul furnizor. De
asemenea, blocul se scria și în MP. În cadrul protocolului MOESI, blocul din
cache-ul furnizor tranzitează în acest caz din starea M în starea O (spre exemplu,
la o citire cu miss a unui CPU), fără a mai fi scris și în MP. Protocolul acesta de
coerență a memoriilor cache este implementat în procesorul AMD Opteron
(multicore).
Protocolul MOESI implică cinci stări posibile pentru un bloc din cache,
anume:

• M - Modified: Blocul din acest cache este prezent numai aici, fiind scris
de către procesorul deținător; așadar, valoarea din memoria principală a acestui
bloc este una incorectă, fiind desuetă. După cum am mai subliniat, în cazul
evacuării blocului din cache, acesta trebuie scris în MP.

415
• O - Owned: Un bloc din cache în această stare deține copia cea mai
recentă a datelor din bloc, în mod unic (doar el, niciun altul). Nici măcar în MP
copia acestui bloc nu este una corectă (dacă blocul ar fi fost în starea S, copia
din MP era corectă). Cum se ajunge în această stare O? Să presupunem că un
procesor deține la acest moment un bloc în starea M (unic deținător). Dacă un alt
CPU dorește să citească o dată din acest bloc, CPU-ul furnizor trece blocul din
starea M în starea O, iar cel care a citit noua dată, trece blocul respectiv în starea
S. Așadar se ajunge în O din M, când un alt procesor vrea să citească valoarea
din blocul aflat în starea M. Procesorul care i-o dă trece M O (Read_miss pe
bus), iar cel care o citește, trece blocul citit în S (în protocoalele anterioare
trecea din M S). Avantajul stării O constă în faptul că se știe acum, în mod
clar, univoc, care procesor furnizeaza valoarea în cazul unui miss (cel în starea
O), făra arbitrare; altfel, nu s-ar ști, celelalte copii, din celelalte cache-uri, fiind
Shared (situația ironică de la protocolul MSI, în care, deși mai multe cache-uri
dețin data în starea S, niciuna nu o poate furniza, din cauza conflictului potențial
pe bus, așă ca data se aducea din MP). Un singur procesor poate deține blocul în
starea O, toate celelalte CPU-uri, dacă o dețin, o vor deține în starea S. Așadar
Owned =Modified AND Non-Shared. Cu alte cuvinte, starea O este o stare S
“mai specială”. Dacă blocurile aflate în starea S sunt egale, cel în starea O este
un fel de primus inter pares (lat., “primul între egali”) între blocurile aflate în
starea S. De ce? Pentru că toate blocurile din starea S au preluat data de la el, cel
care o deține acum în starea O (și nu în S, ca la MSI și MESI).
• E - Datele din bloc sunt prezente doar în acest cache și în memoria
principală. Blocul a ajuns în cache în această stare (E) din MP, la alocare (miss
în toate cache-urile sistemului multiprocesor). În această stare E, la evacuare,
blocul respectiv nu va trebui scris și în MP, pentru că aceasta deține o copie
identică. Acest fapt constituie un avantaj față de protocolul MSI anterior
prezentat. Totodată, dacă un procesor scrie cu hit într-un bloc din cache aflat în
starea E (write back), blocul acesta va trece în starea M, dar, evident, nu mai este
necesară invalidarea altor blocuri memorate în alte cache-uri și deci nici
accesarea busului comun.
• S - Shared: Arată că blocul respectiv există și în alte cache-uri sau în MP.
• I - Invalid: Blocul este invalid (V=0) din cauza unei scrieri anterioare a
altui procesor.

416
Fenomene de livelock pot apărea într-un protocol de coerență de tip
snooping (MSI, MESI, MOESI etc.) dacă acesta nu este proiectat într-un mod
adecvat. Pentru exemplificare, să considerăm că mai multe procesoare scriu,
practic simultan, la aceeași adresă de memorie partajată și, inițial, niciun
procesor nu conținea data în cache-ul propriu. Într-un caz defavorabil (“Murphy
nu doarme!”) ar fi posibil următorul scenariu: un anumit procesor (P1), care
încearcă să scrie blocul corespunzător, înainte de a-l scrie îl aduce în cache-ul
propriu, îl pune în starea Modified și invalidează copiile potențiale din celelalte
cache-uri. Acum, P1 ar trebui să scrie în mod efectiv blocul adus dar, înainte de
a încheia această scriere, blocul este invalidat de un alt procesor P2, care
încearcă și el să-l scrie în propriul cache. În acest caz, la acest moment scrierea
lui P1 va genera un miss, într-un ciclu indefinit. Pentru a se evita astfel de
blocaje de tip livelock, după ce un procesor (P1) obține copia exclusivă, procesul
său de scriere trebuie să fie unul atomic, așadar acesta trebuie să se încheie
înainte de a pierde exclusivitatea.
Aceste controlere de cache se implementează în sistemele multiprocesor
sub forma unor automate finite sincrone (modele de tip Mealy, intrare-stare-
ieșire), implementabile cu câte un circuit basculant bistabil la fiecare stare, cu
stări codificate implementate prin bistabile etc. Spre exemplu, automatul este
descris prin acțiunile sale în fiecare stare (Si), astfel:
...
Starea S5:
dacă intrarea I3=1 activează comanda C7=1 și treci în S8
altfel (I3=0), activează comanda C9=1 și treci în S2
...
O implementare cu câte un bistabil D la fiecare stare ar scrie ecuațiile
bistabililor D astfel (ecuația de stare a unui astfel de bistabil este Q(n+1)=D):

• D2=S5 SI (~I3) SAU... (se vor scrie termenii aferenți tuturor stărilor din
care se poate tranzita în S2). Expresia se va minimiza dacă este posibil.
• D8= S5 SI (I3) SAU... (se vor scrie termenii aferenți tuturor stărilor din
care se poate tranzita în S8). Expresia se va minimiza dacă este posibil.

417
Ecuațiile comenzilor sunt următoarele:

C7= S5 SI (I3) SAU... ; C9= S5 SI (~I3) SAU... Se vor adăuga termenii aferenți
altor stări. Expresiile se vor minimiza dacă este posibil.
Consistența secvențială a unui program concurent

Un program concurent (paralel) trebuie să specifice în mod clar, neambiguu,


ordinea operațiilor de scriere-citire. Consistența secvențială a programului
concurent (CS) înseamnă că ordinea acestor operații în cadrul thread-urilor
componente ale programului (intra-thread) este cea naturală, a fluxului de date
(data-flow order), sau, altfel spus, a dependențelor de date rezultate din scrierea
secvențială a programului. Iar referitor la ordinea operațiilor inter-thread-uri,
rezultatele finale trebuie să fie aceleași, indiferent de ordinea de terminare a
thread-urilor, atunci când nu sunt specificate sincronizări explicite între ele.
Așadar, în mod sintetic am putea scrie, CS: În_Procedură/Thread [data-flow
order] AND Inter_Proceduri [Arbitrar_dacă_Non_Explicit_Synchronization].
După cum am mai subliniat, un program concurent conține mai multe fire de
execuție, care operează asupra unor date partajate. Modelul de programare
paralelă specifică ce date pot fi accesate de către aceste fire, care operații pot fi
efectuate asupra datelor partajate și care este ordinea între aceste operații.
Ordinea cea mai naturală, numită și ordinea din programul secvențial, înseamnă
că, în viziunea programatorului, variabilele sunt citite și scrise în ordinea
specificată de fluxul de date al programului, adică în ordinea dependențelor de
date. Aceasta trebuie să fie asigurată de compilator și de sistemul run-time.
Un program paralel trebuie să coordoneze activitatea firelor sale concurente,
astfel încât să se asigure respectarea dependențelor de date din cadrul
programului. Această cerință necesită sincronizări explicite atunci când ordinea
implicită a operațiilor din program nu este suficientă pentru o terminare
deterministă a programului (adică să genereze aceleași rezultate).
Coerenţa cache-urilor asigură o viziune consistentă a memoriei pentru
diversele procesoare (fire de execuție) din sistem. Nu se răspunde însă la
întrebarea "cât de consistentă?", adică în ce moment trebuie să vadă un procesor
că o anumită variabilă a fost modificată de către un altul?

418
Exemplu (clasic):

(P1) (P2)
A=0; B=0;
---- ----
A=1; B=1;
L1: if(B==0)... L2: if(A==0)...

Se consideră două fire de execuție concurente, care operează asupra


variabilelor partajate A și B. În mod normal, este imposibil pentru ambele
programe (fire), rezidente pe procesoare diferite (P1, P2), să evalueze pe
adevărat condiţiile L1 şi L2, întrucât dacă B=0 (în L1), atunci A=1 (prin
precedenta asignare) şi dacă A=0 (în L2) atunci B=1 (prin precedenta asignare).
Şi totuşi, acest fapt s-ar putea întâmpla. Să presupunem că variabilele partajate
A şi B sunt caşate pe '0' în ambele procesoare, în urma asignărilor respective.
Dacă, spre exemplu, între scrierea A=1 în P1 şi invalidarea lui A în cache-ul lui
P2 se scurge un timp suficient de îndelungat, atunci este posibil ca P2, ajuns la
eticheta L2, să evalueze (eronat) A=0.
Cea mai simplă soluţie pentru această inconsistență constă în forţarea
fiecărui procesor care scrie o variabilă partajată, de a-şi întârzia această scriere,
până în momentul în care toate invalidările (WI) cauzate de către procesul de
scriere (în cache-urile sistemului), se vor fi terminat. Această strategie simplă se
numeşte consistenţă secvenţială. Ea impune ca orice simulare a procesării unui
anumit program, prin respectarea secvenţialităţii interne din cadrul unui fir
(data-flow order) şi, respectiv, prin ordinea aleatoare de execuţie a firelor între
ele, atunci când nu este specificată una anumită prin operații de sincronizare
explicite, să conducă la aceleaşi rezultate finale. Altfel spus, un program scris
corect va duce la aceleaşi rezultate, indiferent de modurile posibile de întreţesere
a firelor pe parcursul rulării lor. Deşi consistenţa secvenţială prezintă o
paradigmă simplă, totuşi ea reduce performanţa, în special pentru sistemele cu
un număr mare de procesoare sau cu reţele de interconectare de latenţe ridicate.

Observație: Consistenţa secvenţială, nu permite, spre exemplu, implementarea


unui "write buffer" la scriere, care pe baza captării adresei şi datei

419
procesorului, să se ocupe în continuare de procesul de scriere efectivă a
datei în memorie, degrevând astfel procesorul de acest proces.

Protocoale de coerență de tip Directory based (Write Invalidate, Write Back)

Protocoalele de coerență de tip snooping, anterior prezentate, necesită în


cazul unui miss în cache comunicații între toate cache-urile implicate, incluzând
aici scrieri ale datelor partajate (invalidări, citire dată dintr-un cache în starea M
sau O sau din memoria principală, evacuare dată din cache-ul care a generat
miss în MP etc.) Schemele acestea beneficiază, oarecum, de absența unei
structuri centralizate, care să memoreze starea blocurilor cache-urilor din SMM,
fiind astfel mai puțin complexe și costisitoare. Pe de altă parte însă, aceste
protocoale de coerență nu permit scalabilitatea SMM-urilor, tocmai datorită
comunicațiilor adiționale implicate pe busul comun. Prin urmare, cercetătorii și-
au pus problema proiectării unui sistem de coerență a cache-urilor care să
permită scalabilitatea mai accentuată a SMM-urilor cu memorie partajată, dar
distribuită din punct de vedere fizic (v. Figura 4.51), inclusiv din acest punct de
vedere (al protocolului de coerență). Aceste scheme se numesc generic, scheme
de coerență de tipul directory-based protocols (bazate pe “directori”) și permit
scalabilitatea protocoalelor de coerență ale cache-urilor într-un SMM cu
memorie distribuită fizic.
Ideea de bază a acestor protocoale de coerență este de a plasa starea
fiecărui bloc de memorie într-un așa numit director. Acest director (directory -
tabelă centralizată) implementat în cadrul schemelor directory-based de
asigurare a coerenței, memorează starea fiecărui bloc care poate fi cașat, prin
informații de genul: care cache-uri dețin copii ale blocului respectiv, dacă blocul
dorit a fost scris sau nu etc. Așadar, fiecare bloc din memorie are asociată o
anumită intrare în structura acestui director. Mai mult, fiecare bloc din memoria
principală, de capacitate egală cu aceea a unui bloc din cache, are asociată o
intrare unică în director, cf. Figurii 4.51b. Această intrare conține informații
referitoare la cache-urile din sistem care memorează copii ale blocului respectiv
(dacă există vreun astfel de cache) și starea acelui bloc, în respectivele cache-uri.
Când un CPU generează un miss în cache-ul propriu, el accesează intrarea
corespunzătoare din director. Astfel, află care cache-uri conțin copii valide ale

420
blocului accesat (dacă există vreun cache care să-l conțină. În caz contrar, blocul
se va afla în MP.) În urma acestei acțiuni, procesorul poate obține, spre
exemplu, un bloc de date în starea Modified de la un alt CPU (cache) sau, în
cazul unei scrieri, acesta va trimite invalidări doar la celelalte cache-uri care
conțin blocul respectiv și va primi confirmările corespunzătoare de la acestea,
prin intermediul rețelei de interconectare - RIC [Cul99]. Schimbările de stare ale
blocului vor fi comunicate intrării directorului, prin intermediul infrastructurii
RIC. La o citire cu miss din cache, directorul accesat indică din care nod poate fi
citită data respectivă (dacă nu există niciunul, aceasta va fi citită din MP). La o
scriere cu miss, directorul identifică unde există copii ale blocului și le
invalidează sau le actualizează prin tranzacții prin intermediul RIC. După cum
am mai menționat, în acest caz se așteaptă ulterior confirmări ale operațiilor
declanșate, prin intermediul RIC (acknowledgements). Din punct de vedere
teoretic, ar rezulta că numărul de intrări ale directorului este proportional cu
produsul dintre numărul procesoarelor și respectiv numărul blocurilor din
memorie. Pentru câteva sute/mii de procesoare (manycore), capacitatea acestei
tabele este nefezabilă, la nivelul tehnologiei actuale. Așadar, pentru sisteme de
tip manycore sunt necesare metode care să permită scalabilitatea eficientă a
structurilor de directoare. Metodele care au fost utilizate în acest sens încearcă să
memoreze în acești directori starea mai puținor blocuri – spre exemplu doar a
celor rezidente în anumite cache-uri – sau încearcă să utilizeze mai puțini biți
pentru o intrare, prin monitorizarea doar a unui număr mai restrâns de
procesoare (astfel se reduce numărul de biți necesari). Pentru a preveni ca un
asemenea director centralizat din punct de vedere logic să devină o sursă de
blocaj (bottleneck), acesta este de obicei distribuit din punct de vedere fizic, prin
implementarea mai multor directoare fizice (distribuite deci). Distribuirea
trebuie să permită însă protocoalelor de coerență să știe care director conține
starea unui anumit bloc. Fiecare astfel de director acoperă deci o anumită zonă
de memorie. Astfel, accesele la directorul centralizat din punct de vedere logic,
se duc de fapt în directoare fizic distribuite, prin intermediul RIC de bandă largă.
Un director distribuit fizic memorează într-o intrare univoc determinată starea
unui anumit bloc din memorie. Această proprietate permite protocoalelor de
coerență să evite comunicațiile excesive, anterior menționate în cadrul
protocoalelor de tip snooping. Așadar, aceste protocoale de menținere a

421
coerenței cache-urilor, bazate pe directoare fizic distribuite, sunt utilizate și
pentru a reduce în mod semnificativ necesitățile de lărgime de bandă într-un
SMM cu memorie partajată centralizată [Hen11].

Figura 4.51b. Sistem multiprocesor scalabil (RIC), cu protocol de


coerență prin directori

Într-un sistem multiprocesor cu memorie distribuită (NUMA), de obicei


fiecare nod din sistem deține o structură de tip director, în vederea asigurării
mecanismului de coerență a cache-urilor. Fiecare astfel de director memorează
starea blocurilor din memoria pe care nodul o gestionează. Practic, acesta se
ocupă cu memorarea stării blocurilor din memoria proprie, care desigur că pot fi
memorate în cache-urile din SMM. Acest director poate comunica atât cu
propriul procesor, cât și cu memoria aferentă lui. Stările blocurilor, memorate
într-un director, sunt relativ similare cu cele utilizate în protocoalele de tip
snooping. Un asemenea director trebuie să memoreze care procesoare dețin copii
ale unui anumit bloc din memorie, astfel încât acele copii să poată fi invalidate

422
la o scriere. Pentru aceasta se poate utiliza, în cadrul intrării din director, un
vector de N biți, numit vector de prezență, pentru fiecare bloc de memorie (1
logic pe poziția k – înseamnă că blocul este cașat de procesorul k, 0 logic – nu
este cașat acolo). S-a considerat că există N procesoare în sistem. Astfel,
invalidările se vor trimite prin RIC doar la cache-urile care conțin blocul
respectiv. Din motive de eficiență, aceste informații pot fi memorate și în cache-
uri. Să mai considerăm că intrarea unui director conține un singur bit de stare
pentru un bloc, anume acela de Dirty. Daca Dirty=1 într-un bloc al unui anumit
nod, atunci numai acel nod conține blocul de date valid. Evident că în acest caz
și bitul de prezență în cache este setat în acea intrare a directorului. Cu o
asemenea structură a directorului, o citire cu miss poate determina dacă există
vreun nod care să aibă o copie Dirty a blocului sau dacă acesta se află în MP. De
asemenea, la o scriere cu miss, se poate determina care noduri trebuie invalidate
[Cul99].
Pentru o exemplificare mai precisă, se consideră un protocol MSI la
nivelul cache-urilor. La o citire cu miss în nodul i, se caută intrarea blocului
respectiv în director și se acționează astfel:
• Dacă bitul Dirty=0, blocul se aduce din MP. Bitul de prezență a
blocului i în intrarea directorului se pune pe 1.
• Dacă însă Dirty=1, se determină care este nodul care conține blocul
respectiv, după vectorul de prezență din director, pentru a fi citit.
Starea cache-ului nodului deținător este trecută în Shared. În
director, mai precis în intrarea blocului citit, se pune bitul i de
prezență pe 1, iar Dirty=0.
La o scriere cu miss în nodul i, se caută intrarea blocului respectiv în
director și se acționează astfel:
• Dacă bitul Dity=0 atunci MP conține o copie curată a datei
accesate. Trebuie trimise semnale de invalidare prin RIC la toate
nodurile j care au biții de prezență puși pe 1. Intrarea directorului
este ștearsă, cu excepția bitului i de prezență, care se pune pe 1, ca
și bitul Dirty aferent acestuia. Și în starea blocului din cache se
pune Dirty=1.
• Dacă Dirty=1, blocul este mai întâi adus din nodul deținător prin
intermediul RIC. Cache-ul din care a fost adus blocul își va

423
schimba starea în Invalid. Blocului accesat, odată ajuns în
procesorul care a dorit să-l citească, i se pune bitul de stare Dirty=1.
Intrarea corespunzătoare din director este ștearsă, cu excepția
bitului i de prezență respectiv a bitului Dirty aferent acestuia, care
vor fi puși pe 1.
Detalii asupra acestui protocol sunt prezentate în [Cul99, Hen11].

Descrierea unor programe concurente pentru sisteme multicore

În continuare vom prezenta etapele necesare a fi parcurse în vederea


dezvoltării unui program paralel. Printr-o operație vom înțelege cea mai mică
unitate concurentă din cadrul aplicației pe care un program paralel ar putea-o
exploata. Dacă o operație necesită o cantitate mică de procesare, aceasta se
numește fine-grain (spre exemplu, o iterație a unei bucle). Dacă, dimpotrivă,
operația necesită o mare cantitate de procesare, se va numi coarse grain (spre
exemplu, calculul unei coloane a matricii rezultat în cadrul unui program de
înmulțire a două matrici). Cu aceste precizări, etapele necesare dezvoltării unui
program paralel sunt următoarele [Cul99]:

• Decompoziția, care înseamnă împărțirea aplicației într-o mulțime de


operații (concurente). Aceste operații pot deveni disponibile în mod
dinamic, pe măsură ce programul rulează pe o anumită mașină. Numărul
maxim de operații disponibile la un moment dat al procesării indică o
limită superioară a firelor de execuție care pot rula în mod simultan, la un
moment dat. Scopul principal al acestei etape este de a scoate în evidență
cât mai multă concurență, pentru a menține firele de execuție cât mai
ocupate. Concurența unei aplicații poate fi mai mare sau mai mică, în
acord cu legea lui Amdahl. Se definește profilul concurenței unei aplicații,
care să pună în evidență câte operații (concurente) sunt disponibile la
fiecare moment al procesării. Acest profil este dependent de problemă și
nu de numărul efectiv de procesoare disponibile în sistem. De fapt,
profilul concurenței consideră, în mod implicit, că există un număr infinit
(nelimitat) de procesoare.

424
• Asignarea operațiilor, care semnifică modul de asignare a operațiilor
anterior definite (N) pe firele de execuție (M threads), N≥M. Această
etapă urmărește o balansare optimală a firelor de execuție, astfel încât să
se reducă comunicațiile inter-thread-uri (Load Balancing). Asignarea
poate fi statică (se face înainte de rularea programului, prin analiza
acestuia) sau dinamică (pe timpul rulării). Asignarea dinamică se face
deseori ca reacție la balansarea neoptimală a thread-urilor. Decompoziția
și asignarea sunt etape independente de arhitecura hardware, dar și de
modelul paralel de programare utilizat.
• Orchestrarea înseamnă implementarea mecanismelor de comunicație de
date și sincronizare între firele asignate, la nivelul limbajului concurent de
programare. În această etapă se specifică modul de exploatare a localității
spațiale a datelor, modul de organizare a structurilor de date etc.
Obiectivul major al acestei etape este acela de a reduce cât mai mult
posibil costurile de comunicație și de sincronizare inter-fire, prin
asigurarea localității spațiale optime a datelor, planificarea optimală a
firelor de control etc.
• Maparea este procesul prin care firele de execuție se alocă procesoarelor
din sistemul de calcul, spre rulare. Această etapă cade în sarcina
sistemului de operare de cele mai multe ori (foarte rar în sarcina
programatorului, pentru că în acest caz portabilitatea programului ar fi
afectată. În plus, necesită cunoștințe serioase despre arhitectura
hardware.) Sistemul de operare implementează algoritmi sofisticați de
planificare, prin care se specifică în mod clar pe ce procesor va rula un
anumit fir, la ce moment etc. Evident, se urmărește o utilizare optimală a
resurselor hardware în cadrul acestei etape. Dezvoltarea unor algoritmi
inteligenți de mapare constituie un obiectiv important al cercetărilor
actuale.

Rularea eficientă a unui program paralel nu necesită doar structuri hardware


suplimentare, față de o arhitectură secvențială de procesor, dar și o modalitate
adecvată de exprimare a calculului paralel. Să considerăm un exemplu

425
2N
elementar, dar sugestiv în acest sens, anume calculul sumei S = ∑ A(k ) , adaptat
k =1

după [Jor03]. Cele două figuri următoare prezintă intuitiv modul de calcul
secvențial (Sserial) respectiv modul de calcul paralel (Sparalel) al acestei sume
iterative.

Figura 4.51c. Graful dependențelor de date pentru sumare serială

Figura 4.51d. Graful depenedențelor de date pentru sumare paralelă

426
Complexitatea algoritmului serial este O(2N) – necesitând 2N pași
secvențiali, pe când cea a algoritmului paralel este O(N) – necesitând deci doar
N pași paraleli. În schimb algoritmul Sparalel are nevoie de 2N-1 sumatoare
hardware (toate necesare în pasul 1, implicând o mașină superscalară sau de tip
multiprocesor) pe când algoritmul Sserial are nevoie de doar un singur sumator
hardware (procesor scalar deci).
Desigur că, pentru a exploata în mod corespunzător un sistem multicore
avem nevoie de un limbaj HLL concurent, prin care să se poată specifica modul
în care programul, partiționat în fire de execuție, rulează pe sistemul paralel.
Deseori, la un limbaj secvențial convențional (ca limbajul C, spre exemplu), se
atașează niște biblioteci sofware, care oferă funcții utile pentru descrierea unui
algoritm paralel (precum OpenMP – pentru modele de programare de tip shared
memory, sau Message Passing Interface ori Parallel Virtual Machine – pentru
modele de programare de tip message passing). Există și limbaje nativ
concurente, precum Open CL sau Cuda (GPU), spre exemplu. Mai jos, se
prezintă câteva directive (pragmas – cum sunt numite în OpenMP) utile pentru
descrierea unor programe concurente, paralelizabile pe SMM [Jor03]:

- fork <label>; startează unul sau mai multe fire de execuție, începând de
la eticheta <label>. Procesul originar (thread-ul principal) continuă cu
instrucțiunile de după directiva fork.
- join <integer>; sincronizează un număr de <integer> fire de execuție
(barieră de sincronizare). Firul principal va continua procesarea cu
instrucțiunile de după barieră, numai după ce toate firele specificate se
vor fi terminat de executat.
- shared <variable list>; variabilele specificate vor fi din categoria
variabilelor partajate. Acestea vor putea fi accesate de către toate firele
de execuție.
- private <variable list>; variabilele specificate vor fi din categoria
variabilelor private (aparținând deci doar unui fir). Fiecare fir are
propriile sale variabile referite în această categorie.

427
Următoarele două figuri au fost preluate din prezentarea Dr. Rodric
Rabbah, intitulată Parallel Programming Concepts (MIT OpenCourseWare,
2007), disponibilă online la http://ocw.mit.edu/courses/electrical-engineering-
and-computer-science/6-189-multicore-programming-primer-january-iap-
2007/lecture-notes-and-video/lec5parallelism.pdf (accesat la 09.06.2016).
Acestea sunt extrem de instructive, întrucât arată că paralelismul la nivelul
firelor de execuție, fără exploatarea localității spațiale a datelor în cadrul
blocurilor fizice de memorie, nu este de mare folos, datorită bottleneck-ului la
memoria partajată.

Figura 4.52. Paralelism la nivelul thread-urilor, fără paralelism la nivelul


memoriei

428
Figura 4.53. Paralelism la nivelul thread-urilor, cu paralelism la nivelul
memoriei

În continuare se prezintă versiuni rafinate succesiv ale unor programe


concurente simple, destinate procesării lor pe SMM, preluate și adaptate din
[Jor03]:

Un exemplu de program paralel


N −1
/* C :=AxB, cij = ∑ aik bkj */ înmulțirea a două matrici pătratice
k =0

private i, j, k;
shared A[N,N], B[N,N], C[N,N], N;

/* Startează N-1 fire noi, fiecare calculând o coloană diferită a matricii C. */


for j := 0 step 1 until N-2 fork MULCOL;
/* Firul principal (j=N-1) va calcula ultima coloană a matricii C */
j := N-1;
MULCOL:

429
/* Se execută N fire, pentru j = 0 la N-1, fiecare lucrând în mod asincron și
concurent la o coloană diferită a matricii C. Implementează astfel conceptul
Single Program Multiple Data (SPMD) */
for i := 0 step 1 until N-1
begin
C[i,j] := 0;
for k := 0 step 1 until N-1
C[i,j] := C[i,j] + A[i,k] * B[k,j];
end
join N; /* Barieră de sincronizare pentru toate cele N fire. */

Un alt exemplu de program paralel (adaptat din [Jor03])

Se are în vedere rezolvarea unui sistem liniar recurent de ordinul (n-1),


astfel:
x1 = c1
x2 = c2 + a21x1
x3 = c3 + a31x1 + a32x2
x4 = c4 + a41x1 + a42x2 + a43x3, și așa mai departe …

După cum se poate observa, variabila xk+1 este dependentă de variabilele


anterioare xk. până la x1. Un pseudo-cod paralel preliminar ar putea fi următorul:

shared n, a[n,n], x[n], c[n];


private i, j;
for i=1 step 1 until n-1 fork DOROW; /* câte un fir pentru fiecare rând x[i]*/
i=n; /*procesul inițial lucrează pentru i=n.*/
DOROW: x[i] = c[i];//paralelism n thread-uri
for j=1 step 1 until i-1
x[i] = x[i] + a[i,j]*x[j];//paralelism n-1 threads (j=1), (n-2)
threads pt. j=2...
join n;

430
În bucla imbricată există o concurență semnificativă. Astfel, spre
exemplu, în cadrul iterației j=1, în timp ce firul 2 (x2) adună la valoarea
momentană (parțială) a variabilei x2 termenul a21x1, firul 3 (x3) adună la valoarea
momentană a variabilei x3 termenul a31x1 ș.a.m.d. (n-1 fire concurente). Analog,
pentru j=2 avem n-2 fire concurente ș.a.m.d. Totuși apar câteva probleme:

• Firele trebuie să recepționeze câte o copie privată a variabilei i, cu valori


distincte pentru fiecare dintre ele, la momentul la care sunt create (fork).
În programul anterior, această problemă este soluționată în mod ambiguu.
• Toate firele trebuie să lucreze simultan în bucla imbricată, după variabila
j. Valoarea finală a elementului x[k] trebuie să fie disponibilă înainte de a
fi accesată de firele având valori i>k (dependențe RAW). Prin urmare, va
fi necesară în acest caz o sincronizare explicită, ca mai jos. În caz contrar,
programul va procesa incorect.
• join n, specifică doar câte fire se vor sincroniza la barieră, fără a le numi
explicit.
• Toate aceste probleme vor trebui clarificate într-o versiune următoare a
programului.

Mecanismele de transfer ale valorilor parametrilor la apelul unei funcții din


cadrul limbajelor secvențiale trebuie utilizate și aici, pentru instanțierea firelor
concurente de execuție. Astfel, variabila i trebuie transmisă prin valoare, la firele
noi create.
Create/Call – creează și startează (prin apel) noi fire de execuție, care vor fi
procesate în paralel cu procesul creator. Acesta din urmă însă (creat cu directiva
Create), spre deosebire de celelalte procese, nu va aștepta după instrucțiunea
return. Un return, în cadrul unei proceduri care poate fi apelată printr-un call
sau un create, este interpretat ca simplă ieșire - quit (în cazul create) sau
sequential return (call). Această formă de implementare a terminării firelor
necesită utilizarea unui contor explicit, care să determine (când este zero)
momentul în care toate procesele s-au încheiat.

shared n, a[n,n], x[n], c[n], done;


private i;

431
done = n; /* numărul de procese*/
for i=1 step 1 until n-1
create dorow (i, done, n, a, x, c); /*creează (n-1) fire*/
i=n;
call dorow (i, done, n, a, x, c); /*apelează firul nr. n.*/
while (done ≠ 0); /*așteaptă până când toate cele n fire s-au încheiat.*/

procedure dorow (value i, done, n, a, x, c)


shared n, a[n,n], x[n], c[n], done;
private i, j;
x[i] = c[i];
for j=1 step 1 until i-1
x[i] = x[i] + a[i,j]*x[j];
done = done -1;
return; /* quit - în cazul create sau return-call*/
end procedure

Există în cazul exemplului considerat necesitatea sincronizării operațiilor


de acces la datele partajate, numite în literatura de specialitate Data-based
synchronization sau Producer/Consumer Synchronization. Aceste sincronizări
implică, în principiu, asocierea unei stări de tip valid (produsă) / invalid
(consumată) - full/empty -, pentru fiecare variabilă partajată. Ele se pot realiza
prin directive de tip produce/consume/copy/void pe variabile partajate, având
următoarea sintaxă respectiv semantică:

produce <shared var> = <expression>; așteaptă ca variabila partajată să devină


empty (consumată), apoi scrie noua valoare a acesteia și îi setează starea ca fiind
acum full – validă pentru consum (variabila deține acum o valoare nouă).
consume <shared var> into <private var>; așteaptă ca valoarea variabilei
partajate să se producă (aceasta să ajungă în starea full), citește valoarea ei și
apoi îi setează starea pe empty (poate fi de acum produsă de o altă scriere.)
copy <shared var> into <private var>; așteaptă ca variabila să devină produsă
(full) și apoi o citește, dar nu o mai setează după citire pe starea empty
(consumată).

432
void <shared var>; inițializează starea variabilei partajate pe starea empty,
semnificând faptul că poate fi produsă valoarea aferentă.
Cu aceste specificații, o variantă mai rafinată și deci mai corectă a
programului anterior este următoarea:

shared n, a[n,n], x[n], c[n], done;


private i;
done = n; /* nr. de procese*/
for i=1 step 1 until n-1
{void x[i];
create dorow (i, done, n, a, x, c); } /*creează n-1 proceduri.*/
i=n;
void x[i];
call dorow (i, done, n, a, x, c); /*apelează al n-lea proces.*/
while (done ≠ 0); /*buclează până când instanțele tuturor celor n proceduri s-au
încheiat.*/

procedure dorow (value i, done, n, a, x, c)


shared n, a[n,n], x[n], c[n], done;
private i, j, sum, priv;
sum = c[i];
for j=1 step 1 until i-1
{copy x[j] into priv; /*asignează valoarea x[j] când este
disponibilă.*/
sum = sum + a[i,j]*priv; }
produce x[i] = sum; /*fă x[i] disponibilă pentru alții.*/
done = done -1;
return;
end procedure

Atomicitatea

Asignarea done = done -1 din program reprezintă o operație de tipul Read


- Modify – Write pe o variabilă partajată și deci, după cum am mai arătat, ar trebui

433
scrisă sub forma unei secțiuni critice, pentru că dacă va fi cumva executată
simultan de mai multe fire, poate crea probleme. Chiar dacă în exemplul
considerat s-ar părea că accesul la această secțiune de cod nu poate apărea
simultan din partea mai multor fire (cele n fire par a intra secvențial în această
secțiune), în alte programe asemenea probleme pot însă apărea.
În cazul accesării concurente a acestei asignări problemele pot să apară
datorită faptului că aceasta nu este de fapt o operație atomică. Astfel, dacă două
procese accesează valoarea variabilei done într-o anumită ordine defavorabilă,
valoarea finală a acesteia ar putea fi done -1 (eronată), în loc de done-2
(corectă). De aceea secvența se atomizează prin directive de tip lock/unlock, ca
mai jos.

shared n, a[n,n], x[n], c[n], done;


private i;
done = n; /* nr. de processe*/
for i=1 step 1 until n-1
{void x[i];
create dorow (i, done, n, a, x, c); } /*creează n-1 proceduri.*/
i=n;
void x[i];
call dorow (i, done, n, a, x, c); /* apelează al n-lea proces.*/
while (done ≠ 0); /* buclează până când instanțele tuturor celor n proceduri s-au
încheiat.*/

procedure dorow (value i, done, n, a, x, c)


shared n, a[n,n], x[n], c[n], done;
private i, j, sum, priv;
sum = c[i];
for j=1 step 1 until i-1
{copy x[j] into priv; /* asignează valoarea x[j] când este
disponibilă.*/
sum = sum + a[i,j]*priv; }
produce x[i] = sum; /* fă x[i] disponibilă pentru alții.*/
critical /*lock/

434
done = done -1; /*decrementează în mod atomic variabila partajată;
control-based synchronization*/
end critical /*unlock*/
return;
end procedure

ATOMIZĂRI ŞI SINCRONIZĂRI. DETALII DE IMPLEMENTARE

Acest paragraf, legat de implementarea în ISA - hardware a atomizărilor și


sincronizărilor, are la bază părți ale unei lucrări anterioare scrise și publicate de
autor, sub forma unei monografii tehnice [Vin00b], într-o versiune ușor revizuită
aici.
În cazul proceselor de sincronizare, dacă un anumit procesor "vede"
semaforul asociat unei variabile globale pe '1' (LOCK - ocupat), are două
posibilităţi de principiu:

a) Să rămână în bucla de testare a semaforului, până când acesta devine '0'


(UNLOCK). Această strategie se numește "spin lock".
b) Să abandoneze intrarea în respectivul proces, care va fi pus într-o stare
de aşteptare, şi să iniţieze un alt proces disponibil – practic se realizează
o comutare de task-uri, în vederea mascării latețelor de așteptare la
semafor.

Strategia a), deşi des utilizată, poate prelungi mult alocarea unui proces de
către un anumit procesor. Pentru a dealoca un proces (variabila aferentă),
procesorul respectiv trebuie să scrie pe '0' semaforul asociat. Este posibil ca
această dealocare să fie întârziată – în mod ironic! – datorită faptului că
simultan, alte N procesoare doresc să testeze semaforul, în vederea alocării
resursei (prin bucle de tip Read - Modify - Write). Pentru evitarea acestei
deficienţe este necesar ca o cerere de bus de tip "Write" să se cableze ca fiind
mai prioritară decât o cerere de bus în vederea unei operaţii de tipul "Read -
Modify - Write". Altfel spus, dealocarea unei resurse este implementată în
hardware ca fiind mai prioritară decât testarea semaforului, în vederea alocării

435
resursei. Strategia b) prezintă deficienţe legate, în special, de timpii mari
determinaţi de dealocarea/alocarea proceselor (salvări/restaurări de contexte).
În ipoteza că în SMM nu există mecanisme de menţinere a coerenţei cache-
urilor, cea mai simplă implementare a verificării disponibilităţii unei variabile
globale este următoarea (spin lock):

li R2, #1; R2 ← ‘1’


test: lock exchg R2, 0(R1); atomică
bnez R2, test

Dacă însă ar exista mecanisme de coerenţă a cache-urilor, semaforul ar


putea fi ataşat local. În acest caz, primul avantaj ar consta în faptul că testarea
semaforului ('0' sau '1') s-ar face din cache-ul local, fără să mai fie necesară
accesarea busului comun. Al 2-lea avantaj - de natură statistică - se bazează pe
faptul dovedit, că este probabil ca într-un viitor apropiat procesorul respectiv să
dorească să testeze din nou semaforul (în virtutea principiilor statistice de
localitate spaţială şi temporală).
În vederea obţinerii primului avantaj, bucla anterioară trebuie modificată.
Fiecare instrucțiune de tip "exchg" implică o operaţie (ciclu) de tip "Write".
Cum secvenţa anterioară de testare a semaforului poate fi executată în paralel de
către mai multe procesoare, se pot genera concurenţial mai mulţi cicli (cereri) de
tip "Write". Cele mai multe asemenea cereri vor conduce la miss-uri, întrucât
fiecare procesor încearcă să obţină semaforul într-o stare "exclusivă".
Aşadar, bucla se va modifica în acest caz ca mai jos:

test: lw R2,0(R1) ; testare pe copia locală


bnez R2, test ; a semaforului.
li R2, #1 ; setare concurenţială a
lock exchg R2,0(R1) ; semaforului de către
bnez R2,test ; procesoare (un singur câştigător).

Să studiem acum implicaţiile acestei secvenţe de program într-un SMM cu


trei procesoare P0, P1, P2, implementând un protocol de coerenţă a cache-urilor
de tip WI şi un protocol de scriere în cache de tip "Write Back".

436
Pas Procesor Procesor P1 Procesor Stare Activitate pe
P0 P2 semafor BUS-ul
comun
Are Sem Testare Testare
1 = 1 Sem=0? Sem=0? Partajat -
(LOCK) NU! NU!
pus chiar
de el
Termină Recepţionea Recepţion Write
2 proces şi ză ează Exclusiv invalidate
pune Sem invalidare în invalidare pentru “Sem”
= 0 în cache în cache de la P0
cache
3 - Read miss Read miss Partajat
Arbitrul îl
serveşte pe
P2;
Write back
de la P0
4 - WAIT Sem = 0 Partajat Read miss-ul
(acces la pentru P2 e
bus) satisfăcut
5 - Sem = 0 Execută Partajat Read miss-ul
“exchg” ⇒ pentru P1 e
cache miss satisfăcut
6 - Execută Terminare Exclusiv P2 servit;
“exchg” ⇒ “exchg”. Write
cache miss Primeşte invalidate
‘0’, scrie “Sem”
Sem = 1

437
7 - Terminare Intră în Partajat P1 servit
“exchg”. secţiunea
Primeşte ‘1’ critică de
⇒ LOCK! program
8 - Testează în
prima buclă - - -
dacă
“Sem” = 0?
Conlucrarea a trei procese într-un SMM

Pentru a minimiza traficul pe busul comun, introdus de către instrucţiunea


"exchg", secvenţa anterioară se poate rafina ca mai jos:

test: ll R2,O(R1)
bnez R2,test
li R2,#1
sc R2,O(R1) ;un singur Pk o va executa cu
begz R2,test ; succes, restul, nu ⇒ scade traficul pe bus.

Sincronizarea la barieră

Este o tehnică de sincronizare deosebit de utilizată în programele cu bucle


paralele. O barieră forţează toate procesele să aştepte, până când toate au atins
bariera, abia apoi permiţându-se continuarea acestor procese. O implementare
tipică a unei bariere poate fi realizată prin două bucle succesive: una atomică, în
vederea incrementării unui contor, sincron cu fiecare proces care ajunge la
barieră, iar cealaltă în vederea menţinerii în aşteptare a proceselor, până când se
îndeplineşte o anumită condiţie (test); se va folosi funcţia "spin (cond)" pentru a
indica acest fapt.
În continuare se prezintă implementarea tipică a unei bariere:

LOCK(counterlock)
Proces if(count==0) release=0 ; /*şterge release la început*/

438
atomic count=count+1 ; /*contorizează procesul ajuns
la barieră.*/
UNLOCK(counterlogic)
if(count==total)
{ /*toate procesele ajunse la barieră!*/
count=0;
release=1;
}
else
{ /*mai sunt procese de ajuns*/
spin(release=1); /*aşteaptă până când ajunge şi ultimul*/
}

“total” – nr. maxim al proceselor ce trebuie să atingă bariera


“release” – utilizat pentru menţinerea în aşteptare a proceselor la barieră

Există totuşi posibilitatea, spre exemplu, ca un procesor (proces / thread) să


părăsească bariera înaintea celorlalte care ar sta în bucla "spin (release=1)" şi ar
rezulta o comutare de task-uri chiar în acest moment. La revenire vor vedea
"release=0", pentru că procesul care a ieşit anterior, a intrat din nou în barieră.
Rezultă deci o blocare nedorită a proceselor în testarea "spin".
Soluţia în vederea eliminării acestui hazard constă în utilizarea unei
variabile private, asociate procesului (local_sense). Bariera anterioară devine
acum:

local_sense=!local_sense;
LOCK(counterlock);
count++;
UNLOCK(counterlock);
if(count==total)
{
count=0;
release=local_sense;
}

439
else
{
spin(release=local_sense);
}

Dacă un proces iese din barieră, urmând ca mai apoi să intre într-o nouă
instanţă a barierei, în timp ce celelalte procese sunt încă în barieră (prima
instanţă), acesta nu va bloca celelalte procese întrucât el nu resetează variabila
"release", ca în implementarea anterioară a barierei.

Obsrvație: Din punct de vedere al programatorului secvenţial, bariera


anterioară este corectă.

Direcții de dezvoltare actuale în sistemele multicore /manycore

În continuare vom prezenta, pe baza lucrării autorului [Vin09], cu revizuiri


și anumite adăugiri în această versiune, câteva dintre direcțiile de dezvoltare mai
importante ale domeniului sistemelor multicore și manycore moderne, așa cum
sunt văzute acestea de autorul lucrării.
Arhitecturile de procesoare de tip single-core, care să exploateze în mod
agresiv paralelismul la nivel de instrucţiuni prin execuţii speculative de tip out of
order, trebuie cercetate şi dezvoltate în continuare, deşi frecvenţa tactului nu mai
poate creşte prea mult, din cauza consumului de putere statică și dinamică
(Pd=kCV2f) şi a disipaţiei termice tot mai dificile. La actualele frecvenţe de tact
de câţiva GHz în cadrul sistemelor de largă folosință, densităţile de putere din
chip-uri sunt enorme, de câteva sute de W/cm2. Supercalculatoarele actuale, cu
zeci de mii de procesoare, consumă puteri electrice de ordinul MWatt. Totodată,
la o frecvenţă de tact de 14 GHz (perioadă 70 ps), întârzierea semnalului pe 5
mm de conductor devine enormă (390 ps). Evident că, în aceste condiţii,
creşterea frecvenţei de tact practic nu mai este posibilă, performanţa putând
creşte doar prin inovaţii arhitecturale. Provocarea esenţială aici constă în
determinarea compromisului optimal între performanţa procesării (Instructions
Per Cycle – IPC) şi complexitatea arhitecturală (puterea consumată, disipaţia
termică, aria de integrare şi bugetul de tranzistori etc.) Astfel, metrici de

440
evaluare de tip MIPS/W (Million Instructions per Second per Watt), MIPS per
area of silicon, Energy per Instruction (Energy Delay Product=E/IPC2) etc. sunt
tot mai frecvent utilizate. Reamintim faptul că energia electrică [Joule=Watt*s]
T
este E (T ) = ∫ P(t )dt , unde P(t) este puterea electrică momentană. Aceste
0

microarhitecturi monoprocesor vor exploata paralelismul la nivel fin (ILP) din


cadrul aplicaţiilor cu secvenţialitate intrinsecă (scrise în limbaje secvenţiale). În
acest sens, pot fi avute în vedere inclusiv metode şi tehnici de compilare
adaptivă sau incrementală a codului obiect, bazate pe informaţii de tip profilings
(tipul algoritmilor, gradul de utilizare al resurselor hardware, nivelul şi
granularitatea paralelismelor la nivelul thread-urilor etc.) Aceste metode pot
determina adaptarea sau reconfigurarea microarhitecturii la cerinţele aplicaţiei,
în vederea maximizării performanței şi minimizării puterii consumate. Şi totuşi,
metodele de exploatare a ILP-ului (Instruction Level Parallelism) au ajuns la o
oarecare saturaţie – ILP Wall (superpipeline cu frecvenţe mari de tact,
procesarea out of order, branch prediction, trace-cache, procesări speculative,
metode de scheduling static, metode de eliminare a memory-wall etc.)
În concordanță cu viziunea rețelei de excelență europene în domeniul
sistemelor de calcul numită HiPEAC (v. https://www.hipeac.net/), aplicațiile
actuale sunt caracterizate de următoarele aspecte:
• Sunt computațional-intensive, indiferent că sunt aplicații dedicate, mobile,
aparținînd unor centre de date (cloud) etc.
• Sunt interconectate cu alte sisteme (prin internet, internet of things etc.)
• Sunt parte a mediului fizic, pe care, deseori, îl și controlează (cyber-
physical systems).
• Sunt inteligente, cognitive, adaptive, capabile să înțeleagă semnificația
datelor din mediu, chiar dacă acestea sunt incomplete, afectate de zgomot
etc.

Aceste aplicații trebuie să fie fiabile, robuste, cu un consum suficient de


redus de energie electrică, sigure și, desigur, performante.

Sisteme multicore şi manycore

441
Soluţia cea mai frecventă la ora actuală pentru creşterea performanţei şi
evitarea limitărilor anterior schiţate, constă însă în dezvoltarea de arhitecturi
multicore şi manycore. Deşi fac mai dificilă activitatea de programare, acestea
oferă o rată de performanţă/Watt mai bună decât sistemele monoprocesor, la o
performanţă similară. În plus, aceste sisteme, care exploatează prin program,
deci în mod explicit, paralelismul thread-urilor, au şanse mai mari decât
sistemele monoprocesor ca să mai micşoreze din prăpastia de comunicație între
microprocesor şi memorie (DRAM). Cercetarea în acest domeniu este extrem de
necesară, având în vedere faptul că se aşteaptă în viitorul imediat
microprocesoare comerciale de uz general de 256 de nuclee. Se consideră că
sistemele multicore (eterogene) vor deveni dispozitivul universal de calcul. Se
speră că aceste sisteme vor putea corela eficienţa procesării cu creşterea
densităţii de integrare, care evoluează conform legii lui Moore. Compania Intel a
fabricat deja (la nivelul anului 2011) un chip cu 80 de core-uri integrate, v.
http://techfreep.com/intel-80-cores-by-2011.htm, pe care îl va lansa în producţia
de masă. Procesoarele grafice Nvidia Tesla C1060, cu 240 de nuclee integrate în
chip, oferă performanţe de până la un Teraflop/s. Procesorul multicore
Sony/Toshiba/IBM Cell, cu 9 nuclee neomogene integrate per chip, atinge rate
de procesare de 200 Gflop/s. Programarea şi utilizarea eficientă în asemenea
cazuri, nu vor fi posibile până când nu vor avea loc schimbări radicale în
modelele de programare, dar şi în instrumentele software disponibile (mai ales
depanatoare de program).
Metricile succesului în cadrul dezvoltării sistemelor multicore se referă în
principal la productivitatea programării, dar şi la performanţa aplicaţiei.
Performanţa trebuie să aibă în vedere minimizarea acceselor la datele aflate în
afara memoriilor locale (prin managementul “localităţii” datelor), optimizarea
balansării încărcării procesoarelor (Load Balance) şi respectiv optimizarea
comunicaţiilor şi sincronizărilor. În acest scop, se impune o nouă proiectare a
algoritmilor, în vederea mapării lor optimale pe sistemele de tip multicore.

Arhitecturi multicore omogene vs. eterogene

Nu putem fi de acord în totalitate cu opinia specialiştilor de la


Universitatea din Berkeley care afirmau într-un cunoscut articol colectiv

442
(Asanovic K. et al., The Landscape of Parallel Computing Research: A View
from Berkeley, Technical Report No. UCB/EECS-2006-183, December 2006
[Asa06]) că nucleele viitoarelor multiprocesoare vor consta în procesoare
simple, omogene. În acest caz, paralelismul la nivel de instrucţiuni, (singurul)
exploatabil la nivelul programelor secvenţiale, ar fi mult diminuat. Acest
dezavantaj este inacceptabil, având în vedere că peste 95% din programele scrise
până acum, au fost scrise în limbaje secvenţiale. În continuare, programarea
secvențială va fi majoritară. Din acest motiv credem că viitorul, în calculul de uz
general, este cel al unor sisteme multicore heterogene (câteva nuclee
superscalare out of order cu execuţii speculative + mai multe nuclee
superscalare mai simple, de tip in order, cu structuri pipeline scurte, de 5-9
stagii). Heterogenitatea permite adaptarea dinamică la diferitele caracteristici ale
programelor rulate. Programele actuale sunt ele însele heterogene, având diferite
tipuri de porțiuni de cod. Acest fapt este agreat de multe cercetări recente şi este
justificabil în baza unui exemplu pe care l-am prezentat deja în această lucrare.
Totuşi, superioritatea sistemelor neomogene faţă de sistemele omogene,
va trebui dovedită mult mai convingător, prin simulări complexe, atât pe
benchmark-uri secvenţiale cât şi pe altele concurente, luând în considerare mai
multe obiective (IPC, energie consumată, buget tranzistori, arie integrare etc.)
De altfel, chiar şi actualele multiprocesoare comerciale sunt neomogene, având
de obicei un nucleu superscalar out of order foarte puternic (IBM Cell BE, Intel
IXP – procesoare de reţea etc.) şi totuşi, la nivelul unor manycore – uri
heterogene, modelele de programare ar putea deveni foarte complicate.
Virtualizarea va juca un rol important, atât în vederea portabilităţii aplicaţiilor
care vor rula pe platforme hardware dedicate tot mai diverse, cât şi în vederea
evidenţierii concurenţelor, mai facilă la nivelul maşinii virtuale, care poate
beneficia de meta-informaţii derivate din codul HLL (High Level Language).
Având în vedere că există deja sisteme multiprocesor cu foarte multe
procesoare, dintre care unele sunt neutilizate de către o aplicație dată (iar această
tendință va continua în mod ascendent), se pune problema virtualizării acestora.
Astfel, multiprocesoarele sau nucleele componente se pot partaja din punct de
vedere logic sau virtualiza în clustere de nuclee/multicore-uri etc., permițând
astfel utilizarea unor sisteme multiprocesor virtuale [Smi05]. În schimb, în multe

443
aplicaţii din calculul dedicat (aplicaţii grafice, aplicaţii numerice etc.) este foarte
probabil ca sistemele multicore omogene să aibă succes.
Sistemele multicore dedicate (implementate sub forma Multiprocessor
Systems-on-Chips) se regăsesc frecvent în electronica domestică și în cea
industrială. Mai concret, acestea echipează telefoanele mobile (procesare de
semnal, implementarea unor protocoale de comunicație, procesare video etc. –
toate acestea la nivel low power), sistemele de telecomunicații și rețele,
sistemele de electronică medicală, televiziunea digitală (decodificări audio-video
în timp real), sistemele de jocuri video (procesări complexe în timp real) etc.
[Jer05].
De remarcat și tendința actuală de a oferi în mod gratuit microprocesoare
hardware de dezvoltare, extrem de puternice – numite hardware open CPUs.
Acestea se bazează pe faptul că unele companii producătoare (ARM – 64 bit 16
core computer, IBM – Open Power, MIPS etc.) și-au făcut publice codurile
VHDL aferente acestor procesoare. Prin urmare, procesoarele pot fi create în
mod facil în structuri logice de tip FPGA. Și mediile academice oferă procesoare
puternice, în mod gratuit. Spre exemplu, Universitatea Berkeley din California,
SUA oferă microprocesorul RISC-V – v. spre exemplu https://riscv.org/ sau
http://www.pulp-platform.org/. Tot în lumea microprocesoarelor moderne, de
această dată dedicate unor aplicații de tip audio-video, se situează și eforturile
IBM de a crea microprocesorul neuronal numit SyNAPSE, având 1 milion de
neuroni artificiali și 256 de milioane de legături sinaptice. Procesorul conține
cca. 5,5 miliarde de tranzistori integrați, care implementează, desigur, o
arhitectură neconvențională (non von Neumann). Un alt proiect similar, numit
SpiNNaker, condus de Steve Furber, dezvoltă un sistem de calcul masiv paralel
pentru înțelegerea funcționalității creierului uman. Se urmărește simularea unui
miliard de neuroni simpli, interconectați – v.
http://apt.cs.manchester.ac.uk/projects/SpiNNaker/.

Exploatarea sinergică a tipurilor de paralelism

Cercetările viitoare trebuie să vizeze şi exploatarea sinergică a diverselor


tipuri de paralelism (de tip pipeline, prin suprapunerea execuţiei fazelor
instrucţiunilor mașină, ILP, TLP – Thread Level Parallelism, ori chiar la nivelul

444
unor task-uri independente). Extensiile de paralelizare ale limbajelor de
programare (spre exemplu, OpenCL, Grandcentral etc.) vor ajuta aplicaţiile să
beneficieze de toate resursele disponibile (CPU, multi-cores, GPU etc.)
Nici paralelismul la nivelul datelor (bit, cuvânt – arhitecturi vectoriale de
tip SIMD) nu trebuie neglijat. Spre exemplu, modelul de programare OpenMP
permite exploatarea paralelismelor la nivel de task-uri, bucle şi date, deopotrivă.
Aplicaţiile de tip web, mobile (cloud computing), Data Mining, Big Data sau
bazele de date sunt caracterizate de paralelism la nivel de tranzacții, fire sau la
nivelul acceselor la memorie şi mai puţin la nivel fin (ILP). În asemenea cazuri,
fiecare client poate fi servit de către un nucleu separat. Aceste aplicaţii necesită
multe procesoare simple, cu memorii performante de mare capacitate.

Ierarhia de memorie

Se arată în literatura de specialitate că ierarhia de cache-uri este departe de


a fi optimal proiectată. Circa 50% din blocurile din cache sunt “moarte” (analog,
cca. 50 % din datele dintr-o pagină sunt nefolosite), aceasta însemnând că după
alocare, nu mai sunt utilizate, până la evacuare. Sub-sistemul de cache-uri nu
mai trebuie văzut ca un modul fix, pe care software-ul trebuie să se plieze. Acest
sistem trebuie să devină mai maleabil, adaptat la cerinţele rulării dinamice,
adaptive, a aplicaţiilor. Sunt necesare cercetări în vederea optimizării ierarhiei
de memorii cache în sistemele multiprocesor, prin exploatarea agresivă a
“localităţilor” (vecinătăţilor) spaţiale şi temporale ale
datelor/instrucțiunilor/thread-urilor. Astfel, spre exemplu, se au în vedere
mecanisme hardware care adaptează dimensiunea logică a blocului din L2
cache la caracteristicile aplicaţiei. Accesarea L2 cache se poate face în paralel
cu accesarea unor aşa numite cache-uri de monitorizare (Observation Cache -
OC), fiecare având lungimi diferite ale blocului. Cache-ul OC, care contorizează
pe un anumit interval de timp un număr minim de miss-uri, determină blocul
optimal din punct de vedere al dimensiunii, necesar a fi adoptat în L2 cache. Un
bloc de o asemenea dimensiune exploatează cel mai bine vecinătatea spaţială a
datelor şi a instrucţiunilor. Un controller special de întreruperi ar putea
monitoriza periodic starea cache-urilor OC si, în consecinţă, modifica în mod
corespunzător lungimea blocului logic din L2 cache. Mecanismul este unul

445
simplu, eficient şi robust, care nu implică intervenţia compilatorului ori a
aplicaţiei software. De asemenea, sunt necesare cercetări care să dezvolte noi
scheme de protocoale de coerenţă a cache-urilor în sistemele MIMD, puternic
scalabile, flexibile, reconfigurabile chiar, care să suporte sute şi chiar mii de
nuclee de procesare integrate pe un singur chip de uz general. Există studii
empirice care arată că la o tehnologie de integrare de 30 nm se pot integra 1000-
1500 de nuclee simple pe o singura pastilă de siliciu (în anul 2014 compania
Intel utiliza tehnologii electronice la 45 nm). Tehnicile de coerenţă actuale de tip
snooping (MSI, MESI, MOESI etc.) precum şi cele de tip directory-based sunt
depăşite din acest punct de vedere, fiind nescalabile la nivelul sutelor şi miilor
de nuclee integrabile pe un chip, după cum am mai menționat. Totuşi, cercetări
incrementale utile, bazate în principal pe predicţie şi speculaţie, ar putea
perfecţiona chiar şi aceste protocoale. Tehnicile de coerenţă vor trebui să aibă în
vedere noile caracteristici specifice sistemelor multicore (latenţele miss-urilor
cache to cache, lărgimea de bandă a bus-urilor de interconectare etc., care sunt
mult diferite, în sens favorabil, decât cele aferente sistemelor multiprocesor
clasice, implementate pe cipuri sau plăci separate.) În consecinţă, se pare că
viitorul aparţine sistemelor multicore cu memorie partajată distribuită
(Distributed Shared Memory sau Non Uniform Memory Architectures) cu
protocoale de coerenţă hardware-software sau sistemelor de tip Message
Passing (NoC – Networks on a Chip) şi implementate în tehnologii de integrare
de tip 3 D. În optimizarea mecanismelor de coerenţă trebuie ţinut cont în mod
deosebit de minimizarea puterii consumate.

Coerenţa şi consistenţa

După cum am mai subliniat în paragraful destinat sistemelor multiprocesor,


un program paralel trebuie să specifice, printre altele, ordinea operaţiilor de
scriere-citire. Cel mai simplu ar fi să se păstreze ordinea impusă de dependenţele
de date din program. Când ordinea implicită a acestor operaţii nu este
determinată în mod unic, sunt necesare operaţii explicite de sincronizare între
fire (creare/unificare fire, excludere mutuală, alocare memorie partajată, bariere,
semafoare etc.) Este necesară dezvoltarea unor noi modele, mai relaxate decât
cel al consistenţei secvenţiale (release consistency), de asigurare a consistenţei

446
memoriei partajate. Unele cercetări alternative se focalizează pe anumite
mecanisme de prefetch a datelor, implementate în cadrul mecanismului simplu
al consistenţei secvenţiale, încercând astfel să reducă degradarea de performanţă
pe care acest mecanism o implică prin secvenţialitatea acceselor la memoria de
date.

Reţele de interconectare

Sunt necesare reţele de interconectare de lărgime de bandă ridicată și


latență redusă, în cadrul unor sisteme cu memorie partajată on-chip, scalabilă.
Actualmente, reţelele de interconectare (on-chip) au lărgimi de bandă relativ
modeste, fiind limitate de capacităţile parazite, datorate modulelor
interconectate, întârzierilor de arbitrare etc. În plus, ele sunt nescalabile la
nivelul sutelor ori miilor de nuclee integrate. O excepţie pozitivă o constituie
cazul HyperTransport, o reţea de interconectare de tip point to point (cu
comutare de pachete), bidirecţională, serial/paralelă, scalabilă, de latenţă mică şi
de lărgime de bandă ridicată, v. http://www.hypertransport.org. Ea permite
interconectarea procesoarelor, a acestora cu interfeţele de I/O şi cu diferitele
acceleratoare. Compania AMD utilizează această reţea flexibilă în multe dintre
multicore-urile sale (Athlon 64, Athlon 64 X2, Athlon 64 FX, Opteron, Sempron
şi Phenom). Rețeaua HyperTransport oferă legături distincte între procesor –
memorie şi respectiv între procesor și sub-sistemul de I/O. Mai mult, ea oferă
legături fizice distincte pentru citiri şi scrieri din/în spaţiul de I/O, oferind deci
paralelizări semnificative ale acestora. La ora actuală reţeaua oferă rate de
transfer de până la 10400 MB/s (în versiunea a 3-a).
În domeniul reţelelor de interconectare implementate on-chip, cercetările
vor avea în vedere, în special, arhitecturile switch-urilor, topologiile şi, nu în
ultimul rând, algoritmii de rutare. Tehnologia VLSI utilizată influenţează în mod
direct soluţiile arhitecturale. Probabil că cercetările se vor inspira din reţelele de
interconectare ale supercomputerelor actuale (spre exemplu, supersistemul IBM
BlueGene, care conţine 65536 de noduri, clusterizate în câte 80 CPU 2-SMT
PIM - Processing în Memory, conectate Crossbar/cluster. Aceste clustere sunt
interconectate prin intermediul unei reţele de tipul 3 D Torus Network care face
ca fiecare nod să poată comunica direct cu alţi 6 vecini situaţi pe axele

447
ortogonale XYZ. Aici şi subsistemul ierarhic de cache-uri va juca un rol extrem
de important, fiind necesară îmbunătăţirea sa prin noi idei. De remarcat că
eficientizarea acestui subsistem va conduce la scăderea presiunii asupra reţelelor
de interconectare şi a memoriei principale partajate. Detecţia şi abortarea
execuţiei instrucţiunilor Store care doresc să scrie o valoare deja existentă în
memoria partajată, numite Silent Stores, ajută de asemenea, în mod semnificativ,
la reducerea presiunii asupra reţelei de interconectare. Cercetări novatoare în
arhitectura memoriilor DRAM sunt foarte necesare, având în vedere că un chip
DRAM de 512 Mbit, spre exemplu, conţine sute de blocuri fizice independente,
oferind deci un potenţial uriaş în creşterea lărgimii de bandă, prin accesări
întreţesute. Integrarea on-chip a memoriilor DRAM nu mai presupune
multiplexarea adreselor (necesară în memoriile off-chip datorită costurilor mari
ale terminalelor). Acest fapt conduce la scăderea semnificativă a timpului de
acces. De altfel, sistemele masiv paralele se bazează pe conceptul de PIM. În
acest caz, memoria DRAM este integrată în cadru procesorului, cu mari
beneficii asupra latenţei şi lărgimii de banda procesor-memorie. O reţea de
procesoare PIM se numeşte și arhitectură celulară. În arhitecturile celulare se pot
conecta milioane de procesoare, fiecare procesor fiind conectat doar la câţiva
vecini din cadrul topologiei de interconectare. Sistemele multicore au accentuat
şi mai mult gap-ul între CPU şi sistemul secundar de memorie (discuri),
conducând la scăderea lărgimii de bandă per core. Este deci imperios necesar un
sistem de I/O mult mai rapid, bazat pe abilitatea de a mapa în mod eficient un
număr mare de operaţii concurente de I/O, pe sutele de unităţi de stocare. În
acest scop trebuie aduse îmbunătăţiri aplicaţiilor, maşinii virtuale, sistemului de
operare şi controlerului dedicat al unităţilor de disc.

Modele de programare paralelă

În următoarea decadă, în cadrul aplicaţiilor software vor fi deosebit de


pregnante următoarele tendinţe: accesul ubicuu (de oriunde), servicii
personalizate şi delocalizate, sisteme masive de procesare a datelor disponible
prin cloud (un model prin care puterea de calcul este abstractizată sub forma
unui serviciu virtual peste o rețea de calculatoare – internet), realitate virtuală de
înaltă calitate, senzori inteligenţi legați în rețele wireless etc. Aceste tendinţe se

448
vor manifesta în aplicaţii concrete, precum cele legate de roboţi domestici,
vehicule auto-pilotate, teleprezenţă, jocuri, implanturi şi extensii ale corpului
uman (human++), gestiunea dronelor etc. Pentru programarea acestor aplicaţii
pe sistemele multicore şi manycore sunt necesare modele eficiente, performante
dar şi simple (!), comprehensibile, de programare paralelă. Cercetările în acest
domeniu trebuie să investigheze metodele de evidenţiere a concurenţelor la
nivelul limbajelor de programare. În general, este de dorit ca aceste modele să
fie independente de numărul de procesoare din sistem, pentru a asigura
portabilitatea. Paralelismul reprezintă rezultatul exploatării concurenţei pe o
platformă hardware paralelă. Rolul modelului de programare este acela de a
exprima concurenţa, într-un mod independent de platformă. Este sarcina
compilatorului, a sistemului de operare şi a maşinii hardware să decidă cum să
exploateze concurenţa, prin procesări paralele. Gradul de abstractizare al
modelului de programare trebuie ales prin prisma compromisului optimal între
productivitatea şi eficienţa programării paralele. Aceste modele trebuie să pună
în evidenţa paralelismele inter-thread-uri, alocările de memorie, accesul la
zonele de date partajate şi modurile de sincronizare. Ingineria programării se
focalizează în continuare pe corectitudinea şi pe reutilizarea codului, pe
productivitatea dezvoltării, dar nu şi pe exploatarea paralelismelor. De aceea,
sistemele multiprocesor sunt încă sub-utilizate în mediul industrial, în contextul
programării concurente. Se estimează că în viitorul apropiat, circa 10% dintre
programatori vor dezvolta programe paralele în mod explicit. Actualmente
aceste modele de programare paralelă sunt nesatisfăcătoare, conducând la o
programare, testare şi depanare extrem de dificile. Spre exemplu, standardul
Posix (p-threads, POSIX Threads API) este considerat ca fiind puţin flexibil şi
de nivel jos. Standardul impus prin biblioteca OpenMP nu exploatează suficient
“localitatea” datelor. De asemenea, modelele de programare actuale nu iau în
considerare heterogenitatea arhitecturii hardware. În plus, scalabilitatea acestor
modele este una scăzută. Productivitatea actualelor paradigme de programare
paralelă (shared memory - memorie partajată respectiv message passing – rețea
cu memorie distribuită logic) este una scăzută. Astfel, spre exemplu, metodele
actuale de sincronizare, bazate pe secţiuni critice atomice (excluziune mutuală
prin lock/unlock), nu mai sunt fezabile, fiind nevoie de metode noi, mai
productive.

449
In acest sens, conceptul de memorie tranzacţională (Transactional
Memory - TM) pare a fi unul promiţător, deşi cercetările sunt încă într-un stadiu
relativ incipient. Tranzacţia constituie o secvenţă de cod care se execută atomic,
în mod speculativ, prin mai multe citiri şi/sau scrieri la nivelul unei memorii
partajate. În acest caz, rularea programului nu ţine cont de secţiunile critice.
Dacă apar conflicte la nivelul variabilelor partajate accesate de fire multiple,
rezidente pe diferite procesoare, aceste conflicte se vor detecta şi firul violat îşi
va relua execuţia tranzacţiei în mod corespunzător (prin roll-backs). Aşadar
gestiunea coerenţei nu se mai face la nivelul fiecărei scrieri aferente unei
variabile partajate, ci la nivelul unor pachete atomice, fiecare pachet conţinând
mai multe astfel de scrieri. Tranzacţia este atomică (se execută din punct de
vedere logic în totalitate sau deloc), consistentă (din punct de vedere al
variabilelor partajate inter-tranzacţii) şi durabilă (odată începută, nu mai poate fi
abortată). TM simplifică tehnicile de excluziune mutuală din programarea
paralelă. Avantajul principal al conceptului de TM nu o constituie atât
performanţa rulării, cât corectitudinea acesteia, chiar şi în condiţiile în care
programatorul (compilatorul) efectuează în mod sub-optimal paralelizarea
aplicaţiei. Productivitatea şi facilizarea programării constituie alte obiective
importante, asociate acestui concept. Probabil că cercetările în domeniul TM
trebuie să abordeze scheme hibride, de tip hardware-software. Aceste cercetări
trebuie dezvoltate în paralel cu extensia şi optimizarea setului de instrucţiuni
maşină (ISA – Instruction Set Architecture) şi a interfeţei hardware-software, în
vederea facilizării programării paralele.
Unii cercetători de la Universitatea din Stanford au propus un nou model
de memorie partajată, numit Transactional memory Coherence and Consistency
(TCC), practic o memorie tranzacţională implementată în hardware. Aici,
tranzacţiile atomice sunt întotdeauna unităţile de bază ale procesării paralele.
TCC trebuie să grupeze, în hardware, toate scrierile dintr-o tranzacţie, într-un
singur pachet. Acest pachet se trimite în mod atomic la memoria partajată, iar
scrierile variabilelor partajate se efectuează la finele execuţiei tranzacţiei
(commit). Se controlează printr-un așa numit hardware roll-back tranzacţiile
procesate în mod speculativ. Aceste tranzacţii speculative necesită roll-back
atunci când mai multe procesoare încearcă să citească şi să scrie, în mod
simultan, aceeaşi dată, producând inconsistențe ale acesteia. Protocoalele de

450
coerenţă de tip snoopy se implementează la nivelul acestor tranzacţii atomice şi
nu la nivelul scrierilor individuale. Ele permit detecţia faptului că tranzacţia
curentă a utilizat date care au fost deja modificate de către o altă tranzacţie
(dependence violation), şi deci, este necesar roll-back-ul tranzacţiei în curs. În
consecinţă, consistenţa secvenţială se implementează la nivelul tranzacţiilor,
care se vor termina în ordinea secvenţială a programului originar şi nu la nivelul
scrierilor individuale. Întreţeserea între scrierile diferitelor procese este permisă
numai la nivelul tranzacţiilor. Acest model complex, numit TCC, impune la
nivelul programatorului să se insereze în mod explicit tranzacţiile în codul sursă,
ca reprezentând nişte regiuni paralelizabile. Aceste tranzacţii pot fi rafinate
iterativ şi adaptiv, în urma diferitelor rulări ale programului. Evident că o
anumita tranzacţie nu poate separa un Load de un Store succesiv, care accesează
aceeaşi variabilă partajată ca şi Load-ul.
O altă provocare tehnică, extrem de importanta, o constituie dezvoltarea
unui model de programare care să permită utilizarea transparentă şi simultană
atât a modelului cu memorie partajată (avantaj: comunicarea implicită), cât şi a
celui cu memorie distribuită (avantaj: scalabilitatea hardware). Câteva încercări
în acest sens se regăsesc în limbajele Co-Array Fortran, UPC, X10, Fortress,
Chapel etc. De asemenea, sunt necesare mecanisme novatoare care să permită
compilatorului şi sistemului run-time să optimizeze structurile de date partajate,
adaptându-le la condiţiile execuţiei. Ideea de esenţa este că o structura de date
partajată să fie distribuită automat în sistem. Cercetările în domeniul
arhitecturilor reconfigurabile (FPGA), în special în lumea embedded, sunt şi ele
extrem de necesare întrucât aceste arhitecturi se pot adapta în mod static sau/şi
dinamic mai bine la cerinţele aplicaţiei specifice. Avantajul limbajului Java în
arhitecturile multiprocesor constă în faptul că implementează în mod nativ
conceptul de fir de execuţie. Din acest motiv, cercetările în domeniul sistemelor
multicore care utilizează procesoare capabile să execute direct în hardware
bytecode-urile Java, sunt deosebit de utile, în special în lumea sistemelor
dedicate. Acestea au și avantajul portabilității întrucât prin tehnologia de
compilare Just-In-Time se translatează codul, din limbajul intermediar al mașinii
virtuale Java (bytecode), în codul obiect al CPU gazdă, chiar în timpul execuției.
Este necesar suport hardware pentru programarea paralelă, inclusiv pentru
depanarea programelor paralele prin monitorizarea execuţiilor. Depanarea unor

451
sisteme multi-core cu sute de thread-uri procesate în limbaje native diferite, de
către procesoare neomogene, este o problemă deschisă, de mare actualitate şi
interes. O singură sesiune a depanării ar trebui să vizualizeze toate instrucţiunile
maşină, informaţii legate de variabile şi funcţii, punerea în evidență a erorilor de
comunicare între module dar şi a erorilor locale etc. Evitarea violărilor de timing
în cazul sistemelor în timp real având constrângeri tari (Worst Case Execuţion
Time) şi a erorilor de execuţie având cauze incerte (Heisenbugs), constituie
provocări majore. Fără ajutorul hardware-ului, care să ofere o viziune globală a
stării maşinii multicore, asemenea cerinţe par dificil, dacă nu chiar imposibil de
îndeplinit. Proiectarea hardware trebuie să aibă în vedere deci şi observabilitatea
rulărilor multiple, fără a genera însă prea mari cantităţi de date nerelevante.

Paralelizarea aplicaţiilor

Paralelizarea automată a aplicațiilor software constituie un obiectiv


maximal, vizat de peste 40 de ani de cercetări asidue. În acest sens s-au obţinut
anumite realizări notabile, îndeosebi în paralelizarea automată a programelor
ştiinţifice, scrise în limbaje orientate pe vectori (Fortran, Matlab etc.) şi pentru
arhitecturi omogene cu memorie partajată, în special din lumea sistemelor
embedded. Aceste realizări trebuie extinse la tipuri cât mai diverse de aplicaţii,
la limbaje bazate pe pointeri şi la sisteme multiprocesor heterogene, cu diferite
modele de memorie. În acest scop sunt necesare, în special, tehnici statice noi de
analiză a programelor scrise în limbaje bazate pe pointeri. O altă direcţie insistă
pe limbajele orientate pe domenii, unde punerea în evidenţă a concurenţelor este
mai facilă decât în cele de uz general. Paralelizările speculative, incluzând aici
tehnici de multithreading speculativ, vor juca un rol tot mai important.
Planificarea dinamică a thread-urilor în vederea optimizării comunicaţiilor şi
sincronizărilor este deosebit de importantă. Astfel, spre exemplu, arhitectura
numită Decoupled Threaded Architecture (DTA) exploatează paralelismele fine
(ILP) şi medii (la nivel TLP) în cadrul unor sisteme many-core heterogene.
Comunicaţiile şi sincronizările între fire sunt interesante, făcându-se pe un
model de tip flux de date (data-flow) şi prin mecanisme de sincronizare de tip
non-blocking. La crearea unui fir, acestuia i se asignează un numărător de
sincronizare (SC), reprezentând numărul datelor de intrare pe care firul trebuie

452
să-l primească de la alte fire. Acest numărător este decrementat de fiecare dată
când firul primeşte o astfel de dată în memoria sa locală, destinată
comunicaţiilor inter-fire. Când SC-ul firului a ajuns la zero, se va starta execuţia
firului respectiv prin încărcarea datelor din memoria locală, în registre. De
asemenea, transmiterea de către compilator a unor informaţii de semantică a
aplicaţiei către sistemul multicore pe care aceasta va rula, ar putea conduce la
optimizări semnificative în procesarea aplicaţiei.

Simularea ca instrument de cercetare

Sunt necesare metode de simulare adecvate, în vederea stăpânirii


complexităţii cercetării-dezvoltării sistemelor multicore și manycore, inclusiv
prin metode de simulare tranzacţională (Transaction-Level Modeling – TLM),
așa precum permite, spre exemplu, mediul de dezvoltare pentru sisteme
multicore numit UniSim, v. - www.unisim.org. Astfel, metoda TLM lucrează la
un nivel de abstractizare superior celui utilizat la nivelul simulării ciclu cu ciclu.
Aici, simulările se focalizează preponderent pe comunicaţiile între modulele
componente, numite tranzacții. Este importantă simularea întregului sistem de
calcul, inclusiv a sistemului de operare, cu toate nivelurile ierarhice funcţionale
pe care acesta le deţine (full-system simulation – FSS, exemple: simulatoarele
M5, Simics, GEMS, Sniper etc.). Simulatoarele monolitice, gen SimpleScalar,
M-Sim, etc., vor fi înlocuite cu simulatoare modulare, care să exploateze
actualele tehnici ale ingineriei programării obiectuale, în vederea facilizării
scrierii programelor şi a reutilizării codului (exemple SystemC, Liberty,
MicroLib, GEMS, SimFlex, M5, UniSim sau ASIM). O idee importantă în acest
sens constă în maparea facilă, intuitivă, a blocurilor hardware pe modulele
(funcţiile, clasele) software ale simulatorului. Prin instanţierea acestor blocuri şi
specificând conexiunile lor, se poate realiza o dezvoltare ierarhizată a sistemului
multicore. Astfel, dezvoltarea proiectului va fi una mult mai facilă decât într-un
mediu monolitic de simulare. Calitatea unui simulator de sisteme multicore este
dată de caracteristici precum: posibilităţi de dezvoltare (modularitate),
benchmark-uri & compilatoare, posibilităţi de tipul full-system simulation, tipuri
de procesoare oferite în biblioteci, caracteristici multiprocesor oferite
(UMA/NUMA, protocoale de coerenţă, reţele interconectare, modele de

453
programare pe care le pune la dispoziţie, modele de consistenţă etc.), facilități de
calcul performanță/putere electrică/temperatură etc., viteză de simulare,
acurateţe de simulare, gradul de parametrizare (flexibilitatea arhitecturală) etc.
Un simulator pentru sistemele multicore care simulează întregul sistem de calcul
este Simics (Virtutech - v. http://www.virtutech.com). Acesta oferă un set larg de
procesoare (Alpha, ARM, MIPS, PowerPC, SPARC, x86-64) şi interfeţe. Simics
poate încărca şi rula sisteme de operare precum Linux, Solaris, Windows XP.
Poate virtualiza maşina ţintă în sisteme multiprocesor, clustere de procesoare
sau reţele. Un simulator pentru sistemele multicore, dezvoltat în mediul
academic american este cel numit RSim - v. http://rsim.cs.uiuc.edu.
Implementează o memorie partajată, distribuită fizic (fiecare procesor având o
memorie locală) şi protocoale de coerenţă de tip directory-based (MSI, MESI).
Oferă procesoare superscalare puternice, cu execuţii out-of-order ale
instrucțiunilor şi arhitecturi complexe de memorie (CC-NUMA), inclusiv cu
posibilităţi de adresare întreţesută. Comunicarea între procesoare se face prin
intermediul unei reţele bidimensionale, de tip plasă (mesh). De asemenea, oferă
suport pentru implementarea consistenţei secvenţiale sau chiar a unor modele
mai relaxate de consistență. Un alt simulator interesant, la nivel de ciclu maşină
(emulator de arhitectură MIPS), destinat sistemelor multicore cu un număr
configurabil de nuclee, este SESC, v. http://sesc.sourceforge.net. Acesta deţine
un modul de calcul al timing-ului de mare acurateţe. M5 este un FSS-simulator
dezvoltat în tehnologia programării pe obiecte (limbajul C++), permiţând deci
instanţierea facilă a modulelor sistemului multiprocesor. Conţine modele pentru
procesoarele Alpha, MIPS, ARM şi SPARC. Implementează ierarhii complexe
de memorii cache cu protocoale de coerenţă de tip snoopy. Încarcă sistemele de
operare Linux şi Unix (Solaris). Permite trei moduri de lucru în vederea
compromisului între viteza de simulare şi acurateţea simulării. În domeniul
sistemelor multicore dedicate aplicaţiilor multimedia, simulatorul Sesame
dezvoltat la Universitatea din Amsterdam este unul cunoscut şi apreciat.
Sniper este un simulator multi-core / many-core ISA x86, paralel, de mare
acuratețe și de mare viteză – v. http://snipersim.org/w/The_Sniper_Multi-
Core_Simulator. Este dezvoltat de compania Intel, în cooperare cu mediul
academic (Universitatea din Gent, Begia etc.) Este bazat pe infrastructura de
simulare numită Graphite, oferind posibilitatea reglării vitezei de simulare.

454
Graphite este o infrastructură de simulare distribuită, creată pentru a permite
evaluări arhitecturale de nivel înalt și dezvoltare de software pentru arhitecturile
multi-core viitoare. Oferă modelare funcțională și a performanței core-urilor,
rețelelor on-chip de interconectare precum și a sub-sistemelor de memorie, care
includ și ierarhiile de cache-uri, împreuna cu protocoalele de coerență ale
acestora. Proiectarea este una modulară, putându-se înlocui cu ușurință diferitele
modele, pentru a simula arhitecturi diverse sau pentru a face compromisuri între
performanța rulării si acuratețe. Este un simulator „multi-core pe multi-core”
proiectat astfel încât să se folosească de puterea hardware a mașinilor multi-core
existente comercial. De asemenea, oferă și posibilitatea distribuirii unei simulari
individuale pe un cluster de servere, pentru accelerarea simulării și studierea
arhitecturilor cu sute de core-uri (many-core). Astfel, se permit o serie de opțiuni
de simulare flexibile atunci când se exploreaza arhitecturi multi-core omogene și
heterogene.
Simulatorul Sniper permite realizarea de simulări rapide, pe volume de
lucru multi-program, dar și pe aplicații multi-fir, cu memorie partajată, cu 10,
până la peste 100 de core-uri. Sniper a fost validat cu sistemele comerciale Intel
Core2 si Nehalem și are o eroare medie de calcul a performanței de pâna la 25%,
cu o viteză a simularii de pana la câțiva MIPS.
În continuare, preluat din lucrarea C. R. Buduleci, 4D – Multi-Objective
Optimization of Sniper Simulator (multicore/manycore), MSc Dissertation, “L.
Blaga” University of Sibiu, 2015 (coordonator științific: Prof. Lucian Vințan),
prezentăm câteva rezultate semnificative, obținute prin rularea simulatorului
multi-core Sniper 5.3. S-au utilizat benchmark-urile concurente SPLASH-2,
simulându-se rularea a aproximativ 3,6 miliarde de instrucțiuni dinamice per
benchmark. Numărul de procesoare componente a variat de la 1 la 16 și s-au
simulat microprocesoare de tipul Intel Nehalem – Gainestown la o frecvență a
tactului de 2.66 GHz (frecvența este utilă în calculul puterii dinamice
consumate). Figura următoare (4.53.a) prezintă performanța medie a sistemului
multicore, exprimată în numărul mediu de instrucțiuni/ciclu CPU (IPC), ca
funcție de numărul de nuclee (procesoare) din sistem. În acord cu intuiția,
performanța crește odată cu creșterea numărului de procesoare, aratând astfel că
există suficientă concurență în benchmark-urile simulate. Figura 4.53.b prezintă
puterea totală medie consumată (adică suma puterilor instantanee – în fiecare

455
ciclu CPU – împărțită la numărul total de cicli procesați). Graficul este intuitiv,
această putere crescând odată cu creșterea numărului de procesoare disponibile.
Interesant însă, puterea totală medie per core scade pe măsură ce numărul de
nuclee (procesoare) crește, subliniindu-se astfel o performanță/Watt(eficiență)
acceptabilă a sistemului multicore (v. Figura 4.53.c).

Figura 4.53.a Performanța globală funcție de numărul de procesoare

Figura 4.53.b Puterea totală medie consumată

456
Figura 4.53.c Puterea totală medie per core

O altă problemă majoră constă în viteza simulărilor. La ora actuală,


simularea cu acurateţe totală a unui sistem monoprocesor real, la nivel RTL –
Register Transfer Logic, necesită în jur de o zi de simulare, pe sisteme
performante! Pentru sisteme multicore, acest timp de simulare va creşte cel puţin
într-o manieră liniară. Având în vedere complexitatea enormă a simulărilor de
tip cycle by cycle, care le face nefezabile pentru optimizarea unor sisteme cu
sute de nuclee integrate, există un mare interes, inclusiv pentru dezvoltarea unor
metode de simulare la nivel de tranzacţii sau chiar a unor metode analitice de
optimizare. Se au în vedere inclusiv metode de simulare statistică, bazate pe
eşantioane reprezentative ale procesării (sampling simulation). Ideea de esenţa
aici constă în selectarea unor subseturi ale benchmark-urilor, având un
comportament suficient de similar cu cel al întregului set de benchmark-uri. Se
pune problema extragerii caracteristicilor benchmark-urilor în urma rulării şi
construcţia unor benchmark-uri sintetice având aceleaşi caracteristici,
reprezentative şi mai scurte (statistical simulation). O altă soluţie în vederea
reducerii timpului de simulare constă în paralelizarea simulatorului şi rularea lui
pe sisteme multicore performante, precum IBM Cell BE de exemplu. O altă
soluţie în vederea reducerii timpului de simulare o constituie clasificarea /
clusterarea benchmark-urilor prin metode statistice. În fine, o altă provocare
importantă o constituie îmbunătăţirea acurateţii simulărilor prin metodologii

457
specifice. Acurateţea relativă a simulărilor poate fi mai importantă decât
acurateţea absolută a acestora, cel puțin în fazele incipiente ale proiectului.

Benchmarking

Ca metodologie de cercetare-dezvoltare se impune tot mai mult


proiectarea hardware-software integrată (hardware-software co-design). Se
pune problema dezvoltării unor metode de benchmarking adecvate evaluării
sistemelor de tip multicore. Problema este una extrem de delicată, întrucât
companiile îşi protejează atent aplicaţiile și produsele hardware comerciale.
Astfel, în locul uzitatelor benchmark-uri (SPEC – pentru procesoare de uz
general, EEMBC şi MiBench – pentru sisteme dedicate, Mediabench, ALPBench
– pentru sisteme multimedia, TPC – pentru baze de date, Livermoore, Parsec,
SPLASH 2 – pentru sisteme cu paralelism masiv etc.) s-au propus aşa numitele
dwarfs (Berkeley), constând în metode algoritmice tipice calculului paralel care
să conţină atât pattern-uri computaţionale cât şi, mai ales, pattern-uri de
comunicaţie între nucleele componente [Asa06]. Şi totuşi, problema rămâne una
deschisă, cu multe semne de întrebare (spre exemplu, benchmarking-ul trebuie
să ţină cont şi de tendinţa accentuată a virtualizării, ca şi metodă esenţială în
asigurarea compatibilităţii şi portabilităţii.)

Automatic Design Space Exploration

Se pune problema dezvoltării unor metode de căutare euristică optimizată


în spaţiul enorm al parametrilor aplicaţiilor şi arhitecturii cercetate - Automatic
Design Space Exploration (ADSE). Spre exemplu, proiectarea unui 4-core în
care fiecare nucleu poate fi ales dintr-o bibliotecă conţinând 480 de modele,
impune evaluarea a peste 2.2 miliarde de posibilităţi! Dacă evaluarea fiecărei
posibilităţi ar necesita o zi, evaluarea tuturor posibilităţilor ar necesita aproape 1
milion de ani. Un alt exemplu: un simulator CPU conține pe puțin 50 de
parametri arhitecturali (număr de nuclee, tipul rețelei de interconectare,
capacitatea cache-urilor etc.) Dacă fiecare parametru are 8 valori posibile,
rezultă un spațiu de proiectare 2150 de instanțe! În figura următoare se arată

458
multiplele posibilități de partajare a cache-urilor de nivel 1 respectiv 2, într-un
sistem de tip dual-core.

Figura 4.53.1 Partajarea cache-urilor într-un dual-core

Relativ la aplicaţii, se au în vedere transformările algoritmice şi de limbaj


ce pot creşte gradul de paralelism TLP. Scopul este evident, anume determinarea
acelor parametri care conduc la optimizarea raportului performanţă/cost (IPC,
energie consumată, arie de integrare, complexitate etc.) Optimizările au deci
obiective multiple. Evident că nu se pune problema căutării complete în spaţiul
enorm al tuturor parametrilor arhitecturii sau/şi aplicaţiilor investigate. Tehnici
din domeniile învăţării automate (machine learning), a cercetărilor operaţionale
ori data mining ar putea fi utile, în vederea reducerii spaţiului de căutare şi deci,
a reducerii numărului de simulări. Aceste metode de optimizare automată a
parametrilor arhitecturii se vor integra sub forma unor API-uri în cadrul
simulatoarelor dezvoltate. Tot aici, sunt necesare tehnici euristice, inspirate din
domeniul machine learning, în vederea optimizării incrementale a compilării.
Ideea este ca programul să se adapteze la schimbările din hardware (spre

459
exemplu, creşterea numărului de procesoare), dar şi la evenimentele dinamice
care apar pe parcursul procesării (spre exemplu miss-uri în cache). La ora
actuală, nu există un instrument ADSE universal, matur, robust, care să fie larg
folosit în optimizarea sistemelor de calcul. Obiectivele constau în creşterea
performanţei, dar şi în reducerea consumului de putere electrică (atât dinamică
cât şi statică). Evident că în acest sens sunt necesare informaţii de feedback,
captate în urma rulărilor aplicaţiei. Este necesară deci o abordare mai strânsă
între cercetările în domeniul microarhitecturilor şi cel al compilatoarelor
(compiler-architecture co-design). Compilatorul trebuie să poată manipula
infrastructura microarhitecturii, în timp ce aceasta trebuie să beneficieze de
informaţiile transmise ei prin intermediul ISA, de către compilator. Din păcate,
este foarte dificilă trecerea de la actualele compilatoare, orientate pe exploatarea
ILP, la compilatoare noi, orientate pe exploatarea TLP. Totodată, această
abordare holistică este una mare consumatoare de timp. În scopul reducerii
timpului de proiectare integrată a dualităţii arhitectură hardware-compilator, se
dezvoltă modele bazate pe învăţare automată, care pot predicţiona performanţa
unui compilator optimizat pentru o anumită arhitectură, fără ca să îl construiască
efectiv. Modelele folosesc ca intrare o parte infimă din spaţiul parametrilor
compilator-arhitectură.
Avem aşadar nevoie de tehnici inteligente şi eficiente de căutare în spaţiul
enorm de valori ale parametrilor sistemului. Adaptarea tehnicilor de căutare-
optimizare din domeniul învăţării automate a fost investigată de către mulţi
autori. Spre exemplu, s-a dezvoltat un instrument complex de accelerare a DSE
pentru arhitecturi multicore, numit Magellan. Acesta determină parametrii cvasi-
optimali, cei care maximizează rata de procesare pentru un buget prestabilit al
ariei de integrare şi puterii disipate. Magellan foloseşte algoritmi euristici de
căutare de tip hill climbing, genetici, stigmergici etc. Clasificarea soluţiilor şi a
benchmark-urilor funcţie de caracteristicile acestora, poate accelera căutarea.
Algoritmul Steepest Ascent Hill Climbing (SAHC) implică în acest caz,
căutarea în vecinătatea celui mai bun k-procesor curent. Un procesor vecin este
un procesor care diferă de cel curent, prin valoarea unui singur parametru.
Următorul procesor optimal este ales ca fiind cel mai bun vecin, dacă este
superior procesorului curent. Algoritmul se opreşte în proximul punct de extrem.
Avantajul principal al acestui algoritm simplu constă în rapiditatea convergenţei

460
sale. Dezavantajele constau în complexitatea exponenţială cu numărul de core-
uri (k) dar şi în posibilitatea eşuării în extreme locale. Evident, rafinamente ale
acestui algoritm au fost propuse şi investigate (spre exemplu, algoritmi tip
annealing search SAHC + alegere random).
Algoritmul genetic (AG) utilizează printre alţi operatori şi operatorul de
reproducere, care transferă în populaţia următoare vecinul cel mai bun. Se
asigură astfel că algoritmul este cel puţin la fel de bun ca SAHC. Operatorul
crossover generează noi procesoare, prin combinarea a două procesoare din
populaţie. Se implementează şi mutaţia, care modifică în mod cvasi-aleator
numărul de nuclee.
Algoritmii stigmergici se bazează pe comportamentul furnicilor din
lumea reală (ant colony optimizations - ACO). În căutarea hranei, furnicile
marchează drumurile cu feromoni, astfel încât traseele să poată fi urmate şi de
alte furnici. Evident, drumurile de succes, care au condus găsirea hranei, au mai
mulți feromoni decât celelalte. Feromonii se evaporează în timp, eficientizând
astfel căutarea (spre exemplu, se evită explorarea unor căi devenite, între timp,
fără de succes.) Se forţează căutarea pe câte o cale diferită în fiecare iteraţie,
prin pornirea căutării de la o soluţie situată în vecinătatea celei precedente. Se
evită astfel eşuarea în extreme locale, explorându-se însă mai mult spaţiul
soluţiilor.
Concluzia acestor cercetări a arătat că aceste tehnici euristice de explorare
sunt cel puţin de 3800 de ori mai rapide decât căutarea exhaustivă, generând
soluţii cu maximum 1% mai puţin performante decât aceasta, fapte remarcabile.
Scalabilitatea este de asemenea asigurată.
Dacă în ştiinţele tari, mature din punct de vedere teoretic, precum chimia,
fizica sau biologia, posibilitatea reproducerii rezultatelor experimentale
constituie o condiţie sine qua non, în arhitectura calculatoarelor acest lucru este
de multe ori imposibil, datorită faptului că nu există încă o metodologie
standardizată a cercetării. În cadrul cercetărilor dintr-o companie, simularea
modulara ar putea constitui o soluţie, dacă toţi cercetătorii ar folosi acelaşi
simulator. Din păcate, la nivelul cercetărilor academice, ori la nivel global inter-
companii, această abordare este, practic, imposibilă. În continuare se prezintă
succint o metodologie automată de optimizare euristică a parametrilor unei
microarhitecturi (ADSE), implementată sub forma unui site web numit

461
ArchExplorer.org. Aceasta este oarecum independentă de simulatorul care
implementează blocul hardware ce se doreşte a fi optimizat, chiar dacă, evident,
îl utilizează pentru căutarea în spaţiul enorm al parametrilor aferenţi arhitecturii
hardware, dar şi ai compilatorului. Desigur că integrarea simulatorului
corespunzător unui anumit bloc hardware (de tip cache, TLB, predictoare de
branch-uri, module DRAM, unităţi funcţionale, reţele de interconectare etc.) în
ArchExplorer.org este mai facilă dacă acesta are deja o construcţie modulară, cu
interfeţe bine definite între module (analoage API-urilor), compatibile cu cele
definite în ArchExplorer.org. Un astfel de simulator este UniSim. Metodologia
de explorare a spaţiului parametrilor arhitecturali se bazează pe algoritmi
genetici. Fiecare modul are asociată o genă, iar aceasta, la rândul ei, deţine mai
multe sub-gene, codificând parametrii modulului. Din păcate, modelarea
operatorilor genetici utilizaţi nu este clar explicitată. În cadrul ADSE, relaţia
arhitectura-compilator, deşi subtilă, este una puternică şi adesea subestimată de
către cercetători. Modificările arhitecturale pot produce modificări importante în
strategiile de optimizare din compilator, dar şi reciproc. Dacă pentru un
compilator neoptimizat performanţele procesorului sunt mai bune pentru setul
de parametri P1 decât pentru P2, atunci este posibil ca folosind un compilator
optimizat pentru P1 respectiv P2, performanţele în punctul P2 să fie mai bune
decât în P1. Interesant este faptul că mediul ArchExplorer.org oferă o explorare
continuă a spaţiului stărilor pe un web-server special dedicat.
O altă platformă web DSE este implementată prin proiectul colaborativ
intitulat cTuning.org - http://ctuning.org/wiki/index.php/Main_Page, care oferă
în mod gratuit utilizatorilor o tehnologie inteligentă în vederea optimizării
ansamblului arhitectură hardware-compilator-aplicaţie, pe baza unor metode din
teoria învăţării statistice şi automate. Se poate accesa o bază de date (Collective
Optimization Database) care conţine detalii asupra optimizărilor aplicate unor
sisteme de calcul complexe. Accesarea acestei baze de date oferă utilizatorilor şi
posibilitatea schimbului reciproc de experienţe, vizând optimizări interesante,
pentru diferite aplicaţii software şi respectiv platforme hardware utilizate pentru
rularea acestora. În principiu, oricine poate să-şi optimizeze în mod automat, cu
instrumentele puse la dispoziţie, o anumită aplicaţie sau benchmark. Trebuie
specificate arhitectura hardware, sistemul de operare şi compilatorul utilizate,
urmând ca apoi serviciul web cTuning.org să încerce optimizarea valorilor

462
parametrilor acestora, în vederea obţinerii unui timp minimal de execuţie, dar şi
a unei lungimi cât mai mici a codului sursă. În final, se generează automat
parametrii optimali aferenţi procesorului, compilatorului (parametrii de
optimizare a codului sursă), sistemului de operare şi aplicaţiei. De asemenea, se
determină şi câştigurile (performanţă, lungime de cod etc.) obţinute faţă de un
compilator GCC standard. Proiectul EC FP7 intitulat MultiCube se ocupă şi el
de asemenea cercetări.
În [Cal10] se descriu principalele caracteristici ale uneltei software
dezvoltată de noi, în cadrul unei teze de doctorat la Universitatea “Lucian
Blaga” din Sibiu condusă de autorul acestei cărți, pentru explorarea automata a
spaţiului de proiectare. Acest instrument este denumit FADSE (Framework for
Automatic Design Space Exploration). Scopul lui este de a accelera procesul de
optimizare, nu doar prin utilizarea algoritmilor euristici, ci şi prin permiterea
evaluării paralele a configuraţiilor. FADSE se cuplează la orice simulator al unei
arhitecturi de calcul, căruia îi variază valorile parametrilor în vederea optimizării
multiobiectiv a simulatorului respectiv. Aplicaţia integrează şi o bază de date
care îi permite reutilizarea rezultatelor obţinute anterior (indivizi deja simulaţi),
conducând deci la o scăderea a timpului necesar pentru explorare.
În FADSE s-a introdus o metodă nouă de accelerare a procesului de
explorare şi de creştere a calităţii rezultatelor. Toţi algoritmii de căutare folosiţi
în acest instrument de optimizare sunt algoritmi generali şi pot fi folosiţi pentru
aproape orice problemă de căutare. Aceștia au fost modificați de noi, cu scopul
de a utiliza informaţii date sub formă de reguli exprimate în logici fuzzy de către
un expert uman. Utilizatorul trebuie să descrie parametrii, folosindu-se de
termeni lingvistici (de exemplu: un cache de nivel unu mai mare de 256 KB este
„mare”, pentru valori sub 64 KB este „mic” etc.) şi apoi să introducă reguli uşor
de înţeles, cum ar fi: dacă nivelul 1 de cache este mic, atunci nivelul 2 de cache
trebuie să fie mare etc. Informaţiile furnizate prin aceste reguli sunt luate în
considerare în timpul procesului de explorare, pentru a ghida căutarea.
Printre altele, se prezintă și rezultatele testelor efectuate cu FADSE pe
simulatorul procesorului superscalar GAP (Grid ALU Processor), dezvoltat la
Universitatea din Augsburg, împreună cu un optimizator de cod dezvoltat
special pentru acest procesor, denumit GAPtimize. După analizarea rezultatelor
s-a ajuns la concluzia că FADSE a descoperit configuraţii mai bune decât cele

463
găsite de către un expert uman prin explorare manuală. De asemenea, s-a arătat
că FADSE este scalabil şi capabil de a găsi rezultate foarte bune în spaţiile de
proiectare extrem de mari, generate de parametrii simulatoarelor.

Arhitecturi multicore cu procesări anticipative

Puţine sunt cercetările care analizează tehnicile Value Prediction (VP) şi


Dynamic Instruction Reuse (DIR), expuse succint și în acest curs dar și în
[Vin07], în cadrul procesării firelor concurente pe procesoare SMT
(Simultaneous Multithreading) sau multicore (personal nu cunosc nici o
cercetare pe problematica grefării DIR în multicores). De aceea grupul nostru de
cercetare doreşte să activeze în aceasta nişă, unde avem deja realizări concrete,
cuantificabile, apreciate (a se vedea http://acaps.ulbsibiu.ro/). În asemenea
arhitecturi, a prezice valoarea unei instrucţiuni şi apoi, a verifica predicţia, după
ce valoarea a fost produsă, nu este întotdeauna suficient. Mai mult, acest proces
poate chiar implica erori de consistenţă ale variabilelor predicţionate. Astfel,
spre exemplu, în unele cazuri predicţia poate să fie corectă, dar execuţia
incorectă datorită violării consistenţei unor variabile. În articolele de specialitate
se exemplifică în mod convingător asemenea anomalii pe baza unui exemplu ce
descrie un fir care inserează un element (nod) în capul (head) unei liste simplu
înlănţuite. Problema este că, înainte de a insera noul nod în capul listei, firul îi
modifică valoarea. Un alt fir, citeşte primul element al listei. Această citire,
bazată pe o VP corectă, poate fi însă eronată. Între predicţia valorilor şi
problema consistenţei cache-urilor în sistemele multiprocesor există legaturi
subtile, neexplorate încă în mod aprofundat.
Preluat din lucrarea Martin M., et al., Correctly Implementing Value
Prediction în Microprocessors that Support Multithreading or Multiprocessing,
Proceedings of the 34-th Annual ACM/IEEE International Symposium on
Microarchitecture, Austin, Texas, December 3-5, 2001, se prezintă în continuare
o figură care explică printr-un exemplu extrem de sugestiv, posibila
inconsistență a variabilelor partajate, predicționate (corect!) într-un sistem
multiprocesor.

464
Figura 4.53.2 Inconsistență a valorii datorată predicției acesteia

Intuitiv, situația ilustrată în Figura 4.53.2 poate fi explicată astfel. Să


presupunem că un student a picat la examen în sesiunea din iarnă. Mai bine
pregătit, el se va prezenta la același examen, în sesiunea următoare, cea din vară.
El va trece examenul, de astă dată. Apare însă o situație ciudată. După examen,
studentul predicționează corect că rezultatele vor fi afișate la Afișierul nr. 3. În
consecință, se deplasează acolo și constată că a picat! Explicația este dată de
faptul că la afișier erau postate rezultatele din sesiunea anterioară, unde el, într-
adevăr, picase. După constatarea nefericită a studentului, profesorul va posta la
Afișierul nr. 3 rezultatele actuale, care atestă faptul că studentul respectiv a
trecut examenul. Așadar, deși studentul a predicționat corect locul în care vor fi
afișate rezultatele, el a citit un rezultat incorect. Observația cheie aici constă în
faptul că, predicția valorii instrucțiunii r1 a permis instrucțiunilor r1 și r2 să se
execute out of order (r2 s-a executat în mod speculativ – v. Figura anterioară).

465
Executând astfel aceste operații dependente cu referire la memorie, se violează
consistența secvențială.
O primă soluţie ar consta în forţarea consistenţei secvenţiale prin detectarea
adreselor de scriere-citire. Un procesor 1 (fir 1) trebuie să detecteze când un
altul (fir 2) scrie la o adresă de la care el (1) a citit speculativ, printr-o
instrucţiune încă nefinalizată. În cazul unei asemenea detecţii a violării
consistenţei secvenţiale, procesorul trebuie să efectueze un roll-back într-o stare
anterioară consistentă, nespeculativă. O altă soluţie constă în forţarea
consistenţei secvenţiale prin detectarea valorilor citite de Load-uri. Un anumit
Load, deja executat în mod speculativ (prin predicţia valorii adresei operandului
său), trebuie să aştepte până când valoarea adresei sale de memorie devine
cunoscută cu certitudine. (Evident că între timp execuţia speculativă a
instrucţiunilor dependente continuă.) Apoi, acest Load va citi valoarea. Se va
compara această valoare certă cu cea anterior prezisă. În caz de nepotrivire, se
va declanşa mecanismul standard de recovery prin roll-back. Alte variaţiuni sunt
desigur posibile. Așa cum am mai constatat, între predicţia valorilor şi problema
consistenţei cache-urilor în sistemele multiprocesor există legaturi subtile,
neexplorate încă în mod aprofundat. Mai mult, cercetări incrementale, bazate în
principal pe predicţie şi speculaţie, ar putea perfecţiona şi protocoalele actuale
de coerenţă a cache-urilor.
De asemenea, implementarea unor mecanisme DIR în arhitecturile
multicore este o problemă deschisă. Spre exemplu, metoda DIR ar impune ca
invalidările din cadrul buffer-ului de reutilizare (Reuse Buffer – RB) aferent
procesorului considerat, să fie efectuate în mod global, deci inclusiv de către
instrucţiuni ALU / Store executate de către un alt procesor. Mecanismele globale
de asigurare a coerenţei cache-urilor bazate pe invalidări la scrieri ar putea ajuta
şi la menţinerea consistenţei datelor din RB. Spre exemplu, invalidarea unei date
din D-cache ar trebui să determine automat invalidarea datei din RB.
Un alt obiectiv important al cercetărilor în domeniu constă în exploatarea
sinergică (adică, așa cum am mai precizat în această lucrare, interacțiunea
algoritmilor utilizați genereaza efecte mai bune decât superpoziția efectelor lor
individuale) a paralelismelor de diferite granularităţi (faze instrucțiuni & ILP &
MLP & TLP & tasks) în cadrul unui sistem multicore performant. Acest obiectiv
poate fi obţinut, în primul rând, prin implementarea heterogenităţii

466
componentelor de procesare în cadrul sistemului. Acest fapt înseamnă utilizarea
atât a unor procesoare simple, în vederea exploatării paralelismelor de nivel
masiv, dar şi a unor procesoare sofisticate, precum arhitecturile SMT spre
exemplu, în vederea exploatării paralelismelor de nivel mai fin. Tehnicile de
anticipare selectivă a instrucțiunilor, deja prezentate, vor avea probabil un rol
important, în special în accelerarea programelor intrinsec secvenţiale. Devine
clar încă o dată că între predicţia valorilor şi problema consistenţei cache-urilor
în sistemele multiprocesor, există legaturi subtile, neexplorate încă într-un mod
aprofundat, riguros.
Instrucţiunile cu latenţă ridicată reprezintă o sursă importantă de limitare a
paralelismului la nivelul instrucţiunilor. În [Gel09] am prezentat dezvoltarea
unui mecanism de anticipare selectivă a valorilor instrucţiunilor cu latenţă
ridicată de execuţie, care include o schemă de reutilizare dinamică pentru
instrucţiunile Mul şi Div, respectiv un predictor de valori pentru instrucţiunile
Load critice, adică cele cu miss în ierarhia de cache-uri. Rezultatele simulărilor
efectuate au arătat creşteri de performanţă (numărul mediu de instrucțiuni per
ciclu, IPC) de 3,5% pe benchmark-urile SPEC 2000 întregi respectiv de 23,6%
pe cele flotante şi o scădere importantă a consumului relativ de energie (a
Energy Delay Product-ului), de 6,2% pe întregi respectiv 34,5% pe flotante.
După ce am arătat utilitatea anticipării selective a instrucţiunilor cu latenţă
ridicată într-o arhitectură superscalară, am analizat eficienţa acestor metode şi
într-o arhitectură SMT, focalizându-ne pe aceleaşi instrucţiuni: Mul şi Div
respectiv Load-uri critice, mari consumatoare de timp . În acest context, firele de
execuţie conţinând Load-uri critice ori alte instrucţiuni de latenţă ridicată pot
bloca resursele partajate ale procesorului şi, în consecinţă, pot bloca celelalte fire
şi, în consecință, pot reduce performanţa globală. Rezultatele au arătat
îmbunătăţiri ale IPC pe toate configuraţiile SMT evaluate. Cu cât numărul de
fire de execuție este mai mare, cu atât creşterea de performanţă devine însă tot
mai puţin semnificativă, datorită exploatării tot mai eficiente a unităţilor de
execuţie partajate de către procesorul SMT. Plastic spus, cu motorul SMT
mergând în plin, sporul de performanţă aferent tehnicilor anticipative
implementate adiţional devine mai mic. Cele mai bune performanţe medii, de
2,29 IPC pe benchmark-urile de numere întregi respectiv de 2,88 IPC pe cele
flotante, s-au obţinut cu şase fire de execuţie. În lucrarea [Gel12] această

467
arhitectură predictiv-speculativă a fost optimizată multi-obiectiv, cu ajutorul
instrumentului FADSE dezvoltat la Sibiu, printr-o teză de doctorat coordonată
de autorul acestei lucrări.
În lucrarea Craeynest K. Van, Eyerman S., Eeckhout L., MLP-Aware
Runahead Threads în a Simultaneous Multithreading Processor, Proceedings of
The 4th HiPEAC International Conference, pp. 110-124, Paphos, Cyprus,
January 2009, autorii se ocupă tot de problema instrucţiunilor Load critice în
arhitecturile SMT, însă abordarea aceasta este una diferită de cea precedentă.
Ideea de bază este ca la detecţia unui Load critic, în anumite condiţii, să se
declanşeze execuţia speculativă a instrucţiunilor următoare, în scopul exploatării
paralelismelor la nivelul memoriei, prin procese de pre-fetch (MLP – Memory
Level Parallelism). Gradul de MLP este dat de numărul de accese independente
la memorie. O astfel de instrucțiune Load de mare latenţă va genera în mod
automat un checkpoint, salvând deci starea curentă a procesorului (regiştrii
logici, istoria branch-urilor etc.) Execuţia anticipată a instrucţiunilor care
urmează unui Load critic, în cadrul unui anumit fir de control, se face numai
dacă gradul de MLP estimat este unul suficient de ridicat. În plus, aceste
procesări speculative nu vor afecta starea arhitecturală (logică) a procesorului.
Există implementat câte un predictor dinamic al gradului de MLP per thread.
Acesta estimează gradul de MLP, după detectarea fiecărei instrucţiuni Load
critice, în timpul rulării. În caz contrar (grad MLP redus), firul respectiv este
rejectat din procesor, evitând astfel blocarea resurselor partajate ale acestuia. În
cadrul procesării predictiv-speculative, unele instrucţiuni independente de Load-
ul critic pot cauza miss-uri în ierarhia de cache-uri. Latenţa acestora se
suprapune peste cea a Load-ului critic aflat în curs de execuţie. Astfel, se
exploatează gradul de MLP existent în programele de test. Când un Load critic
se încheie, procesorul iese din modul speculativ de execuţie, goleşte structurile
pipeline de procesare a instrucțiunilor, restaurează checkpoint-ul şi reia
procesarea normală, cu instrucţiunea următoare Load-ului respectiv. Această
execuţie normală se va face mai rapid, având în vedere aducerile anticipate în
cache-uri, efectuate pe parcursul execuţiilor speculative. Aşadar, performanţa
per thread va creşte. Foarte important, această execuţie speculativă a
instrucţiunilor evită blocajul procesării în momentul în care un Load critic a
atins vârful Reorder Buffer-ului (reducând deci Memory Wall-ul). Se arată că

468
această arhitectură duce la beneficii considerabile, în special în cazul unor
programe care lucrează intensiv cu memoria.
Credem că asemenea idei merită aprofundate, îmbunătăţite şi investigate
în continuare. Nu trebuie neglijat paralelismul fin, de tip ILP, existent la nivelul
unui fir de execuție. Acesta nu poate fi exploatat prin caracteristica de multicore,
ci prin caracteristicile unui procesor (nucleu) în sine. Un studiu comparativ între
aceste două abordări, care urmăresc reducerea influenţei instrucţiunilor Load
critice într-o arhitectură SMT, ar fi unul interesant şi util. Desigur, comparaţia
trebuie efectuată multicriterial (IPC, energie consumată, disipaţie termică, arie
de integrare, complexitate etc.) Mediul de simulare utilizat ar putea fi M-Sim,
întrucât acesta permite şi calculul puterii (prin simulatorul Wattch pe care îl
conţine).
Grefarea unor asemenea tehnici predictiv-anticipative, precum cele de
reutilizare dinamică a instrucţiunilor sau de predicţie dinamică a rezultatelor
instrucţiunilor mașină în cadrul arhitecturilor multicore şi manycore, reprezintă
o problemă deschisă (2016), de mare interes în opinia noastră. Din păcate,
puține sunt – dacă sunt – preocupările în acest domeniu. Implementarea unor
scheme de VP şi DIR, care să nu violeze consistenţa variabilelor partajate în
sisteme SMT şi multicore este o problemă de mare interes. După cum se arată în
literatura de specialitate, chiar benchmark-urile concurente (SPLASH-2,
PARSEC etc.) nu conţin suficient paralelism pentru sistemele multiprocesor
moderne. În aceste condiţii, creşterea performanţei porţiunilor secvenţiale de cod
peste bariera impusă de celebra lege a lui Amdahl (practic, bariera generată de
dependenţele de date între instrucţiuni), se poate realiza prin anticiparea
rezultatelor instrucţiunilor (înainte ca acestea să fie produse, încă din faza de
decodificare). Aceste tehnici anticipative vor reduce semnificativ accesele la
reţeaua de interconectare, utilizată în vederea comunicării prin variabile
partajate. La ora actuală, influenţa predicţiei dinamice a valorilor şi, cu atât mai
puţin a reutilizarii dinamice a instrucţiunilor, sunt departe de a fi înţelese bine, în
cadrul sistemelor SMT sau multiprocesor.
Implementarea unor mecanisme de reutilizare dinamică a instrucțiunilor
în arhitecturile multicore impune ca invalidările din cadrul buffer-ului de
reutilizare aferent procesorului considerat, să fie efectuate în mod global, deci
inclusiv de către instrucţiuni Store executate de către un alt procesor, care scrie

469
la o adresă existentă în RB-ul procesorului considerat. Mecanismele globale de
asigurare a coerenţei cache-urilor, bazate pe invalidări la scrieri, ar putea ajuta și
la menţinerea consistenţei datelor din buffer-ul de reordonare. Invalidarea unei
date din cache-ul de date ar trebui să determine automat invalidarea datei din
Reuse Buffer, dacă aceasta este stocată şi acolo.
Paradigma de procesare pe baza fluxurilor de date (Data-Flow, DF)
găseşte adepţi şi în domeniul multiprocesoarelor. În cadrul acestui model
procesările se execută de îndată ce valorile operanzilor sursă devin disponibile,
deci aceste arhitecturi sunt de tip data driven, fiind de arhitecturi de calcul non
von Neumann. Modelul DF, dezbătut și cercetat puternic prin anii ’80, reprezintă
un model formal distribuit, inerent paralel, funcţional şi asincron. După cum am
precizat, execuţia într-un nod startează de îndată ce toate datele de intrare sunt
disponibile. Programul se reprezintă sub forma unui graf, în care nodurile
reprezintă operaţii, iar arcele reprezintă sursele (căile) de date. Modelul Data
Driven Multithreading (DDM), relativ nou, exploatează modelul DF la nivelul
firelor de execuţie, în cadrul unor sisteme multicore. Modelul DDM se bazează
pe conceptul paralelizării automate, la nivelul hardware – software, incluzând
tehnici de reprezentare poliedrală a programelor. Modelul poliedral de
reprezentare a programelor (polyhedral model - Karp, Miller și Winograd)
reprezintă un mod de reprezentare semantică, pe baze algebrice, care oferă
posibilități de optimizare sofisticate ale programelor. Acest model poliedral este
mai apropiat de execuția programului decât clasicul model operational - sintactic
de reprezentare. Compilatoarele pentru sisteme multicore utilizează tot mai mult
acest model de reprezentare (spre exemplu, compilatoarele GCC 4.4 și IBM XL).
Actualmente se cercetează ansambluri multicore tip DDM – compilator, atât la
nivel de virtualizare cât şi la nivelul unor implementări hardware
reconfigurabile, prin tehnologii FPGA.

Putere consumată, disipaţie termică

Un obiectiv important al cercetărilor autorului acestei cărți și a grupului


său, a constat şi în grefarea unor module care să calculeze automat puterea
electrică, dar şi disipaţia termică pe procesorul SMT, îmbunătăţit cu predicţia
valorilor instrucţiunilor Load critice (cu miss în L2 cache) respectiv cu

470
reutilizarea dinamică a instrucţiunilor de latenţă ridicată (Mul/Div etc.) Astfel,
pentru calculul puterii electrice statice şi dinamice, s-a putut utiliza, în cadrul
simulatorului SMT îmbunătăţit de noi, simulatorul Wattch
(http://www.eecs.harvard.edu/~dbrooks/wattch-form.html), care este deja
integrat în mediul de simulare M-Sim utilizat. Acesta se bazează pe simulatorul
SimpleScalar sim-outorder, ver. 3.0 şi calculează puterea electrică disipată
pentru structuri de tip vector, structuri asociative de tip Reorder Buffer, Load
Store Queue etc., logică de tip combinaţional (pentru partea de comenzi ale
procesorului şi unităţi funcţionale) etc. Astfel, s-a extins calculul puterii electrice
disipate la întregul procesor. De asemenea, este de un real interes şi calculul
puterii statice, în special în cadrul unor memorii de capacităţi relativ mari,
precum L2 caches (nivelul 2 de memorii cache). În particular, unii autori au
integrat simulatorul Simics cu Wattch-ul şi au implementat un simulator
performant numit SimWattch (simulează inclusiv procesarea sistemului de
operare + putere electrică disipată).
Un modul pentru investigarea efectelor de disipaţie termică aferente
acestor tehnici, implementat ca un API al simulatorului M-Sim, ar fi de mare
utilitate. Se ştie că hotspot-urile (pusee de temperatură peste limita maximă
admisă – cca. 118 grade Celsius) conduc la erori de procesare, iar temperatura
prea mare micşorează durata de viaţă a circuitului. Este interesant de evaluat
efectul acesteia în contextul noilor structuri anticipative pe care noi le-am
introdus în arhitectura SMT M-Sim.
În concluzie la acest paragraf, am realizat un studiu asupra impactului
arhitecturilor multicore şi manycore în cadrul ingineriei calculatoarelor. S-au
identificat şi s-au analizat în mod sistematic, pe baza unei literaturi de
specialitate relativ recente, următoarele provocări importante pentru cercetarea şi
dezvoltarea acestor sisteme:

• Arhitecturi multicore omogene vs. eterogene


• Exploatarea sinergică a tipurilor de paralelism
• Ierarhia de memorii cache
• Coerenţa şi consistenţa variabilelor partajate – scheme scalabile
• Reţele de interconectare
• Modele de programare paralelă

471
• Paralelizarea aplicaţiilor
• Simularea ca instrument de cercetare
• Benchmarking
• Explorarea automată a spaţiului parametrilor
• Arhitecturi multicore cu procesări anticipative
• Putere consumată, disipaţie termică

Concluzia de bază este că sunt necesare progrese importante în toate aceste


domenii, pentru ca proiectarea dar şi utilizarea sistemelor multicore şi manycore
să fie adecvate. Cu sau fără voia noastră, aceste sisteme vor constitui
dispozitivele de calcul universale. Abordarea oricăreia dintre aceste provocări
trebuie să fie una de tip holistic, integrator la nivelul hardware-software. S-a
arătat cum vor schimba aceste noi arhitecturi, paradigma ştiinţei și ingineriei
calculatoarelor. În particular, programarea acestor sisteme constituie o provocare
majoră, pentru care nu suntem încă pregătiţi suficient.

4.13. OPTIMIZAREA MULTI-OBIECTIV A SISTEMELOR DE


CALCUL

În continuare, prezentăm pe baza lucrării noastre [Vin16], într-o formă


revăzută și completată cu informații noi, câteva idei considerate fundamentale în
optimizarea sistemelor complexe de calcul. Problema optimizării multi-obiectiv
a sistemelor de calcul este una complexă, datorită faptului că spațiul de
proiectare este, în general, unul enorm, care nu poate fi cercetat și evaluat într-
un mod exhaustiv. Spre exemplu, un sistem de calcul cu 50 de parametri
(numărul de procesoare din sistem, tipul rețelei de interconectare între acestea,
capacitatea cache de nivel 3/2/1 etc.), fiecare dintre aceștia putând avea doar 8
valori discrete, implică un spațiu de căutare enorm, de 2150 configurații distincte!
Problema care se pune este: care dintre aceste configurații sunt cele mai
performante? Evident că aceasta este o problemă de complexitate NP-hard. Ca
să dăm un alt exemplu, mai practic, simulatorul software M-SIM 2 (procesor
superscalar/SMT) are 2,5 milioane de miliarde de configurații posibile. Este

472
evident că o evaluare manuală nu este posibilă în asemenea cazuri. Nici măcar
una automata, bazată pe evaluări efectuate pe sisteme de calcul (performante). În
realitate, problema este și mai complexă pentru că, din această mulțime enormă
de instanțe CPU (în cazul nostru), nu o dorim neapărat pe cea mai performantă,
ci, mai degrabă, pe cea mai performantă în condiții de consum energetic
minimal, complexitate hardware cât mai mică etc. Așadar, problema de
optimizare nu este una cu un singur obiectiv, ci una cu mai multe obiective, în
general contradictorii unele față de celelalte (spre exemplu, performanța vs.
puterea consumată). Problema pusă devine deci și mai complexă, iar soluțiile nu
pot fi decât aproximative, generabile prin algoritmi euristici.
Dar nu numai arhitectura hardware trebuie optimizată, ci și compilatorul
și programele HLL care vor rula pe această arhitectură. Aceste programe, spre
exemplu, sunt compilate cu diferite opțiuni de optimizare. Este evident că
opțiunile optimale de compilare (metodele de scheduling utilizate) sunt și ele
dependente de parametrii arhitecturii hardware. În consecință, este nevoie de o
co-optimizare hardware-software (hardware software co-optimization cross-
layer optimization), ceea ce înseamnă că spațiul de căutare este în realitate și
mai mare, fiind dat de totalitatea instanțelor hardware – software ale sistemului
de optimizat!
O funcție vector este o funcție care mapează un t-uplu de m parametri
arhitecturali, pe un t-uplu de n obiective [Vin13]. Mai precis:

Min/Max y = f(x) = (f1(x), f2(x) … fn(x))

x = (x1, x2, …, xm) Є X – vectorul de decizie (nr. de nuclee, tip rețea de


interconectare, capacitate cache, nr. unități de execuție per procesor etc.)

y = (y1, y2, …, yn) Є Y- vector obiectiv (performanță, energie consumată,


complexitate hardware, Worst Case Execution Time - WCET etc.)

473
Figura 4.54. Curba Pareto pentru o problemă de minimizare cu două obiective

În Figura 4.54 se prezintă curba Pareto pentru o problemă de minimizare


cu doar două obiective (f1, f2). Se numește astfel, după economistul, sociologul
și matematicianul italian Vilfredo Pareto. Se doresc identificați acei indivizi,
considerați optimali, care minimizează ambele obiective f1 și f2. Oricare doi
indivizi de pe curba Pareto (A, B) sunt non-dominați. Spre exemplu, individul A
domină doar parțial individul B, din punct de vedere al obiectivului f2 (pentru că
f2(A)<f2(B)), în timp ce individul B domină doar parțial individul A, din punct
de vedere al obiectivului f1 (f1(B)<f1(A)). În schimb, se poate observa că pentru
orice individ C, care nu aparține frontului Pareto, există cel puțin un punct
(individ) pe curba Pareto care să-l domine pe acesta (C), din punct de vedere al
ambelor obiective f1 și f2. Așadar, indivizii Pareto sunt dominanți, față de cei
care nu aparțin curbei respective. Într-un spațiu ortogonal cu 3 obiective, avem
suprafața Pareto (în locul frontului Pareto din planul euclidian). Într-un spațiu n-
dimensional, avem o hiper-suprafață Pareto. Problema unei optimizări multi-
obiectiv complexe este aceea de a determina hiper-suprafața Pareto reală, care
aproximează pe cât de bine posibil hiper-suprafața Pareto ideală (în general,
imposibil de determinat).

474
Pentru a determina Hiper-Suprafața Pareto (HSP) aproximativă, s-au
dezvoltat algoritmi euristici de optimizare multi-obiectiv. Printre cei mai
cunoscuți astfel de algoritmi sunt algoritmii genetici multi-obiectiv (evolutivi, de
tip NSGA-II, SPEA2, CNSGA-II etc.) și algoritmii de tip Particle Swarm
Optimization (bio-inspirați din inteligența colaborativă a stolurilor de păsări în
căutarea hranei etc.) sau stigmergici (bio-inspirați din inteligența colaborativă a
roiurilor de furnici, albine în căutarea hranei etc.), de asemenea adaptați la
multiple obiective. Spre exemplu, biblioteca software gratuită jMetal oferă
implementări ale unor astfel de algoritmi.

Figura 4.55. Schema logică a unui algoritm genetic

În Figura 4.55 se prezintă schema logică de principiu a unui algoritm


genetic. Acesta pornește de la o populație (generație) inițială de N indivizi
distincți, numiți cromozomi, prin analogie cu genetica. Această generație inițială
ar putea fi generată cvasi-aleator sau pe baza experienței utilizatorului, care
poate să introducă anumite soluții cunoscute, performante, în ea. Un cromozom
reprezintă practic o succesiune de așa numite gene, concatenate. O genă, în cazul
nostru particular, reprezintă un parametru al arhitecturii de calcul care trebuie
optimizată (spre exemplu, numărul de procesoare din SMM, tipul RIC,
capacitatea cache-urilor etc.) Evident că aceste gene sunt codificate într-un
anumit mod (deseori binar). Apoi, fiecare individ al populației este evaluat. În
cazul nostru, evaluarea se face prin simularea fiecărei instanțe a sistemului de
calcul respectiv (din cele N existente), utilizând un simulator software dedicat și

475
unul sau mai multe benchmark-uri (această evaluare este, în general, mare
consumatoare de timp; din fericire poate fi paralelizată prin calcul paralel sau
distribuit). În urma evaluării, fiecare individ (cromozom) din populație va obține
Fk
o așa numită rată de fitness ( N
∈ [0,1] ), în general normalizată în intervalul [0,
∑ Fk
k =1

1].
În continuare, se selectează într-un mod stohastic perechi de indivizi spre
a fi încrucișați, cu o anumită probabilitate. Există mai mulți algoritmi de
selecție. Spre exemplu, cel numit deterministic tournament selection alege
aleator k indivizi, 1<k<N, iar apoi selectează pe cel mai bun dintre aceștia în rol
de părinte. Se repetă până când se obțin cei N părinți. Acestor perechi de părinți
li se aplică operatorul genetic de încrucișare, numit Crossover. Evident, indivizii
cu rate de fitness mai mari, vor avea șanse mai mari de a fi selectați pentru
încrucișare decât cei cu performanțe mai reduse. Există mai multe strategii de
Crossover. Cea mai simplă (numită single point crossover operator), împarte
părinții în două părți (P11&P12 – părinte 1, P21&P22 – părinte 2), într-un mod
cvasi-aleator și obține doi noi indivizi (copii, offsprings), prin încrucișarea
acestor părți (P11&P22 –offspring 1, P21&P12 – offspring 2). Se obțin astfel
încă N indivizi. Acum, celor N indivizi vechi (părinți), precum și celor N
indivizi nou creați (offsprings), li se aplică operatorul genetic de mutație
(Mutation), cu o anumită probabilitate prestabilită (în general, mică). Alte
versiuni aplică mutația doar asupra copiilor. Dacă, spre exemplu, probabilitatea
de mutație este 0,05 atunci doar 5% dintre cei 2N indivizi vor suferi mutații, pe
anumite gene. Mutația (the bit flip mutation) opereză pe un singur individ, ca
mai jos.
1. Pentru toate genele (parametrii) aferente unui individ (cromozom);
1.1. Se generează un număr cvasi-aleator (rațional) între 0 și 1;
1.2. Dacă numărul generat < probabilitatea de mutație;
1.2.1. Schimbă valoare genei respective în mod cvasi-aleator;
2. STOP.
Mutația asigură diversitatea noii generații de cromozomi, prin explorarea
uniformă a spațiului de căutare. Fără mutație, s-ar putea degenera într-un
fenomen de endogamie genetică, prin care noile generații ar deriva doar din

476
caracteristicile strămoșilor lor (nu s-ar mai aduce “sânge proaspăt” în noile
generații). Există prezentate în literatura de specialitate multe alte variațiuni ale
mutației. Cei N indivizi nou obținuți, în urma încrucișărilor și mutațiilor, se
evaluează și ei, alocându-se fiecăruia o rată de fitness normalizată. Apoi, dintre
cei N indivizi ai populației inițiale, respectiv dintre cei N indivizi nou creați, se
selectează prin ierarhizare, pe baza ratei lor de fitness, cei mai performanți N
indivizi (cromozomi), care vor forma următoarea generație (operatorul genetic
de selecție - Selection). Această selecție este evident una elitistă (pot fi
implementați și alți algoritmi de selecție). Alte versiuni consideră că noua
generație este formată doar din indivizii nou creați. Algoritmul se va repeta până
când condiția de oprire va fi îndeplinită (spre exemplu, se atinge numărul
prestabilit de generații, performanța ultimei generații nu a mai crescut în mod
semnificativ etc.) Există foarte multe variațiuni ale acestui algoritm euristic. Prin
astfel de algoritmi, în generația finală se obțin indivizi optimizați (adică
performanți) din punct de vedere al ratei de fitness. Această ultimă afirmație are
și o justificare teoretică solidă, dată de așa numită teoremă a schemei (schemata
theorem), demonstrată de Holland în anii 70 ai secolului trecut, care justifică
faptul că algoritmii genetici converg.
Algoritmul Particle Swarm Optimization – abreviat PSO (dezvoltat de
Russell Eberhart și James Kennedy, 1995) este un alt algoritm euristic de
căutare, inspirat din inteligența colectivă a stolurilor de păsări (particule,
particles) aflate în zbor, în căutarea hranei (aceasta este considerată unică în
acest algoritm, într-un anumit punct dat din spațiul de căutare). PSO poate
constitui o alternativă la algoritmii genetici. Acest algoritm utilizează
r
informațiile numite global best - xgbest (cea mai bună soluție globală găsită până
la momentul curent, reprezentând deci poziția din spațiu cea mai apropiată de
hrană; cu cât o pasăre este mai aproape de hrană, cu atât ciripește mai tare!) și
r
personal best - x pbest (cea mai bună soluție locală – deci aferentă doar unui
i

individ (i) – găsită până la momentul curent). Evident, sursa de hrană este
ascunsă pentru particulele-păsări (ele simt doar mirosul aferent, mai puternic sau
mai slab și ciripesc cu o intensitate în consecință). Aceste două informații
semnifică deci distanțele momentane față de hrană. Algoritmul generează soluția
în vederea găsirii hranei de către stolul aflat în căutarea acesteia. Fiecare

477
r
particulă (i) deține parametri precum poziția sa (x), viteza (v) și x pbest . Aceștia se i

adaptează iterativ, precum se prezintă în continuare.


Viteza fiecărui individ din mulțimea de optimizat este modificată iterativ
conform formulei următoare:
r r r r r r
vi (t ) = Wvi (t − 1) + C1 r1 ( x pbesti − xi (t − 1)) + C 2 r2 ( x gbest − xi (t − 1))
În consecință, noua poziție a unui individ va deveni:
r r r
x i ( t ) = x i ( t − 1) + v i ( t )
unde W reprezintă masa inerțială (inertia weight), iar C1 și C2 reprezintă factorii
de învățare (în general, dar nu întotdeauna, constanți). Deseori se consideră
C1=C2 =2. r1, r2 sunt valori aleatoare din intervalul [0, 1]. Parametrul W se ia în
mod frecvent ca o valoare aleatoare între 0.1 și 0.5. Problema convergenței
acestor algoritmi bio-inspirați este una extrem de dificilă din punct de vedere
teoretic (matematic), depășind cadrul prezentării noastre. Utilitatea
demonstrației matematice a convergenței este una importantă, dacă dorim să
înțelegem profund funcționarea unor metode de tip PSO. Un articol de referință
și de sinteză în acest sens, care a pus în evidență progrese semnificative în
înțelegerea convergenței PSO, însă prin asumarea unor contexte restrictive,
oarecum rigide, care limiteaza "metafora" constitutivă, este următorul: Dong
ping Tian, A Review of Convergence Analysis of Particle Swarm Optimization,
International Journal of Grid and Distributed Computing Vol.6, No.6 (2013),
pp.117-128 (disponibil la
http://www.sersc.org/journals/IJGDC/vol6_no6/10.pdf, accesat la 01.08.2016).
Revenind acum la algoritmii evoluționiști, problema specifică algoritmilor
genetici multi-obiectiv este următoarea: cum am putea sorta (ierarhiza) indivizi
reprezentați într-un spațiu (ortogonal sau cvasi-ortogonal) multi-obiectiv? Pentru
un singur obiectiv, sortarea era trivială, funcție doar de valoarea ratei de fitness.
Desigur, o soluție ar consta în reducerea algoritmului genetic multi-obiectiv la
unul mono-obiectiv, prin agregarea obiectivelor multiple într-unul singur, printr-
o formulă de agregare. Ideea este de a micșora, pe cât mai mult posibil,
pierderea de informație pe care procesul de agregare o implică. Personal nu
agreez în mod deosebit asemenea metode. În literatura matematică de
specialitate s-au formulat trei condiții necesar a fi îndeplinite într-un proces
corect de agregare. Astfel, indicatorul global (agregat) trebuie să fie sensibil

478
(senzitiv în mod corect la variațiile valorilor indicatorilor componenți). Apoi,
acesta trebuie să fie anti-catastrofic, în sensul dat de teoria catastrofelor
elaborată de matematicianul francez Rene Thom – laureat al Medaliei Fields
(1958). Intuitiv, aceasta înseamnă că variații mici ale valorilor intrărilor, nu
trebuie să producă valori mari („catastrofice”, discontinue, instabile) ale ieșirii.
În fine, indicatorul agregat trebuie să fie necompensatoriu, adică modificarea
într-un sens (creștere/descreștere) a valorii unui indicator component să nu poată
fi compensată de modificarea în sens invers, a altuia. S-a arătat că nu există
nicio operație de agregare a indicatorilor, care să satisfacă, simultan, cele trei
condiții de bun-simț enunțate anterior, ceea ce închide iluzia unei agregări
adecvate. În consecință nu vom stărui pe abordări de acest fel, deși ele există.
Prezentăm în continuare câteva abordări native Pareto (multi-obiectiv) la
această problemă. Spre exemplu, algoritmul genetic multi-obiectiv NSGA-II
(Non-dominated Sorting Genetic Algorithm) soluționează această problemă de
ierarhizare elitistă, sortând indivizii în fronturi Pareto succesive. După această
operație, se selectează indivizii de pe primul front Pareto, P1, al indivizilor non-
dominați. Dacă mai este nevoie de indivizi în noua generație, se selectează cei
aparținând frontului Pareto P2 ș.a.m.d. Așadar, acest algoritm preferă un individ
situat pe un front Pareto (HSP) mai bun. Se pune, totuși, problema sortării
indivizilor de pe același front Pareto, Pk, dacă este nevoie. În acest caz, se
calculează distanțele între un individ i aparținând HSP Pk și vecinii săi, i+1
respectiv i-1 (Crowding Distance - CD), pentru toți indivizii aparținând frontului
Pk. Algoritmul NSGA-II preferă în acest caz indivizii cu un CD mai mare. Spre
exemplu, dacă trebuie aleși doi indivizi de pe un front Pareto în care indivizii
sunt grupați în două clustere distincte, se va alege prin acest algoritm câte un
individ din fiecare cluster. Ideea intuitivă care stă la baza acestei alegeri este
aceea de a selecta, pe cât posibil, indivizi de pe întreg frontul Pk, asigurându-se
astfel o oarecare diversitate a noii generații și o căutare mai uniformă în spațiul
de proiectare.
Algoritmul CNSGA-II (Controlled NSGA-II), spre exemplu, procedează
altfel. Acesta preferă să selecteze indivizi de pe toate fronturile Pareto, jertfind
deci elitismul selecției NSGA-II, în favoarea unei mai mari diversități a
generației următoare (prin includerea, deliberată, a unor indivizi dominați,

479
aparținând unor fronturi Pareto inferioare). Mecanismul de selecție în acest caz
1− r
este controlat după rata de selecție (r), folosind formula ni=Nri-1 , unde:
1− r k
N = numărul de indivizi dintr-o generație

ni= numărul maxim de indivizi aparținând frontului Pareto nr. i

r = rata de reducere; 0<r<1

k = numărul total de fronturi Pareto rezultat în urma sortării

Astfel, spațiul de căutare este eșantionat într-un mod și mai uniform.


O metrică de calitate a acestor algoritmi de optimizare multi-obiectiv,
utilă și utilizată, este dată de așa numitul hiper-volum (HV). Într-o problemă de
minimizare, măsura acestui HV [%] este dată de formula (HVreal/HV
ideal)x100%. HVreal reprezintă măsura hiper-volumului cuprins între frontul
Pareto real și un așa numit punct de referință. Atunci când frontul Pareto ideal
este cunoscut, HVideal este măsura volumului cuprins între frontul Pareto ideal
și un așa numit punct de referință. Când însă frontul ideal nu este cunoscut
(cazul problemelor complexe), HVideal se consideră a fi dat de măsura
volumului cuprins între axele ortogonale și punctul de referință Acest punct de
referință are coordonatele date de valorile maxime ale obiectivelor, ca în figura
următoare.

Figura 4.56. Hiper-volumul într-o problemă de minimizare bi-obiectiv

480
Într-o problemă de minimizare, un HV mai mare înseamnă, în principiu, o
calitate mai bună a soluțiilor de pe frontul Pareto real (desigur, extremitățile
frontului pot varia de la o iterație la alta). Măsura HV poate fi folosită și pentru
condiția de oprire a algoritmului genetic. Spre exemplu, într-o problemă de
minimizare, când măsura HV nu mai crește semnificativ față de generația
precedentă, algoritmul s-ar putea opri. O altă metrică este așa numita Two Set
Difference Hypervolume (TSDH) și este definită ca:
TSDH ( X ' , X " ) = H ( X '+ X " ) − H ( X " ) , unde X ' , X ' ' ⊆ X sunt două mulțimi de vectori
de decizie, H ( X ) este HV-ul aferent vectorului de decizie X, iar X '+ X " este
vectorul de indivizi non-dominați obținuți prin reuniunea mulțimilor X ' și X "
(practic, o superpoziție). TSDH ( X ' , X " ) calculează HV din hiper-spațiul care este
dominat de X ' , dar nu și de X " . Dacă [TSHD(X1, X2) = 0 și TSHD(X2, X1) > 0]
atunci X2 este absolut superior lui X1.
O altă metrică de calitate, numită Coverage, compară doi algoritmi multi-
obiectiv sau două rulări ale aceluiași algoritm. Această metrică va calcula
procentajul de indivizi dintr-o populație care sunt dominați de indivizi din
cealaltă populație. Considerând X ' , X ' ' ⊆ X două mulțimi de vectori de decizie,
funcția C (Coverage) mapează perechea ordonată de vectori de decizie ( X ' , X ' ' )
în intervalul [0, 1] astfel (s-a notat operatorul de dominanță cu f ):
{a' '∈ X ' ' ; ∃a'∈ X ': a' fa' '}
C( X ', X ' ' ) =
X ''
Dacă toate elementele din X’’ sunt dominate (sau egale) de (cu) elementele
din X’ atunci C ( X ' , X ' ' ) = 1. Dacă C ( X ' , X ' ' ) = 0, atunci niciun element din X’’
nu este dominat de elemente din X’. Faptul că C ( X ' , X ' ' ) > C ( X ' ' , X ' ) nu înseamnă
neapărat că soluțiile din prima mulțime (front Pareto) sunt net mai bune decât
cele din a 2-a, după cum se poate constata din exemplul dat în figura următoare.
În general, C ( X ' , X ' ' ) ≠ C ( X ' ' , X ' ) .

481
Figura 4.56.b. Hiper-volum vs. Coverage
Algoritmii de optimizare sunt prea generali, nefiind specializați pe o
anumită problematică particulară (sistemele de calcul, la noi). Pentru a obține o
convergență mai rapidă a algoritmilor multi-obiectiv de optimizare, dar și pentru
o calitate mai bună a soluțiilor, autorul acestei cărți și colaboratorii săi au
implementat utilizarea unor cunoștințe specifice de domeniu (Domain-
Knowledge), exprimabile în acest caz prin reguli în logici fuzzy, în cadrul
acestor algoritmi. Această idee a fost publicată pentru prima dată în lucrările
[Cal11] și [Jah12], la care și autorul acestei cărți a contribuit. Ea a fost, de altfel,
anunțată în premieră în lucrarea H. CALBOREAN, R. JAHR, T. UNGERER, L.
VINȚAN - Optimizing a Superscalar System using Multi-objective Design
Space Exploration, Proceedings of the 18th International Conference on Control
Systems and Computer Science (CSCS-18), vol. I, pp. 339-346, ISSN 2066-
4451, Bucharest, May 2011. Câteva dintre aceste reguli, exprimate în logici
fuzzy pentru o arhitectură parametrizabilă de procesor superscalar, inteligibile
ușor, sunt de genul: IF IL1Cache_Size IS small AND DL1Cache_Size IS small
THEN UL2Cache_size IS big or IF IL1Cache_Size IS big AND DL1Cache_Size
IS big THEN UL2Cache_size IS small, IF Number_of_Physical_Register_Sets
IS small/big THEN Decode/Issue/Commit_Width IS small/big etc. Pentru fiecare
astfel de regulă, exprimată într-o formă normală conjunctivă, se generează un
process de fuzificare inferențe logice defuzificare [Cal11].

482
Logicile fuzzy au la bază teoria mulțimilor fuzzy, dezvoltată de profesorul
Lotfi Zadeh de la Universitatea Berkeley, SUA, începând cu anul 1965. În teoria
clasică a mulțimilor, funcția caracteristică asociată unei mulțimi A, pentru un
anumit element x, este 1 sau 0, după cum x aparține respectiv nu aparține
mulțimii A. Se arată simplu că A=B dacă și numai dacă µ A = µ B , unde prin µ A
am notat funcția caracteristică a mulțimii A. Se demonstrează elementar
proprietățile de mai jos ale funcțiilor caracteristice aferente a două mulțimi
clasice.

µ A = 1 − µ A oricare ar fi xєA.
µ A∧ B = µ A ⋅ µ B
µ A∨ B = µ A + µ B − µ A ⋅ µ B

Observație: Proprietățile anterioare reduc identitățile între mulțimi la


identități algebrice elementare. Spre exemplu, relații între mulțimi precum cele
ale lui De Morgan se demonstrează imediat pe această bază.
În teoria mulțimilor fuzzy această funcție caracteristică este una graduală,
putând avea orice valoare reală între 0 și 1. Iată, spre exemplu, în continuare,
funcțiile caracteristice asociate variabilelor lingvistice scund (small), mediu și
înalt (tall).

Se pune problema cum se definesc conectorii logici (ȘI, SAU) în acest


nou context? Răspunsul dat de L. Zadeh și de alți logicieni se bazeză pe
analogiile cu algebra logică a lui G. Boole. Astfel, conectorii logici pot fi definiți
astfel:

483
µA = 1− µA
µ A∧ B = µ A ⋅ µ B sau µ A∧ B = min{µ A , µ B }
µ A∨ B = µ A + µ B − µ A ⋅ µ B
sau µ A∨ B = max{µ A , µ B } , unde µ este funcția
caracteristică asociată mulțimii fuzzy respective.

Iată și câteva formule utilizate pentru operatorul de implicație materială


( ) în logica fuzzy:

µ A→ B ( x, y ) = max((1 − µ A ( x)), µ B ( y ) ) ; derivă din logica clasică, unde implicația


materială este definită A B = (~A) SAU B = max (Valoare(~A), Valoare(B)). S-
a considerat că µ A (x ) = 1 − µ A (x ) , µ A (x ) ∈ [0,1] (~A=A negat).

µ A→ B ( x, y ) = max[min(µ A ( x), µ B ( y ) ), (1 − µ A ( x) )] ; L. Zadeh

µ A→ B ( x, y ) = min(1,1 − µ A ( x) + µ B ( y ) ) ; Lukasiewicz

µ A→ B = µ A ∪ B = 1 - µ A + µ A ⋅ µ B (Reichenbach, naturală prin analogie cu logica


clasică)

µ A→ B ( x, y ) = min(µ A ( x), µ B ( y ) ) ; Mamdani. Arată ciudat în opinia autorului acestei


cărți, pentru că și µ A∧ B ( x, y) = min(µ A ( x), µ B ( y)) ! Această ultimă formulă este
naturală, fiind derivată prin analogie cu logica booleană unde: V(A&B) = min
(Valoare(A), Valoare(B)). În aceste condiții formula lui Mamdani pentru
implicația materială ( ) este de neînțeles, cel puțin pentru autorul acestei
lucrări. În schimb, poate fi utilizată necontradictoriu în condițiile utilizării
formulei alternative µ A∧ B = µ A ⋅ µ B .
Observație: Implicația materială este un operator esential și în logica
clasică. Este evident că: A B este echivalent cu (~B) (~A) pentru că A B =
(~A)UB = BU(~A) = (~B) (~A), q.e.d. (U=SAU logic). Deci A B este
echivalent cu (~B) (~A). În particular, dacă (~B =1)&((~B) (~A))=1 atunci
~A =1, echivalent cu A=0. Această echivalență logică stă la baza
raționamentului de reducere prin absurd - reductio (argumentum) ad absurdum,

484
utilizat în demonstrațiile matematice. În acord cu această metodă, dacă prin
absurd (ad absurdum!) considerăm B=0 și demonstrăm ((~B) (~A))=1 atunci
A=0. Dar această ultimă identitate este imposibilă, întrucât contrazice ipoteza
(hypo thesis, adică aceea care stă la baza tezei) A=1. Rezultă deci în mod
necesar B=1, q.e.d. Analog B A este logic echivalent cu (~A) (~B).
Cu aceste formule logice definite, procesele de inferențe logice (ex. Dacă
A și B atunci C etc.) sunt deci calculabile și în logicile fuzzy. O introducere
intuitivă și rapidă în problematica logicilor fuzzy vizate aici, se găsește în
referința web elaborată de autor [Vin13]. Spre exemplu, să considerăm două
reguli exprimate în logică fuzzy, într-o formă normal conjunctivă, ca mai jos:

R1: IF age IS young AND car-power IS high THEN risk IS high


R2: IF age IS normal AND car-power IS medium THEN risk IS medium

Mai întâi se vor defini funcțiile graduale de apartenență aferente


variabilelor lingvistice implicate, ca mai jos (fuzificare).

Practic, conform regulii logice R1 se deduce funcția de apartenență


aferentă ieșirii risk=high, conform regulii de ȘI LOGIC, anume
µ risk / high / R1 = min{µ age / young =a , µ car − power / high=b } . Analog, conform regulii R2 se deduce
funcția de apartenență aferentă ieșirii risk=medium, conform regulii
µrisk / medium / R 2 = min{µage / normal = a , µcar − power / medium=b } . În consecință, se superpoziționeză

485
µ risk / high / R1 cu µ risk / medium / R 2 (v. figura următoare). Se calculează apoi centrul de
greutate aferent funcției de apartenență rezultată prin super-poziție.
Așadar, în urma proceselor de inferențe logice (date de regulile logice
fuzzy R1 și R2) și defuzificare, rezultă valoarea concretă a riscului pentru
valorile concrete age=a și car-power=b, astfel (punctul c reprezintă centrul de
greutate al funcției de apartenență rezultate):

Defuzificarea se face pe baza determinării centrului de greutate (Center of


Gravity - COG) al funcției de apartenență a obiectivului (obiectivul risk, în cazul

prezentat), dat de relația COG = ∫ x ⋅ µ ( x)dx , aproximată prin formula


∫ µ ( x)dx
max_ x

∑x
j =0
j ⋅ µ j (x j )
COG approx = max_ x
, unde µ (x ) este funcția de apartenență rezultată în urma
∑µ
j =0
j (x j )

proceseleor de inferențe logice fuzzy.


În consecință, operatorul de mutație a fost modificat astfel (pseudo-cod):

486
1. Pentru toți parametrii dintr-un cromozom individual;
1.1. Dacă există o regulă în logică fuzzy pentru parametrul curent care să îl
aibă pe acesta consecvent (adică, urmează după THEN în regulă);
1.1.1. Calculează centrul de greutate - Center Of Gravity (COG) al acestui
parametru, luând în considerare valorile curente ale celorlalți parametri;
1.1.2. Calculează valoarea funcției de apartenență µ (COG ) a COG;
1.1.3. Generează un număr cvasi-aleator între 0 și 1;
1.1.4. Dacă numărul generat aleator anterior este mai mic decât
probabilitatea de a efectua o mutație tip fuzzy;
1.1.4.1. Parametrul curent este setat la o valoare egală cu COG;
1.1.5. Jump la următoarea iterație;
1.2. Altfel (bit flip mutation);
1.2.1. Generează un număr aleator între 0 și 1;
1.2.2. Dacă numărul generat aleator anterior este mai mic decât
probabilitatea mutației normale;
1.2.2.1. Modifică valoarea parametrului curent la o valoare aleatoare;
1.2.3. Jump la următoarea iterație;
2. STOP.

O paranteză. Gradul de contradicție între reguli exprimate în logici fuzzy

Scopul este acela de a analiza câteva posibile grade de contradicție între două
reguli exprimate în logici de tip fuzzy. Pe această bază, s-ar putea propune
implementări software utile, în special în cercetări referitoare la optimizarea
multi-obiectiv automată a unor sisteme complexe de calcul. În particular,
calculul automat al gradelor de contradicție aferente unor ontologii de domeniu,
reprezentate prin reguli exprimate în logici fuzzy, este de un interes științific
cert. Să considerăm două reguli în logică clasică, scrise în forma normală
conjunctivă, ca mai jos:

R1: IF A1=5 AND A2=9 THEN O=1,


R2: IF A1=5 AND A2=9 THEN O=0

487
Este evident că cele două reguli sunt contradictorii, pentru că au intrări
identice și ieșiri diferite. Această metaforă a contradicției va fi folosită și pentru
a stabili gradul de contradicție între două reguli logice de tip fuzzy, Ri și Rj.

Ri: IF A1=V(A1i) AND A2=V(A2i) AND … AND Am=V(Ami) THEN O=V(Oi),

unde V(A1i), V(A2i), …, V(Ami) sunt funcții de apartenență de tip fuzzy, asociate
variabilelor lingvistice A1, A2, …, Am respectiv, iar V(Oi) reprezintă funcția de
apartenență de tip fuzzy a variabilei de ieșire Oi. Analog:

Rj: IF A1=V(A1j) AND A2=V(A2j) AND … AND Am=V(Amj) THEN O=V(Oj).

În acest context, are sens definirea unui grad de contradicție între Ri și Rj,
prin analogie cu logica clasică. Acest grad de contradicție – notat C(Ri, Rj) – s-ar
putea defini sub forma:
m

∑ S (V(A i
k ), V(A kj ))
C(Ri, Rj)= S(Ii, Ij).D(V(Oi), V(Oj)) = k =1 .
D(V(Oi), V(Oj))
m
unde S (V(A ik ), V(A kj )) reprezintă o metrică de similaritate între două mulțimi
fuzzy (aferente antecedenților din reguli, sau intrărilor regulilor cu alte cuvinte -
Ii, Ij), iar D reprezintă o metrică de disimilaritate între două mulțimi fuzzy.
Așadar, similaritatea / disimilaritatea a două mulțimi fuzzy pot reprezenta gradul
de contradicție (necontradicție) între două reguli exprimate în logică fuzzy. Dacă
S și D sunt normalizate în intervalul [0, 1], rezultă că D(x, y)=1-S(x, y), ∀ x,
y∈X.
m

∑ S (V(A
k =1
i
k ), V(A kj ))
S-a notat S(Ii, Ij)=
m
Metricile de similaritate îndeplinesc proprietăți precum simetria (S(x,
y)=1 ⇔ x=y) și reflexivitatea (S(x,y) = S(y,x)). De asemenea, este necesar ca ∀
x,y,z∈X S(x,z) ≤ S(x,y) + S(y,z). Este evident că C(Ri, Rj)=1 dacă și numai
dacă S(Ii, Ij)=1 (similaritate pe intrări) și D(V(Oi), V(Oj))=1 (disimilaritate pe
ieșiri).

488
Unii autori au definit disimilaritățile între două mulțimi fuzzy – notate de
noi cu D(V(A ik ), V(A kj )) ∈ [0, 1] – ca fiind modulul normalizat al diferențelor
ariilor definite de cele două funcții de apartenență. În acest caz, similaritatea este
complementară, anume S (V(A ik ), V(A kj )) =1- D(V(A ik ), V(A kj )) . Alți autori au
definit similaritatea (S) pe baza distanței între două mulțimi fuzzy - d(A,B).
Astfel, spre exemplu, S(A,B)=1/(1+d(A,B)). O problemă interesantă ar fi
următoarea. Cum am putea calcula, în mod algoritmic, gradul de contradicție
între N (N>2) reguli exprimate în logică fuzzy? Dacă acest grad este mai mare
decât un prag admisibil, cum l-am putea reduce? Prin eliminarea unor reguli?
Dacă da, care ar fi acestea? Prin modificarea unor reguli? Cum? Etc. Detalii sunt
disponibile în lucrarea noastră VINȚAN L. - Degrees of Contradiction for Fuzzy
Logic Rules implementing Computer Architecture Ontologies (Grade de
contradictie pentru ontologii de domeniu reprezentate prin logici fuzzy), Revista
Română de Informatică şi Automatică, ISSN: 1220-1758, Editura ICI,
Bucuresti, vol. 23, nr. 3, pg. 23-26, 2013.
O altă problemă deschisă, formulată de autorul acestei lucrări, este
următoarea. Nu cumva mulțimile Pareto sunt mai degrabă mulțimi de tip
fuzzy, decât mulțimi clasice? Prin mulțime Pareto înțelegem exclusiv mulțimea
indivizilor de pe frontul Pareto (hiper-suprafața Pareto), nicidecum, deci,
indivizi din afara acestuia. Astfel, spre exemplu, dacă se consideră un set de 20
de obiective și doi indivizi din mulțimea Pareto, notați cu Si și Sj, s-ar putea ca
Si să-l domine pe Sj din punct de vedere a doar două obiective, iar Sj să-l
domine pe Si din punct de vedere al celorlalte 18 obiective. Gradul de dominare
(superioritate) al lui Si față de Sj ar fi Grad (Si,Sj)=2/20=0.1, iar Grad
(Sj,Si)=18/20=0.9. Este clar, într-un asemenea caz, că Sj este (net) mai bun
decât Si, deși în optimizarea Pareto sunt considerați egali. Astfel, s-ar putea
calcula gradele de superioritate între oricare doi indivizi (Si, Sj) din mulțimea
Pareto (prin n(n-1)/2 comparații). În acest caz, cum s-ar putea calcula gradul de
apartenență la mulțimea Pareto a unui individ oarecare Si? (Evident, mulțimea
Pareto în această abordare este una de tip fuzzy). Poate că metoda numită
Analytic Hierarchy Process, atribuită lui Tom Saaty, ar putea da un răspuns la o
asemenea întrebare – v. http://www.dii.unisi.it/~mocenni/Note_AHP.pdf. Cred
că un astfel de răspuns ar putea fi extrem de fertil din punct de vedere științific,
pentru că ar putea sta la baza optimizării Pareto de tip fuzzy, care ar constitui

489
un model original, interesant și util, în opinia autorului acestei cărți.
Desigur că această mulțime Pareto (front) de tip fuzzy va influența semnificativ
procesele de selecție din cadrul algoritmilor genetici multi-obiectiv de
optimizare.
Revenind la problema noastră, s-a arătat în mod convingător, că astfel de
cunoștințe de domeniu implementate, în algoritmii genetici multi-obiectiv,
conduc la soluții mai bune, dar și la un timp de rulare mai scăzut [Cal11, Jah12,
Gel12, Jah15].

0.5

0.45

0.4
CPI

0.35

0.3

0.25
7.00E+09 1.20E+10 1.70E+10 2.20E+10 2.70E+10 3.20E+10 3.70E+10 4.20E+10 4.70E+10

Energy
Run without fuzzy Fuzzy with constant probability Manual

Figura 4.57. Fronturile Pareto cu și fără cunoștințe de domeniu (fuzzy)

În Figura 4.57 se arată fronturile Pareto într-un spațiu bi-obiectiv (CPI –


Clocks Per Instruction și respectiv energia consumată), obținute automat, cu și
fără cunoștințe de domeniu (exprimate prin reguli logice fuzzy). Se remarcă
faptul că domeniul specific de cunoștințe a generat un front Pareto mai bun, în
special pentru valori medii ale celor două obiective. Tot în această figură se
poate remarca și frontul Pareto obținut prin optimizare manuală, efectuată de
experți umani. Se observă, în acest caz, că soluțiile obținute sunt de calitate mai
slabă decât cele obținute prin optimizare automată. În plus, spațiul de căutare al
expertului uman este unul mult redus, ceea ce arată că intuiția nu este
întotdeauna eficientă. Acest studiu de caz s-a realizat pe o arhitectură

490
superscalară / SMT, augmentată cu un predictor de valori pentru instrucțiunile
Load critice (cu miss în sub-sistemul de cache-uri) [Gel12].

Figura 4.58. Schema boc a utilitarului FADSE de optimizare multi-


obiectiv

În Figura 4.58 se prezintă schema bloc a utilitarului FADSE de optimizare


multi-obiectiv (v. https://github.com/horiacalborean/fadse), sub forma unui
cadru software gratuit, dezvoltat în grupul de cercetare condus de autorul acestei
cărți, la Universitatea ”Lucian Blaga” din Sibiu (v. http://acaps.ulbsibiu.ro/) și
care înglobează toate metodele succint expuse anterior [Cal10, Cal11]. Acest
instrument este denumit FADSE (Framework for Automatic Design Space
Exploration). Scopul lui este de a accelera procesul de optimizare automat, nu
doar prin utilizarea algoritmilor euristici multi-obiectiv, ci şi prin permiterea
evaluării paralele / distribuite a configuraţiilor. Aplicaţia integrează şi o bază de
date, care îi permite reutilizarea rezultatelor obţinute anterior (indivizi care au
fost deja simulaţi), conducând deci la o scădere a timpului necesar pentru
explorare. Arhitectura cadrului FADSE este una de tip client – server. Pe server
rulează algoritmii multi-obiectiv de optimizare, iar pe partea de client,
simulatorul de optimizat. Cele două părți interacționează prin intermediul unui
conector specific. FADSE permite evaluarea paralelă a indivizilor, prin calcul

491
paralel (multicore, High Performance Computer) sau distribuit (rețele LAN).
Așadar, mai multe instanțe ale simulatorului de optimizat pot rula în paralel, în
vederea optimizării. În cadrul grupului de cercetare condus de autorul acestei
cărți, s-au realizat evaluări ale arhitecturilor de calcul vizate, prin simulări
paralele, efectuate pe un High Performance Computer având 30 de procesoare
de tipul Intel Xeon E5405 quad cores omogene (deci, conținând 120 de nuclee
de procesare, care lucrează la o frecvență de tact de 2 GHz), implementate pe 15
plăci (blades). Pe fiecare placă (care conține deci două quad-core-uri) există
4.84 GB de memorie DRAM. Capacitatea de stocare pe disc a HPC-ului este de
cca. 1.2 TB. HPC-ul rulează un sistem de operare de tipul RedHat Enterprise 5.4
Linux Operating System.
FADSE se configurează printr-un fișier XML. Din acest fișier se citesc
parametrii simulatorului de optimizat, configurația acestuia etc. Există o
componentă în FADSE care încarcă din biblioteca jMetal algoritmul de
optimizare specificat în fisierul XML.

Figura 4.59. Schema bloc a unui proces de meta-optimizare

Figura 4.59 prezintă în mod intuitiv procesul de meta-optimizare


(optimizare hibridă efectuată prin intermediul mai multor algoritmi euristici,
care lucrează în paralel) dezvoltat de autor și colaboratorii săi în lucrarea
[Vin15]. Populația master curentă, notată cu PM (t ) , este evaluată în mod automat,
utilizând un simulator dedicat arhitecturii de calcul vizate. După acest proces, în
general mare consumator de timp (din fericire poate fi paralelizat / distribuit,
după cum am arătat), fiecare individ va avea asociată o rată de fitness pentru

492
fiecare obiectiv. Această populație evaluată, notată cu PM' (t ) , este procesată în
paralel de fiecare dintre cei doi algoritmi genetici multi-obiectiv utilizați, notați
cu A (NSGA-II în cazul concret) și B (SPEA2 - Strength Pareto Evolutionary
Algorithm), prin intermediul propriilor operatori genetici. Algoritmul de
optimizare A va genera prin aplicarea operatorilor genetici indivizi
(offsprings). Într-un mod analog, algoritmul B va genera și el
= indivizi. Toți acești N noi indivizi produși, vor fi apoi
evaluați, în mod automat, prin intermediul simulatorului arhitecturii de calcul
vizate. Astfel, se stabilesc valorile obiectivelor acestor indivizi. După acest
proces complex de evaluare, fiecare dintre aceste două seturi de noi indivizi se
vor reuni cu populația master curentă PM' (t ) . Apoi, prin intermediul mecanismelor
specifice de selecție implementate în algoritmii NSGA-II (A) și SPEA2 (B), vor
fi obținuți cei mai performanți indivizi din această generație (t), numiți NSAt și
NSBt. Aceștia, reuniți, vor forma noua populație master PM' (t +1) . S-a considerat că
fiecare generație conține un număr de N=NSAt+NSBt indivizi distincți
(cromozomi). Această nouă populație master PM' (t +1) este apoi evaluată global,
din punct de vedere calitativ, prin intermediul indicatorilor de calitate prezentați
anterior, anume Two Set Difference Hyper-volume (TSDH) și Coverage (C).
Considerând acești doi algoritmi de optimizare (A și B), performanța globală (ρ)
reprezintă o agregare între cei doi indicatori de calitate ( ) și Coverage ( ),
după cum urmează:

,
,
and
, unde “t” reprezintă indexul generației numărul t.

Din păcate, procesul de selecție a indivizilor din cele două populații


combinate, poate conduce la duplicări ale acestora (populația rezultată nu
conține doar indivizi distincți). O soluție elegantă la această problemă este
prezentată în [Vin15]. Conform acesteia, cei mai buni N indivizi selectați pot fi
grupați, în acord cu cei doi algoritmi de optimizare, în NSelA indivizi selectați

493
numai de algoritmul A, respectiv NSelB indivizi selectați exclusiv de algoritmul
B și NSelAB indivizi selectați de ambii. Dacă vom considera că algoritmul A a
selectat un număr de NSA indivizi într-o anumită generație și că algoritmul B a
selectat NSB indivizi, se pot scrie identitățile:

NSA=NSelA+NSelAB
NSB=NSelB+NSelAB

În total s-au obținut NSelA+NSelB+2NSelAB=N indivizi, însă numai


NSelA+NSelB+NSelAB dintre aceștia sunt unici. Rezultă că un număr de NSelAB
dintre ei sunt duplicați. În consecință, este necesar să fie selectați în continuare
încă NSelAB indivizi, astfel încât în final să se obțină N soluții unice. Este necesar
să se determine ca algoritmul A să selecteze nNSelA indivizi în plus și, analog,
algoritmul B să selecteze nNSelB indivizi adiționali. La sfârșit, fiecare algoritm a
selectat nNSA, respectiv nNSB, indivizi, astfel încât:

nNSA= NSA+ nNSelA=NSA+ ρ A NSelAB


nNSB= NSB+ nNSelB=NSA+ ρ B NSelAB
nNSA+ nNSB=N

Evident, duplicările pot apărea din nou. În acest caz, soluția prezentată
anterior se va aplica iterativ, până când, în final, se vor selecta N indivizi unici.
Această condiție va fi sigur îndeplinită, întrucât în cel mai defavorabil caz
PM ( t +1) = PM (t ) .
S-a demonstrat că metoda de meta-optimizare propusă generează rezultate
mai bune decât cele două metode individuale, precum și în comparație cu
fronturile Pareto superpoziționate, generate de fiecare dintre cele două metode
de optimizare folosite. Mai mult, aceste rezultate superioare au fost obținute
simulând doar jumătate din indivizi, în comparație cu metodele individuale.
Practic: HV(Meta-Optimizare)>HV(A ∨ B), unde HV = hiper-volum, A (NSGA-
II) și B (SPEA2) iar A ∨ B semnifică reuniunea populațiilor generate de algoritmii
A și B. Arhitectura țintă de optimizat a fost în acest caz procesorul numit Grid
ALU Processor (GAP). Detalii se pot găsi în lucrarea [Vin15].

494
Practic, este vorba în cadrul sistemelor meta-algoritmice despre un
concept extrem de delicat, anume acela de sinergie, semnificând, în esență,
faptul că interacțiunea algoritmilor utilizați genereaza efecte mai bune decât
superpoziția efectelor individuale ale acestor algoritmi. Conceptul acesta nu pare
a fi dezvoltat într-un mod deosebit de matur, matematic riguros, în literatura de
specialitate. Este clar ca sinergia este un atuu potențial al sistemelor neliniare,
pentru că în cazul celor liniare, principiul superpoziției se aplică, deci sinergia
nu este posibilă în acele cazuri.
Reamintim succint noțiunea de sistem liniar. Fie un sistem determinist F,
care transformă o intrare x(t) într-o ieșire y(t), t=timpul. Prin definiție, funcția de
transfer a sistemului este H(t)=y(t)/x(t). Deseori, pentru a evita lucrul cu ecuații
diferențiale sau/și integrale, se lucrează în spațiul Laplace, utilizând
transformata cu același nume – H(s). În cazul sistemelor discrete se folosește
transformata Z. Considerând două semnale de intrare x1(t) și x2(t), evident că
avem ieșirile corespunzătoare y1(t)=F(x1(t)) respectiv y2(t)=F(x2(t)). Sistemul
este liniar dacă pentru orice scalari a și b (reali), avem îndeplinită identitatea:

a y1(t) + by2(t) = F(ax1(t) + bx2(t)); superpoziție


p =t
dx(t )
Spre exemplu, funcția F(t)=C1 x(t)+ C2
dt
+ C3 ∫ x( p)dp este o funcție
−∞

liniară.

Figura 4.60. Hiper-volumul meta-optimizării este sinergic

495
Facem aici o paranteză, sperăm utilă. Ideea meta-optimizării sau a unor
metode hibride de optimizare, este oarecum conexă cu cea a predictoarelor
hibride de salturi condiționate, prezentată în Capitolul 3 al lucrării. Mai general,
ideea de a dezvolta metode meta-algoritmice, care să orchestreze mai mulți
algoritmi utili în rezolvarea unei probleme dificile, este una naturală, atât timp
cât fiecare metodă (algoritm) are propriile-i avantaje dar și dezavantaje.
Iată, spre exemplu, ideea de meta-clasificare. Figura 4.61 prezintă o
posibilă schemă pentru un sistem generic de meta-clasificare a documentelor
[Vin16]. Acesta conține m clasificatoare distincte (hibride – ex. Support Vector
Machine, Naïve Bayes, neuronale, arbori de decizie etc.) Fiecare clasificator va
clasifica documentul de intrare într-una din cele n posibile clase de ieșire. Spre
exemplu, ieșirea unui clasificator ar putea fi reprezentată printr-un vector
conținând n scalari; fiecare scalar reprezintă probabilitatea ca documentul
respectiv să aparțină uneia dintre cele n clase posibile. Ca un caz extrem, un
scalar ar putea fi un singur bit. În acest caz, dacă un anumit clasificator (i)
clasifică documentul de intrare ca aparținând clasei numărul k, bitul Vik=1 și toți
ceilalți biți din vector Vij=0, oricare ar fi j, j≠k, unde i,j,kє{1,2,…,n}. Toate cele
m ieșiri ale clasificatoarelor (vectori) vor fi intrări în cadrul sub-sistemului de
meta-clasificare. Acesta conține de fapt două meta-clasificatoare cascadate.
Unul de tip neadaptiv (spre exemplu un simplu voter sau un circuit de selecție cu
priorități fixe) iar altul, de tip adaptiv (spre exemplu, o rețea neuronală de tip
perceptron cu mai multe niveluri și cu un algoritm de învățare supervizată).
Meta-clasificatorul neadaptiv comprimă printr-o metodă de agregare cei n
vectori (V1 la Vn) într-unul singur, numit AV (Average Vector). Acesta este o
intrare în meta-clasificatorul adaptiv care clasifică documentul în una din cele n
posibile clase (C1 la Cn). Așadar, meta-clasificatorul final va încerca să prezică
clasa corectă pentru documentul curent. Spre exemplu, acest meta-clasificator
final poate genera un vector binar care să conțină un singur bit de 1 logic, anume
acela corespunzător clasei căreia îi aparține documentul. Un avantaj semnificativ
al meta-clasificatorului cu învățare supervizată constă în posibilitatea antrenării
sale offline. Desigur că în acest caz sunt necesare actualizări ale meta-
clasificatorului, atunci când se știe dacă clasificarea făcută a fost, sau nu,
corectă. Performanța globală a unui asemenea sistem de clasificare nu este dată
doar de acuratețea clasificărilor, ci și de timpul de răspuns. Utilizând o asemenea

496
schema generică se poate înțelege practic orice sistem particular de clasificare
hibridă. Detalii asupra unei asemenea cercetări pot fi găsite în lucrarea:
CRETULESCU R., MORARIU D., VINȚAN L., COMAN I. D. – An Adaptive
Meta-classifier for Text Documents, The 16th International Conference on
Information Systems Analysis and Synthesis (ISAS 2010), vol. 2, pp. 372-377,
ISBN-13: 978-1-934272-88-6, Orlando Florida, USA, April 6th – 9th 2010
[Cre10].

Figura 4.61. Sistem meta-clasificare documente

497
6. PROBLEME PROPUSE SPRE REZOLVARE

Parte a acestor probleme propuse au fost preluate din cartea autorului


[Vin00b]. Multe alte probleme sunt publicate pentru prima dată în acest curs.
1. Considerăm un procesor scalar pipeline cu 5 nivele de procesare (IF, ID,
ALU, MEM, WB) şi o secvenţă de două instrucţiuni succesive şi dependente
RAW, în două ipostaze:
A. i1: LOAD R1, 9(R5)
i2: ADD R6, R1, R3

B. i1: ADD R1, R6, R7


i2: LOAD R5, 9(R1)
a. Stabiliţi cu ce întârziere (Delay Slot) startează a doua instrucţiune ?
b. Aceași întrebare, dacă se aplică mecanismul de forwarding ?
În acest caz, pentru secvenţa B, cât ar fi fost întârzierea dacă în
cazul celei de a doua instrucţiuni, modul de adresare nu ar fi fost indexat,
ci doar indirect registru ? Comentaţi.
c. Verificaţi rezultatele obţinute pe procesorul DLX (DLX simulator –
[Flo03]). Determinaţi creşterea de performanţă obţinută aplicând
mecanismul de forwarding [(IRCu forwarding - IRFara forwarding) / IRFara forwarding].

Obs. Se consideră că operanzii instrucţiunilor sunt necesari la finele fazei ID, iar
rezultatele sunt disponibile în setul de regiştri generali la finele fazei WB.

2. Scrieţi o secvenţă de program asamblare RISC, care să reprezinte translatarea


corectă a programului scris în limbaj C, de mai jos. Iniţial, regiştrii Ri, Rk, Rl,
Rj, Rm conţin respectiv variabilele i, k, l, j şi m.
K = X[i-4]+12;
L = Y[j+5] XOR K;
M = K AND L;

498
Se consideră programul executat pe un procesor scalar pipeline RISC cu 4
nivele de procesare (IF, ID, ALU/MEM, WB) şi operanzii instrucţiunilor sunt
necesari la finele fazei ID, iar rezultatele sunt disponibile în setul de regiştri
generali la finele fazei WB. Se cere:
a. Reprezentaţi graful dependenţelor de date.
b. În câte impulsuri de tact se execută secvenţa de program asamblare ?
c. Reorganizaţi această secvenţă în vederea minimizării timpului de
execuţie (se consideră că procesorul deţine o infinitate de regiştri
generali).
d. Aplicând tehnica de forwarding, în câte impulsuri de tact se execută
secvenţa reorganizată ?

3. Se consideră o arhitectură superscalară caracterizată de următorii parametri:

FR = 4 instr. / ciclu; – nr. de instrucţiuni citite simultan din cache-ul de


instrucţiuni sau din memoria de instrucţiuni, în caz de
miss în cache.
IRmax = 2 (respectiv 4) instr. / ciclu; – nr. maxim de instrucţiuni independente
lansate simultan în execuţie.
N_PEN = 10 impulsuri de tact; – număr impulsuri de tacte de penalizare
necesare accesului la memoria de instrucţiuni, în
caz de miss în cache.
Latenţa = 2 impulsuri de tact; – nr. impulsuri de tacte necesare execuţiei pentru
orice tip de instrucţiune, lansată din buffer-ul de
prefetch (se consideră unităţi de execuţie
nepipeline-izate).
IBS = 8 locaţii (instrucţiuni); – dimensiunea buffer-ului de prefetch.
RmissIC = 40%; – rata de miss în cache-ul de instrucţiuni (din 5 citiri din I-
Cache, primele două sunt cu miss).
Considerăm următoarea secvenţă succesivă de 20 instrucţiuni, caracterizată
de hazarduri RAW aferente, executată pe arhitectura dată.

i1 – i2 – i3 – RAW – i4 – i5 – i6 – RAW – i7 – i8 – i9 – i10 – i11 – RAW –


i12 – i13 – i14 – i15 – i16 – i17 – RAW – i18 – i19 – i20. (în continuare “nu mai
sunt instrucţiuni de executat”.)

499
Obs. În cadrul unui ciclu de execuţie se realizează următoarele: din partea
inferioară a buffer-ului de prefetch sunt lansate maxim IRmax instrucţiuni
independente, iar simultan, în partea superioară a buffer-ului, sunt aduse,
dacă mai există spaţiu disponibil, FR instrucţiuni din cache-ul sau memoria
de instrucţiuni.
Determinaţi creşterea de performanţă (studiu asupra ratei medii de
procesare) prin varierea parametrului IRmax de la 2 la 4 instrucţiuni / ciclu.
Prezentaţi în fiecare ciclu de execuţie conţinutul buffer-ului de prefetch.

4. Considerăm un procesor RISC scalar pipeline caracterizat, printre altele, de


următoarele instrucţiuni:

ADD Ri, Rj, Rk – al doilea operand poate fi şi valoare imediată


LD Ri, adresă
ST adresă, Ri
MOV Ri, Rj
BEQ Ri, Rj, label
BNE Ri, Rj, label
J label

a. Acest procesor execută următoarea secvenţă:

ST (R9), R6
LD R10, (R9)

Rescrieţi această secvenţă folosindu-vă de instrucţiunile cunoscute pentru a


elimina ambiguitatea referinţelor la memorie apărută în secvenţa originală
dată şi pentru a le executa mai rapid.

b. Se dă secvenţa de instrucţiuni de mai jos:

ST 4(R5), R8
LD R9, 8(R6)

Realizaţi o nouă secvenţă, cât mai rapidă în procesare, care să o înlocuiască


în mod corect pe cea de mai sus şi care să elimine posibila ambiguitate a
referinţelor la memorie, favorizând execuţia instrucţiunii LD înaintea lui ST.

500
5. Dându-se următoarele secvenţe de instrucţiuni, care implică dependenţe reale
de date (RAW), să se rescrie aceste secvenţe, cu un număr minim de
instrucţiuni şi folosind doar aceiaşi regiştri (eventual R0 = 0 suplimentar), dar
eliminând dependenţele respective.

Obs. Unele instrucţiuni pot rămâne nemodificate.

a) MOV R6, R7
ADD R3, R6, R5
b) MOV R6, #4
ADD R7, R10, R6
LD R9, (R7)
c) MOV R6, #0
ST 9(R1), R6
d) MOV R5, #4
BNE R5, R3, Label
e) ADD R3, R4, R5
MOV R6, R3

6. Se consideră o arhitectură superscalară caracterizată de următorii parametri:

FR = 4 (respectiv 8) instr. / ciclu; – nr. instrucţiuni citite simultan din cache-ul


de instrucţiuni sau memoria de instrucţiuni, în caz
de miss în cache.
IRmax = 4 instr. / ciclu; – nr. maxim de instrucţiuni independente lansate
simultan în execuţie.
N_PEN = 10 impulsuri de tact; – nr. impulsuri de tacte de penalizare necesar
accesului la memoria de instrucţiuni, în caz de
miss în cache.
Latenţa = 2 impulsuri de tact; – nr. impulsuri de tacte necesare execuţiei pentru
orice tip de instrucţiune, lansată din buffer-ul de
prefetch (unităţi de execuţie nepipeline-izate).
IBS = 8 locaţii (instrucţiuni); – dimensiunea buffer-ului de prefetch.
RmissIC = 50% (iniţial); – rata de miss în cache-ul de instrucţiuni (din două
citiri din I-Cache, prima se va considera cu miss).

501
Pe arhitectura dată se procesează următoarea secvenţă succesivă de 32
instrucţiuni, caracterizată de hazarduri RAW aferente.

i1 – i2 – i3 – RAW – i4 – i5 – i6 – RAW – i7 – i8 – i9 – i10 – i11 – RAW –


i12 – i13 – i14 – i15 – i16 – i17 – RAW – i18 – i19 – i20 – i21 – i22 – RAW –
i23 – i24 – i25 – i26 – i27 – RAW – i28 – i29 – i30 – i31 – RAW – i32.

Obs. În cadrul unui ciclu de execuţie se realizează următoarele: din partea


inferioară a buffer-ului de prefetch sunt lansate maxim IRmax instrucţiuni
independente, iar simultan, în partea superioară a buffer-ului sunt aduse,
dacă mai este spaţiu disponibil, FR instrucţiuni din cache-ul sau memoria
de instrucţiuni.
Determinaţi creşterea de performanţă (studiu asupra ratei medii de
procesare) prin varierea parametrului FR de la 4 la 8 instrucţiuni / ciclu. De
menţionat că această variaţie a FR de la 4 la 8 instrucțiuni, implică diminuarea
ratei de miss în cache-ul de instrucţiuni cu 50%. Prezentaţi în fiecare ciclu de
execuţie conţinutul bufferului de prefetch. În ce constă limitarea performanţei în
acest caz?

7. Se consideră următoarea secvenţă de instrucţiuni, executată pe un procesor


pipeline cu 4 nivele (IF, ID, ALU/MEM, WB), fiecare fază necesitând un tact,
cu următoarea semnificaţie:
IF = citirea instrucţiunii din cache-ul de instrucţiuni sau din memorie
ID = decodificarea instrucţiunii şi citirea operanzilor din setul de regiştri
generali
ALU / MEM = execuţie instrucţiuni aritmetice sau accesare memorie
WB = înscriere rezultat în registrul destinaţie
Operanzii instrucţiunilor sunt necesari la finele fazei ID, iar rezultatele sunt
disponibile în setul de regiştri generali, la finele fazei WB.

i1: ADD Ri, R0, #i


i2: ADD Rj, Ri, #4
i3: LOAD R1, (Ri)
i4: LOAD R2, (Rj)
i5: ADD R3, R1, R2
i6: SUB R4, R1, R2
i7: ABS R4, R4

502
i8: ADD R1, R13, R14
i9: DIV R1, R1, #2
i10: STORE (Ri), R1

Se cere:
a. Reprezentaţi graful dependenţelor de date (RAW, WAR, WAW) şi
precizaţi în câte impulsuri de tact se execută secvenţa? Iniţial, structura
“pipeline” de procesare este goală, iar regiştrii sunt iniţializaţi cu 0.
b. Reorganizaţi această secvenţă în vederea minimizării timpului de
execuţie (se consideră că procesorul deţine o infinitate de regiştri
generali disponibili). În câte impulsuri de tact s-ar procesa, în acest caz,
secvenţa ?
c. Ce simulează secvenţa iniţială dacă în instrucţiunea i8, în locul
regiştrilor R13 am avea R3, iar în locul lui R14 am avea R4. Precizaţi
formula matematică obţinută.

8. Se consideră secvenţa de program RISC:

i1: ADD R1, R2, #15


i2: ADD R3, R4, #17
i3: ADD R 5 , R3 , R1
i4: ADD R6, R5, #12
i5: ADD R3, R7, #3
i6: ADD R8, R3, #2
i7: ADD R9, R8, #14

Se cere:
a. Să se construiască graful dependenţelor de date (RAW, WAR, WAW)
aferent acestei secvenţe şi să se precizeze în câte impulsuri de tact se
execută secvenţa, ştiind că latenţa de execuţie a instrucţiunii ADD este
de un ciclu.
b. Să se determine modul optim de execuţie al acestei secvenţe
reorganizate, pe un procesor RISC superscalar cu 6 seturi fizice de
regiştri generali şi 3 unităţi ALU.

9. Se consideră un microprocesor RISC cu o structură «pipeline» de procesare a


instrucţiunilor, având vectorul de coliziune ataşat 101011. Să se determine

503
rata teoretică optimă de procesare a instrucţiunilor pentru acest procesor
[instr./ciclu] şi să se expliciteze algoritmul după care trebuie introduse
instrucţiunile în structură.

10. Fie vectorul de coliziune 1001011 ataşat unui microprocesor RISC cu o


structură «pipeline» de procesare a instrucţiunilor. Să se determine rata
teoretică optimă de procesare a instrucţiunilor pentru acest procesor
[instr./ciclu] şi explicitând algoritmul după care trebuie introduse
instrucţiunile în structură. Determinaţi creşterea de performanţă comparativ
cu situaţia în care s-ar procesa pe o structură CISC convenţională [(IRRISC -
IRCISC) / IRCISC].

11.a) În cadrul unui procesor vectorial se consideră următoarea secvenţă de


program:
x=0
for i = 1 to 100 do
x = x + A[i]*B[i];
endfor

În câţi cicli de tact se execută secvenţa ? Este bucla vectorizabilă ? În


caz negativ, scrieţi o nouă secvenţă de program care să aibă acelaşi efect,
dar care să fie executată mai rapid ? Determinaţi noul timp de execuţie al
secvenţei ? Concret, din ce motive se câştigă timp de procesare ?
b) Să se proiecteze un cache de instrucţiuni cuplat la un procesor superscalar
(VLIW). Lungimea blocului din cache se consideră egală cu rata de fetch a
procesorului, în acest caz 8 instrucţiuni / bloc. Cache-ul va fi de tipul 4 way
set associative (cu 4 blocuri / set). Explicaţi semnificaţia fiecărui câmp
utilizat (necesar) în proiectare.
c) Descrieţi avantajelor introduse de Tomasulo în cadrul arhitecturii care îi
poartă numele.

12.a) De ce nu este suficientă doar optimizarea unităţilor secvenţiale de program


(“basic-blocks”), fiind necesară optimizarea globală, a întregului program?
b) Care sunt motivele pentru care microprocesoarele superscalare au un
succes comercial net superior celor cu paralelism exploatat prin optimizări
de program (“Scheduling” static) – de exemplu microprocesoare tip VLIW,

504
EPIC etc., în cadrul sistemelor de calcul de uz general (laptopuri, desk-top-
uri, servere etc.) ?

13. Relativ la gestiunea memoriei în sistemele moderne, trataţi într-o manieră


concisă următoarea problematică:
a. În ce rezidă necesitatea unui sistem de memorie virtuală (MV) ?
b. Explicaţi principiile protecţiei în sistemele cu MV ?
c. Explicaţi succint rolul TLB (Translation Lookaside Buffer) în
translatarea adreselor. S-ar putea implementa MV fără TLB?
d. În ce constă dificultăţile implementării unui cache virtual ? Care ar fi
avantajele acestuia ?

14.a) Care este semnificaţia termenilor:


Arhitectură RISC (Reduced Instruction Set Computer)
Arhitectură CISC (Complex Instruction Set Computer)
Care arhitectură este mai rapidă şi de ce (tehnologii echivalente) ?
b) Explicaţi semnificaţia nivelelor pipeline aferente unui microprocesor (IF,
ID, ALU, MEM, WB). Explicaţi avantajele introduse de conceptul de
procesare "pipeline".
c) Ce este o arhitectură VLIW (Very Long Instruction Word) ? Descrieţi
asemănările şi deosebirile dintre o arhitectură VLIW şi una superscalară.
Subliniaţi avantajele arhitecturii VLIW faţă de o arhitectură convenţională
de procesor.
d) Un microprocesor cu 4 nivele pipeline (IF, ID, ALU/MEM, WB) execută
următoarea secvenţă de cod:
ADD Adunare
JMP SUB1 Salt necondiţionat - apel subrutină
SUB Scădere
MUL Înmulţire
.
.
SUB1: Instrucțiune ; Intrarea în subrutină - adresa primei
instrucţiuni din subrutină
Ce se întâmplă când procesorul întâlneşte instrucţiunea de salt
necondiționat (situaţia instrucţiunilor în "pipeline")? Considerând că
procesorul primeşte o întrerupere, după ce execută instrucţiunea de salt,
explicaţi clar secvenţa de evenimente petrecute după recepţia întreruperii.

505
15. Considerăm un procesor pipeline cu 5 nivele (IF, ID, ALU, MEM, WB) în
care condiţia de salt este verificată pe nivelul de decodificare, operanzii
instrucţiunilor sunt necesari la finele fazei ID, iar rezultatele sunt disponibile
în setul de regiştri generali la finele fazei WB. Trasaţi diagrama ciclului de
tact a procesorului, incluzând dependenţele reale de date (RAW).
Reprezentaţi graful dependenţelor de date, la execuţia următoarei secvenţe de
instrucţiuni. Stabiliţi cu ce întârziere (Delay Slot) startează a doua
instrucţiune, în cazul existenţei unui hazard RAW între două instrucţiuni.
Care este timpul total de procesare al secvenţei? Subliniaţi toate tipurile de
hazarduri (de date, de ramificaţie), dacă şi unde poate fi aplicat mecanismul
de forwarding. Presupunând că instrucţiunea de salt condiționat este corect
predicţionată ca not-taken în câţi cicli de tact se va procesa secvenţa ?
ADD R1, R2, R3
LD R2, 0(R1)
BNE R2, R1, dest
SUB R5, R1, R2
LD R4, 0(R5)
SW R4, 0(R6)
ADD R9, R5, R4

16.a) În general, un microprocesor consumă relativ multe impulsuri de tact


pentru a citi o locaţie de memorie DRAM, datorită latenţei relativ mari a
acesteia. Implementarea căror concepte arhitecturale micşorează timpul
mediu de acces al microprocesorului la memoria DRAM ?
b) Care este principalul concept arhitectural prin care se micşorează timpul
mediu de acces al procesorului la discul magnetic ?

17. Să se proiecteze un cache de instrucţiuni cuplat la un procesor


superscalar (VLIW). Lungimea blocului din cache se consideră egală cu rata de
fetch a procesorului, în acest caz 4 instrucţiuni / bloc. Cache-ul va fi de tipul:
a) Semi-asociativ, cu 2 blocuri / set (two – way set associative)
b) complet asociativ (full - associative)
c) cu mapare directă (direct mapped)
Ce se întâmplă dacă în locul adresării cu adrese fizice se consideră adresare
cu adresă virtuală?

506
18.a) De ce este dificilă procesarea «Out of Order» a instrucţiunilor Load
respectiv Store într-un program şi de ce ar putea fi ea benefică?
b) Care dintre cele două secvenţe de program s-ar putea procesa mai rapid pe
un procesor superscalar cu execuţie «Out of Order» a instrucţiunilor?
Justificaţi.

B1. B2.
for i=1 to 100 a[2]=x[1];
a[2i]=x[i]; y[1]=a[2]+5;
y[i]=a[i+1]; for i=2 to 100
a[2i]=x[i];
y[i]=a[i+1]+5;

19. Se consideră un procesor scalar pipeline RISC, având 3 faze diferite de


procesare (IF,EX,WR), fiecare fază necesitând un tact, cu următoarea
semnificaţie:

IF = aducere şi decodificare a instrucţiunii


EX=selecţie operanzi din setul de regiştri şi execuţie
WR=înscriere rezultat în registrul destinaţie

Se consideră secvenţa de program:

1: R1<-- (R11)+(R12)
2: R1<-- (R1)+(R13)
3: R2 <-- (R3)+4
4: R2 <-- (R1)+(R2)
5: R1<-- (R14)+(R15)
6: R1<-- (R1)+(R16)

a) În câte impulsuri se execută secvenţa? (iniţial, structura «pipeline» de


procesare este «goală») Reorganizaţi această secvenţă de program în
vederea minimizării timpului de execuţie (se consideră că procesorul
deţine o infinitate de regiştri generali disponibili). În câte impulsuri de
tact s-ar procesa, în acest caz, secvenţa ?

507
b) În câte tacte (minimum) s-ar procesa secvenţa, dacă procesorul ar putea
executa simultan un număr nelimitat de instrucţiuni independente? Se
consideră că procesorul poate aduce în acest caz, simultan, 6 instrucţiuni
din memorie. Justificaţi.

20. Se consideră o structură «pipeline» de procesare a instrucţiunilor având un


nivel de citire a operanzilor din setul de regiştri (RD), situat anterior unui
nivel de scriere a rezultatului în setul de regiştri (WR). Căreia dintre cele două
operaţii (RD, WR) i se dă prioritate în caz de conflict şi în ce scop ?

21. Un procesor superscalar poate lansa în execuţie, simultan, maximum N


instrucţiuni ALU independente. Logica de detecţie a posibilelor hazarduri
RAW (Read After Write) între instrucţiunile ALU are costul «C». Cât va
costa logica de detecţie, dacă s-ar dori ca să se poată lansa simultan în
execuţie maxim (N+1) instrucţiuni ALU independente ? (Se vor considera
costurile ca fiind direct proporţionale cu «complexitatea» logicii de detecţie a
hazardurilor RAW).

22. Relativ la o memorie cache cu mecanism de adresare tip «mapare directă»,


precizaţi valoarea de adevăr a afirmaţiilor de mai jos, cu justificările de
rigoare.
a) Rata de hit creşte dacă și capacitatea memoriei creşte;
b) Data de la o anumită locaţie din memoria principală poate fi actualizată
la orice adresă din cache;
c) Scrieri în cache au loc numai în ciclurile de scriere cu miss în cache;
d) Are o rată de hit net mai mare decât cea a unei memorii complet
asociative şi de aceeaşi capacitate.

23. Se consideră secvenţa de program RISC:

1: ADD R1, R11, R12


2: ADD R1, R1, R13
3: ADD R2, R3, R9
4: ADD R2, R1, R2
5: ADD R1, R14, R15

508
a) Reprezentaţi graful dependenţelor de date (numai dependenţele de tip
RAW)
b) Ştiind că între două instrucţiuni dependente RAW şi succesive este
nevoie de o întârziere de doi cicli, în câţi cicli s-ar executa secvenţa ?
c) Reorganizaţi secvenţa în vederea unui timp minim de execuţie (nu se
consideră alte dependenţe decât cele de tip RAW).

24.a) Considerând un procesor RISC pe 5 nivele pipeline de procesare a


instrucțiunilor (IF, ID, ALU, MEM, WB), fiecare durând un ciclu, precizaţi
câţi cicli de întârziere («branch delay slot») impune o instrucţiune de salt
condiționat care determină adresa de salt la finele nivelului ALU ?
b) De ce se preferă implementarea unor busuri şi memorii cache separate pe
instrucţiuni, respectiv date, în cazul majorităţii procesoarelor RISC
(pipeline)?
c) De ce sunt considerate instrucţiunile CALL / RET mari consumatoare de
timp în cazul procesoarelor CISC (ex. x86) ? Cum se evită acest consum de
timp în cazul microprocesoarelor de tip RISC ?

25. Considerând un microprocesor virtual pe 8 biţi, având 16 biţi de adrese, un


registru acumulator A pe 8 biţi, un registru PC şi un registru index X, ambele
pe 16 biţi şi că opcode-ul oricărei instrucţiuni este codificat pe un octet, să se
determine numărul impulsurilor de tact necesare aducerii şi execuţiei
instrucţiunii «memorează A la adresa dată de (X+deplasament)». Se consideră
că instrucţiunea este codificată pe 3 octeţi şi că orice procesare (operaţie
internă) consumă două tacte. Un ciclu de fetch opcode durează 6 tacte şi orice
alt ciclu extern durează 4 tacte.

26. Relativ la o arhitectură de memorie cache cu mapare directă se consideră


afirmaţiile:

a) Nu permite accesul simultan la câmpul de date şi respectiv «tag» al unui


cuvânt accesat.
b) La un acces de scriere cu hit, se scrie în cache atât data de înscris cât şi
«tag-ul» aferent.

509
c) Rata de hit creşte uşor dacă două sau mai multe blocuri din memoria
principală - accesate alternativ de către microprocesor - sunt mapate în
acelaşi bloc din cache.

Stabiliţi valoarea de adevăr a acestor afirmaţii şi justificaţi, pe scurt,


răspunsul.

27. Ce corecţie (doar una!) trebuie făcută în secvenţa de program asamblare


pentru ca translatarea de mai jos să fie corectă şi de ce ? Iniţial, regiştrii Ri,
Rk, Rl, Rj conţin respectiv variabilele i, k, l, j. Primul registru după
mnemonică este destinaţie. (Rj+offset) semnifică operand în memorie la
adresa dată de (Rj+offset).

k = a[i+2]+5; i1: ADD Rk, #2, Ri


l = c[j+9] - k; i2: LOAD Rk, (Rk+0)
i3: ADD Rk, Rk, #5
i4: ADD Rl, #9, Rj
i5: LOAD Rl, (Rj+0)
i6: SUB Rl, Rl, Rk

28. Se consideră un microsistem realizat în jurul unui microprocesor care ar


accepta o frecvenţă maximă a tactului de 20 MHZ. Regenerarea memoriei
DRAM se face în mod transparent pentru microprocesor. Procesul de
regenerare durează 250 ns. Orice ciclu extern al procesorului durează trei
perioade de tact. Poate funcţiona în aceste condiţii microprocesorul la
frecvenţa maximă admisă? Justificaţi.

29. Explicaţi concret rolul fiecăreia dintre fazele de procesare (ALU,


MEM, WB) în cazul instrucţiunilor RISC:

a) STORE R5, (R9)06h;


sursă

b) LOAD R7, (R8)F3h;


dest

510
c) AND R5, R7, R8.
dest

30. Se consideră un procesor scalar pipeline, în trei faze diferite de procesare


(IF, EX, WR), fiecare fază necesitând un tact, astfel:

IF = fetch instrucţiune şi decodificare;


EX = selecţie operanzi din setul de regiştri şi execuţie;
WB = înscriere rezultat în registrul destinaţie.

a) În câte impulsuri de tact se execută secvenţa de program de mai jos ?


b) Reorganizaţi această secvenţă în vederea minimizării timpului de
execuţie.

1: ADD R3, R2, #2


2: ADD R1, R9, R10
3: ADD R1, R1, R3
4: ADD R2, R3, #4
5: ADD R2, R1, R2
6: STORE R3, (R1)2

31. Un procesor pe 32 biţi la 50 MHZ, lucreează cu 3 dispozitive periferice prin


interogare. Operaţia de interogare a stării unui dispozitiv periferic necesită
100 de tacte. Se mai ştie că:

a) interfaţa cu mouse-ul trebuie interogată de 30 de ori / s pentru a fi siguri


că nu se pierde nici o «mişcare» a utilizatorului.
b) floppy - discul transferă date spre procesor în unităţi de 16 biţi şi are o
rată de transfer de 50 ko / s.
c) hard - discul transferă date spre procesor în unităţi de 32 biţi şi are o rată
de transfer de 2 Mo / s.

Determinaţi în [%], fracţiunea din timpul total al procesorului, necesară


interogării stării fiecărui periferic. Comentaţi.

511
32. Considerăm 3 memorii cache care conţin 4 blocuri a câte un cuvânt / bloc.
Una este complet asociativă, alta semi-asociativă cu două seturi a câte două
cuvinte şi ultima cu mapare directă. Ştiind că se foloseşte un algoritm de
evacuare de tip LRU (în cazul celor asociative), determinaţi numărul de
accese cu HIT pentru fiecare dintre cele 3 memorii, considerând că procesorul
citeşte succesiv de la adresele 0, 8, 0, 6, 8, 10, 8 (primul acces la o anumită
adresă va fi cu MISS).

33. Se consideră secvenţa de program RISC:

1: ADD R3, R2, #2


2: ADD R1, R9, R10
3: ADD R1, R1, R3
4: ADD R2, R3, #4
5: ADD R2, R1, R2

Între două instrucţiuni dependente RAW şi succesive în procesare, este


nevoie de o întârziere de un ciclu de tact.
a) În câţi cicli de tact se execută secvenţa iniţială ?
b) În câţi cicli de tact se execută secvenţa reorganizată această
secvenţă în vederea unui timp minim de procesare ?

34. Se consideră o unitate de disc având rata de transfer de 25×104 biţi/s, cuplată
la un microsistem. Considerând că transferul între dispozitivul periferic şi
CPU se face prin întrerupere la fiecare octet, în mod sincron, că timpul scurs
între apariţia întreruperii şi intrarea în rutina de tratare este de 2µs şi că rutina
de tratare durează 10µs, să se calculeze timpul pe care CPU îl are disponibil
între două transferuri succesive de octeţi.

35. a. Dacă rata de hit în cache ar fi de 100%, o instrucţiune s-ar procesa în 8.5
cicli CPU. Să se exprime în [%] scăderea medie de performanţă dacă rata
de hit devine 89%, orice acces la memoria principală se desfăşoară pe 6
tacte şi că orice instrucţiune face 3 referinţe la memorie.
b. De ce este avantajoasă implementarea unei pagini de capacitate «mare»
într-un sistem de memorie virtuală ? De ce e dezavantajoasă această

512
implementare? Pe ce bază ar trebui făcută alegerea optimală a capacităţii
paginii ?

36. Se consideră un procesor scalar pipeline, cu 3 faze diferite de procesare


(IF, EX, WR), fiecare fază necesitând un tact, astfel:

IF = fetch instrucţiune şi decodificare;


EX = selecţie operanzi din setul de regiştri şi execuţie;
WB = înscriere rezultat în registrul destinaţie.

a) În câte impulsuri de tact se execută secvenţa de program de mai jos


?
b) Reorganizaţi această secvenţă în vederea minimizării timpului de
execuţie (se consideră că procesorul deţine o infinitate de regiştri
generali).

1: R1 ← (R11) + (R12)
2: R1 ← (R1) + (R13)
3: R2 ← (R3) + 4
4: R2 ← (R1) + (R2)
5: R1 ← (R14) + (R15)
6: R1 ← (R1) + (R16)

37. Se consideră un microprocesor RISC cu o structură «pipeline» de procesare


a instrucţiunii, având vectorul de coliziune ataşat 01011. Să se determine rata
teoretică optimă de procesare a instrucţiunii pentru acest procesor
[instr./ciclu].

38. De ce implementarea algoritmului lui R. TOMASULO într-o arhitectură


superscalară ar putea reduce din «presiunea» la citire asupra seturilor de
regiştri generali ? Găsiţi vreo similitudine în acest sens, între un CPU
superscalar având implementat acest algoritm şi un CPU de tip TTA
(Transport Triggered Architecture) ?

39. De ce consideraţi că o instrucţiune de tip RETURN este mai dificil de


predicţionat printr-un predictor hardware ? Puteţi prezenta vreo soluţie, în

513
vederea eliminării acestei dificultăţi ? În ce constă noutatea «principială» a
predictoarelor corelate pe două nivele ?

40. Se consideră structura hardware a unui microprocesor RISC, precum în


figura de mai jos.

Răspundeţi la următoarele întrebări.


a) Ce tip de instrucţiuni activează sumatorul «sum 2» şi în ce scop ?
b) Într-un tact, la setul de regiştri pot fi necesare două operaţii
simultane: citire (nivelul RD din pipeline), respectiv scriere (nivelul WB
din pipeline). Cărei operaţii i se dă prioritate şi în ce scop ?
c) Ce rol are unitatea ALU în cazul unei instrucţiuni de tip LOAD ?
d) Ce informaţie se memorează în latch-ul EX/MEM în cazul
instrucţiunii: ST (R7)05, R2 şi de unde provine fiecare informaţie ?

41. Scrieţi un program, folosind recursivitatea, care citeşte de la tastatură două


numere întregi pozitive şi afişează cel mai mare divizor comun şi cel mai mic
multiplu comun al celor două numere.

42. Scrieţi un program recursiv care rezolvă problema Turnurilor din Hanoi
pentru n discuri (n – parametru citit de la tastatură). Enunţul problemei este
următorul:
Se dau trei tije simbolizate prin A, B şi C. Pe tija A se găsesc n discuri
de diametre diferite, aşezate în ordine descrescătoare a diametrelor privite

514
de jos în sus. Se cere să se mute discurile de pe tija A pe tija B, folosind tija
C ca tijă de manevră, respectându-se următoarele reguli:
La fiecare pas se mută un singur disc.
Nu este permis să se aşeze un disc cu diametrul mai mare peste un disc
cu diametrul mai mic.

43. Realizaţi un program care citeşte de la tastatură două numere naturale n şi k


(n>k) şi calculează şi afişează pe consolă valorile următoare:

k k
C n şi An

44. Să se citească un şir de numere întregi de la tastatură, a cărui dimensiune


este citită de la tastatură. Sortaţi şirul prin metoda “bubble sort”, memoraţi
succesiv datele la adresa 0x10012000 şi afişaţi şirul sortat pe consolă.

45. Scrieţi un program care afişează primele n perechi de numere prime impare
consecutive (n - număr impar citit de la tastatură). Exemplu: (3,5), (5,7) etc.

46. (Conjectura lui Goldbach) Scrieţi un program în limbaj de asamblare DLX,


care citeşte de la tastatură, prin intermediul modulului Input.s, un număr n
par, n>6. Să se determine toate reprezentările lui n ca sumă de numere prime,
sumă cu număr minim de termeni. Afişarea se face pe consolă pe fiecare linie
câte o soluţie.

47. Se consideră un procesor cu un cache conectat la o memorie principală


printr-o magistrală (bus de date) de 32 de biţi; un acces cu hit în cache
durează un ciclu de tact. La un acces cu miss în cache, întregul bloc trebuie
extras din memoria principală, prin intermediul magistralei. O tranzacţie pe
bus constă dintr-un ciclu de tact pentru trimiterea adresei pe 32 de biţi spre
memorie, 4 cicli de tact în care are loc accesarea memoriei (strobarea datelor
pe bus) şi un ciclu pentru transferarea fiecărui cuvânt de date (32 octeţi) în
blocul din cache. Se presupune că procesorul continuă execuţia doar după ce
ultimul cuvânt a fost adus în cache. Următorul tabel exprimă rata medie de
miss într-un cache de 1Mbyte pentru diverse dimensiuni a blocului de date.

515
Dimensiunea blocului Rata de miss
(B), în cuvinte (m), în %
1 4,5
4 2,4
8 1,6
16 1,0
32 0,75

Se cere:
a) Pentru care din blocuri (pentru ce dimensiune) se obţine cel mai bun
timp mediu de acces la memorie ?
b) Dacă accesul la magistrală adaugă doi cicli suplimentari la timpul mediu
de acces la memoria principală (dispută pentru ocuparea bus-ului), care
din blocuri determină valoarea optimă pentru timpul mediu de acces la
memorie?
c) Dacă lăţimea magistralei este dublată la 64 de biţi, care este dimensiunea
optimă a blocului de date, din punct de vedere al timpului mediu de
acces la memorie?

48. Să se optimizeze prin tehnica Trace Scheduling următoarea secvenţă de


program. Prima coloană reprezintă secvenţa de program C (extras dintr-o buclă
de interclasare a doi vectori), iar a doua coloană semnifică secvenţa de cod
obiect, obţinută prin compilare. Execuţia secvenţei se face pe un procesor
superscalar IN-ORDER care decodifică 4 instrucţiuni simultan şi deţine 5
unităţi de execuţie (ALU1, ALU2, SHIFT, LD/ST, BRANCH). Doar
instrucţiunile Load consumă două tacte (Store – doar un tact), restul unităţilor
execută operaţiile aferente într-un singur tact. Se consideră că saltul condiţionat
se face preponderent. Determinaţi creşterea de performanţă obţinută după
reorganizare (graful de control aferent secvenţei de optimizat şi graful
dependenţelor pentru trace-ul considerat - modelele de execuţie). Subliniaţi
eventualele coduri compensatorii introduse.
Se cunoaşte că registrul:
R1 reţine adresa elementului a[i] din tabloul a.
R2 păstrează adresa elementului b[j] din tabloul b.
R3 reţine adresa elementului c[k] din tabloul c.
R4 păstrează elementul curent a[i], din tabloul a.
R5 reţine elementul curent b[j], din tabloul b.

516
i=0; j=0; k=0; CREŞTE_K:
if (a[i]<b[j]) 13 ADD R3, R3, 4
{ :
c[k]=a[i];
i++;
}
else
{
c[k]=b[j];
j++;
}
k++;

1: ADD R1, R0, base_a


2: ADD R2, R0, base_b
3: ADD R3, R0, base_c
4: LD R4, (R1)
5: LD R5, (R2)
6: SGT R7, R4, R5 //if
(R4)>(R5), R7=1
7: BEQZ R7, B_MARE
8: ST R5, (R3)
9: ADD R2, R2, 4
10 JMP CREŞTE_K
:
B_MARE:
11 ST R4, (R3)
:
12 ADD R1, R1, 4
:

517
49. Se consideră un procesor scalar pipeline, având un "branch delay slot"
(BDS) de doi cicli CPU şi secvenţa de program de mai jos:

1. A←B
2. if (B=Q) then goto 6
NOP  BDS = 2 cicli
NOP
3. B←B–1
4. Q ← Q +1
5. D←E
6. E←F
7. X←Q
8.

a) Determinaţi rata de procesare (instrucțiuni utile / ciclu) a secvenţei de


program în cazul în care branch-ul (2) nu se face, respectiv se face.
b) Reorganizaţi secvenţa de program având ca scop principal mascarea
(umplerea) BDS-ului cu instrucţiuni utile. Determinaţi performanţa
conform cerinţelor punctului a) şi în cazul programului optimizat astfel.

TEST DE VERIFICARE A CUNOȘTINȚELOR


(120 MINUTE)

1.
a) Se consideră un microprocesor RISC cu o structură pipeline de procesare a
instrucţiunilor având vectorul de coliziune ataşat 01011. Să se determine rata
teoretică optimă de procesare a instrucţiunilor pentru acest procesor [instr./ciclu]
și să se expliciteze algoritmul optimal de alimentare a structurii pipeline.

b.) Se consideră 3 memorii cache, fiecare conţinând 4 blocuri a câte un cuvânt /


bloc. Una este complet asociativă, alta semi-asociativă cu două seturi a câte
două cuvinte/set şi ultima cu mapare directă. Ştiind că se foloseşte un algoritm
de evacuare de tip LRU, determinaţi numărul de accese cu HIT pentru fiecare
dintre cele 3 memorii, considerând că procesorul citeşte succesiv de la adresele
0, 8, 5, 6, 11, 12, 5, 6, 8, 13, 8 (primul acces la o anumită adresă va fi cu MISS).
Ce observaţi?

518
2.
a.) În cadrul unui procesor vectorial (SIMD) se consideră următoarea secvenţă
de program:
x=0
for i = 1 to 999 do
x = x + A[i]*B[i];
endfor
În câţi cicli se execută secvenţa ? Vectorizaţi bucla (chiar și parțial).
Determinaţi timpul de execuţie al secvenţei (parțial) vectorizate.

b.) Se consideră un ReOrderBuffer (ROB) de tip coadă circulară FIFO.


(Structura unei intrări ROB este: Rdest, Valoare, Ready_Bit.)

Când o instrucţiune ALU se introduce într-o staţie de rezervare, ce s-ar


putea scrie în câmpurile Q respectiv V ale acesteia? De unde se scriu
aceste informatii?
În ce condiţii se efectuează faza Commit?
Ce proces din pipeline garantează efectuarea In Order a fazei Commit şi
de ce?

3. Se consideră secvenţa de program RISC pipeline:

i1: ADD R1, R2, #15


i2: ADD R3, R4, #17
i3: ADD R 5 , R3 , R1
i4: ADD R6, R5, #12
i5: ADD R3, R7, #3
i6: ADD R8, R3, #2
i7: ADD R9, R8, #14

Se cere:

c. Să se construiască graful dependenţelor de date (RAW, WAR, WAW)


aferent acestei secvenţe şi sa se precizeze în câte impulsuri de tact se

519
execută secvenţa pe un procesor scalar, ştiind că latenţa de execuţie a
instrucţiunii ADD este de un ciclu ?
d. Să se determine modul de execuţie al secvenţei optimizate pe un
procesor RISC pipeline superscalar, cu 6 seturi fizice de regiştri generali
şi 3 unităţi ALU.

4.
Se considera un sistem multiprocesor cu protocol de coerenta a memoriilor
cache de tip snooping MSI. Precizați acțiunile controlerului de cache atunci
când:

• Un anumit CPU face un acces la memorie soldat cu Read_miss în cache-


ul propriu, blocul accesat în acest cache fiind în starea Shared
• Un anumit CPU face un acces la memorie soldat cu Read_miss în cache-
ul propriu, blocul accesat în acest cache fiind în starea Modified
(Exclusive)
• Un anumit CPU face un acces la memorie soldat cu Write_hit în cache-ul
propriu, blocul accesat în acest cache fiind în starea Shared

5.
a.
• Prin ce se caracterizează un sistem multiprocesor de tip NUMA (Non
Uniform Memory Access sau Distributed Shared-Memory)?
• Care sunt avantajele unei rețele de interconectare de tip crossbar între
cele N CPU-uri și cele N memorii fizice, într-un sistem multiprocesor? Se
pot întâmpla în acest caz accese simultane a două procesoare la același
bank fizic de memorie?
b.
• Se consideră bucla de program:

for (k=0;k<16;k++)
Y(k)=X(k)+5;

Considerând un sistem multiprocesor cu 4 bank-uri fizice de memorii partajate


(memorii cu acces întrețesut), paralelizați aceasta buclă prin 4 fire de control
(Thread Level Parallelism), exploatând inclusiv paralelismul la nivelul acceselor
la memorie (Memory Level Parallelism).

520
6.
Aparent, în mod “normal” ar fi imposibil pentru ambele programe (fire de
control), rezidente pe procesoare diferite, cu memorii cache proprii (P1, P2), să
evalueze pe adevărat condiţiile L1 şi L2, întrucât dacă B=0 (P1) atunci A=1 (P2)
şi dacă A=0 (P2) atunci B=1 (P1). Şi totuşi, acest fapt s-ar putea întâmpla în
anumite condiţii. Explicaţi în ce condiţii s-ar putea întâmpla această incoerenţă.
În ce mod elimină consistenţa secvenţială a variabilelor partajate din cache-uri,
această posibilitate?

(P1) (P2)
A=0; B=0;
---- ----
A=1; B=1;
L1: if (B==0)... L2: if (A==0)...

TEST DE VERIFICARE A CUNOȘTINȚELOR


(120 MINUTE)

1.
Se consideră următoarea secvenţă de program C:
for (i=1000; i>0; i--)
x[i]= x[i] + s;
Secvenţa este compilată pentru procesorul MIPS şi rezultă următoarea buclă în
limbaj de asamblare.

Loop: L.D F0, 0(R1) // încarcă o valoare dublă precizie (8 octeţi) din
memorie în registrul F0
ADD.D F4, F0, F2 // incrementează valoarea elementului tabloului
cu valoarea aflată în F2
S.D F4, 0(R1) // scrie noua valoare (dublă precizie) în memorie
DAD R1, R1, #8 // decrementare adresă pentru indexarea
următorului element
BNE R1, R2, Loop

521
Optimizaţi această buclă de program aplicând tehnica "software pipelining".
Considerând un procesor superscalar cu o infinitate de unităţi de execuţie,
predicţie perfectă a branch-urilor şi un "load delay slot" de un ciclu, precizaţi în
câţi cicli se execută bucla iniţială, respectiv cea optimizată.

2. Se consideră un procesor scalar RISC pipeline, cu (doar) 3 faze diferite


de procesare (IF, EX, WB), fiecare fază necesitând un tact, astfel:

IF = fetch instrucţiune şi decodificare;


EX = selecţie operanzi din setul de regiştri şi execuţie;
WB = înscriere rezultat în registrul destinaţie.

c) În câte impulsuri de tact se execută secvenţa de program de mai jos ?


d) Reorganizaţi această secvenţă în vederea minimizării timpului de execuţie
(se consideră că procesorul deţine o infinitate de regiştri generali).

1: R1 ← (R11) + (R12)
2: R1 ← (R1) + (R13)
3: R2 ← (R3) + 4
4: R2 ← (R1) + (R2)
5: R1 ← (R14) + (R15)
6: R1 ← (R1) + (R16)

3.

Care dintre cele 3 branch-uri de mai jos consideraţi că ar putea fi mai dificil de
predicţionat dinamic? Comentaţi. (HRg reprezintă conținutul momentan al
registrului de corelație globală – pe 2 biți aici.)

HRg T NT
B1 01 de 3 ori de 1049 ori
B2 10 2 3
B3 11 4907 5044

4. Se consideră un ReOrderBuffer (ROB) de tip coadă circulară FIFO. Structura


unei intrări ROB este: Rdest, Valoare, Ready_Bit.

522
• Ce se întâmplă în <Tail_ROB> după decodificarea instrucţiunii curente?
• Ce se întâmplă în ROB pe timpul fazei WriteBack?
• Care dintre fazele Dispatch, Issue, Exec, WriteBack, Commit se
desfăşoară Out of Order şi de ce?
• Ce rol are câmpul Rdest în ROB? Când se exercită acest rol?

5. Se consideră un sistem multiprocesor cu protocol de coerență a memoriilor


cache de tip snooping MSI. Precizați acțiunile controlerului de cache aferent
unui anumit CPU, atunci cand acestuia i se solicita de pe bus-ul comun:

• O cerere de tip Read_miss iar blocul pe care se mapează cererea în cache-


ul CPU se află în starea Shared. Ce s-ar întampla diferit în acest caz, dacă
protocolul ar fi MOESI?
• O cerere de tip Read_miss iar blocul pe care se mapează cererea în cache-
ul CPU este în starea Exclusive (Modified).
• O cerere de tip Write_miss iar blocul pe care se mapează cererea in cache-
ul CPU este în starea Exclusive.

6.
a.) Definiți notiunea de sistem multiprocesor de tip UMA (Uniform
Memory Access).

b.) Se consideră secvenţa de program, executată simultan pe două


procesoare, înglobate într-un sistem de tip bi-procesor, cu memorie globală
partajată (GlobSum este o variabila globală partajată):

LocSum=0;
for i=1 to Max
LocSum=LocSum+LocTable[i]; secvenţa executată în paralel de
; către fiecare procesor!
Proces LOCK
atomic GlobSum=GlobSum+LocSum; secțiune critică; se va executa la
un moment dat de către un singur CPU.
UNLOCK

523
Ce eroare s-ar fi putut întâmpla, dacă nu s-ar fi impus atomicitatea sectiunii
critice? Explicați.

TEST DE VERIFICARE A CUNOȘTINȚELOR


(120 MINUTE)

1. Se consideră următoarea secvenţă de program C:


for (i=1000; i>0; i--)
x[i]= x[i] + s;
Secvenţa este compilată pentru procesorul MIPS şi rezultă următoarea buclă în
limbaj de asamblare.

Loop: L.D F0, 0(R1) // încarcă o valoare dublă precizie (8


octeţi) din memorie în registrul F0
ADD.D F4, F0, F2 // incrementează valoarea elementului
tabloului cu valoarea aflată în F2
S.D F4, 0(R1) // scrie noua valoare (dublă precizie) în
memorie
DADDIU R1, R1, #8 // decrementare adresă pentru indexarea
următorului element
BNE R1,R2, Loop

Optimizaţi această buclă de program, aplicând tehnica "software pipelining".


Considerând un procesor superscalar cu o infinitate de unităţi de execuţie,
predicţie perfectă a branch-urilor şi un "load delay slot" de un ciclu, precizaţi în
câţi cicli se execută bucla iniţială, respectiv cea optimizată.

2. Să se optimizeze prin tehnica Trace Scheduling secvenţa următoare de


program. Prima coloană reprezintă secvenţa de program C (extras dintr-o
buclă de determinare a numărului de elemente pozitive / negative dintr-un
vector) iar a doua coloană semnifică secvenţa de cod obiect, obţinută prin
compilare. Execuţia secvenţei se face pe un procesor superscalar care
decodifică 4 instrucţiuni simultan şi deţine 5 unităţi de execuţie (ALU1,
ALU2, SHIFT, LD/ST, BRANCH). Doar instrucţiunile Load consumă doi
cicli (Store – un singur ciclu), restul unităţilor execută operaţiile aferente într-
un ciclu. Se consideră că saltul condiţionat se face preponderent iar

524
procesarea instrucţiunilor este IN-ORDER. Determinaţi creşterea de
performanţă obţinută după reorganizare (graful de control aferent secvenţei
de optimizat şi graful dependenţelor pentru trace-ul considerat). Determinați
rata de procesare și subliniați, daca este cazul, codurile compensatoare
necesare.

525
i=0;contor_plus=0;contor_minus=0;
if (a[i]<0)
contor_minus++;
else
contor_plus++;
i++;

1 ADD R1, R0, base_a


2 LD R4, (R1)
3 SGE R7, R4, R0 //if
(R4)>0, R7=1
4 BEQZ R7, Minus
5 ADD contor_plus,
contor_plus, 1
6 JMP Creşte_I
Minus:
7 ADD
contor_minus,contor_minus,1
Creşte_I:
8 ADD R1, R1, 4

526
Se cunoaşte că registrul:
R1 reţine adresa elementului a[i] din tabloul a.
R4 reţine elementul curent a[i], din tabloul a.

3. Scrieţi o secvenţă de program asamblare RISC, care să reprezinte translatarea


corectă a programului scris în limbaj C. Variabilele i, j, k, şi l pot fi asignate
oricăror regiştri (iniţial toţi regiştrii procesorului sunt disponibili).

K = X[i-4]+12;
L = Y[j+5] XOR K;
Z[K+2] = K AND L;

Se consideră programul executat pe un procesor scalar pipeline RISC, cu 5


niveluri (IF, ID, ALU, MEM, WB). Se consideră că operanzii instrucţiunilor
sunt necesari la finele fazei ID, iar rezultatele sunt disponibile în setul de regiştri
generali la finele fazei WB. Se cere:
• Reprezentaţi graful dependenţelor de date (numai dependenţele de tip
RAW).
• În câte impulsuri de tact se execută secvenţa de program asamblare ?
• Reorganizaţi această secvenţă cu algoritmul List Scheduling în vederea
minimizării timpului de execuţie (se consideră că procesorul deţine o
infinitate de regiştri generali).
• Aplicând tehnica de forwarding, în câte impulsuri de tact se execută
secvenţa reorganizată ?

4. a.) Relativ la o schemă de predicţie a branch-urilor de tip GAg (v. Figura


următoare), construiţi un automat cu 4 stări (într-o stare acesta poate
predicţiona Taken sau Not_Taken) care să predicţioneze 100% corect un
branch având următorul comportament dinamic: NT, NT, T, T, NT, NT, T,
T, … Câte astfel de automate distincte îndeplinesc condiţiile impuse
anterior?
Observație: Se va considera că procesele de predicţie startează după
iniţializarea automatului.

b.) Care dintre cele 3 branch-uri de mai jos consideraţi că ar putea fi mai
dificil de predicţionat dinamic ? Comentaţi.
HRg T NT
B1 01 3 1049
B2 10 2 3
B3 11 4907 5044

Structură de predicţie a branch-urilor de tip GAg

5.
a.) Definiți noțiunea de sistem multiprocesor de tip UMA (Uniform
Memory Access).

b.) Definiți problema coerenței cache-urilor în sistemele multiprocesor.


Avantaje ale protocolului de coerență a cache-urilor MESI, față de protocolul
MSI.

c.) Care dintre cele două secvenţe de program s-ar putea procesa mai rapid
pe un procesor superscalar cu execuţie «Out of Order» a instrucţiunilor?
Justificaţi.

B1. B2.
for i=1 to 100 a[2]=x[1];
a[2i]=x[i]; y[1]=a[2]+5;
y[i]=a[i+1]+5; for i=2 to 100
a[2i]=x[i];
y[i]=a[i+1]+5;

528
6.
a.) Avantaje ale protocolului de coerență a cache-urilor MOESI față de
protocolul MESI.

b.) Se consideră secvenţa de program executata simultan pe două


procesoare înglobate într-un sistem bi-procesor, cu memorie globală partajată
(GlobSum este o variabila globală partajată):

LocSum=0;
for i=1 to Max
LocSum=LocSum+LocTable[i]; secvenţa executată în paralel de
; către fiecare procesor!
Proces LOCK
atomic GlobSum=GlobSum+LocSum; secțiune critică
UNLOCK

Ce eroare s-ar fi putut întâmpla dacă nu s-ar fi impus atomicitatea secțiunii


critice? Explicați.

529
BIBLIOGRAFIE SELECTIVĂ

[Arm98] ARMAT C., VINȚAN L. – Some Investigations about Selective


Victim Caching into a Superscalar Environment, Transactions on Automatic
Control and Computer Science, Volume 43 (57), No. 4, University
"Politehnica" of Timișoara, Romania, 1998

[Asa06] ASANOVIC K. et al. – The Landscape of Parallel Computing


Research: A View from Berkeley, Technical Report No. UCB/EECS-2006-
183, December 2006

[Cal10] CALBOREAN H., VINȚAN L. – An Automatic Design Space


Exploration Framework for Multicore Architecture Optimizations,
Proceedings of The 9-th IEEE RoEduNet International Conference, pp. 202-
207, ISSN 2068-1046, Sibiu, June 24-26, 2010

[Cal11] H. CALBOREAN – Multi-Objective Optimization of Advanced


Computer Architectures using Domain-Knowledge (Optimizarea multi-
obiectiv a unor arhitecturi avansate de calcul utilizând cunoştinţe de
domeniu), PhD Thesis, “L. Blaga” University of Sibiu, November 25th 2011
(conducător științific: prof. univ. dr. ing. Lucian Vințan)

[Cre10] CRETULESCU R., MORARIU D., VINȚAN L., COMAN I. D.


– An Adaptive Meta-classifier for Text Documents, The 16th International
Conference on Information Systems Analysis and Synthesis (ISAS 2010),
vol. 2, pp. 372-377, ISBN-13: 978-1-934272-88-6, Orlando Florida, USA,
April 6th – 9th 2010

[Cul99] D. E. CULLER et al – Parallel Computer Architecture: A


Hardware/Software Approach, Morgan Kaufmann Publishers, 1999

530
[Ega03] EGAN C., STEVEN G., QUICK P., ANGUERA R., VINȚAN L.
– Two-Level Branch Prediction using Neural Networks, Journal of Systems
Architecture, vol. 49, issues 12-15, pg.557-570, ISSN: 1383-7621, Elsevier,
December 2003

[Fis05] FISHER J., FARABOSCHI P., YOUNG C. – Embedded


Computing, Morgan Kaufmann Publishers (Elsevier), 2005

[Flo03] FLOREA ADRIAN, VINȚAN N. LUCIAN – Simularea și


optimizarea arhitecturilor de calcul în aplicații practice, Editura Matrix
ROM, Bucuresti, 2003

[Gel07] GELLERT A., VINȚAN L. N., FLOREA A. – A Systematic


Approach to Predict Unbiased Branches, “Lucian Blaga” University Press,
Sibiu, 2007

[Gel09] GELLERT A., FLOREA A., VINȚAN L. – Exploiting Selective


Instruction Reuse and Value Prediction in a Superscalar Architecture,
Journal of Systems Architecture, ISSN: 1383-7621, Elsevier, Volume 55,
Issue 3, March 2009

[Gel12] Á. GELLÉRT, H. CALBOREAN, L. VINȚAN, A. FLOREA –


Multi-Objective Optimizations for a Superscalar Architecture with Selective
Value Prediction, IET Computers & Digital Techniques, United Kingdom,
Vol. 6, Issue 4, ISSN: 1751-8601, 2012

[Hay98] HAYES J. – Computer Architecture and Organization, Third


Edition, McGraw Hill, 1998

[Hen11] HENNESSY J., PATTERSON D. – Computer Architecture: A


Quantitative Approach, Morgan Kaufmann (Elsevier), 5-th Edition, 2011

[Jah12] JAHR R., CALBOREAN H., VINȚAN L., UNGERER T. -


Boosting Design Space Explorations with Existing or Automatically Learned

531
Knowledge, The 16-th International GI/ITG Conference on Measurement,
Modelling and Evaluation of Computing Systems and Dependability and
Fault Tolerance (MMB/DFT 2012), March 19-21 2012

[Jah15] JAHR R., CALBOREAN H., VINȚAN L., UNGERER T. –


Finding Near-Perfect Parameters for Hardware and Code Optimizations
with Automatic Multi-Objective Design Space Explorations, Concurrency
and Computation: Practice and Experience, doi: 10.1002/cpe.2975, vol. 27,
issue 9, ISSN 1532-0626, John Wiley & Sons, 2015

[Jer05] JERRAYA A., WOLF W. (editori) – Multiprocessor Systems-on-


Chips, Elsevier (Morgan Kaufmann Publishers), 2005

[Jor03] JORDAN H., ALAGHBAND G. – Fundamentals of Parallel


Processing, Prentice Hall, 2003

[Mit97] MITCHELL T. – Machine Learning, McGraw Hill, 1997

[Pat04] PATT Y., PATEL S. – Introduction to Computing Systems: from


Bits and Gates to C and Beyond, McGraw Hill, 2nd Edition, 2004

[Pat98] PATTERSON D., HENNESSY J. – Computer Organization and


Design, The Hardware / Software Interface, Second Edition, Morgan
Kaufmann Publishers, 1998 (există și o traducere românească la Editura
ALL, 2000)

[Rad11] C. RADU –Optimized Algorithms for Network-on-Chip Application


Mapping (Algoritmi optimizaţi pentru maparea aplicaţiilor paralele pe
arhitecturi de tipul Network-on-Chip), PhD Thesis, “L. Blaga” University of
Sibiu, November 25th 2011 (conducător științific: prof. univ. dr. ing.
Lucian Vințan).

[Rad13] C. RADU, MD. S. MAHBUB, L. VINȚAN – Developing Domain-


Knowledge Evolutionary Algorithms for Network-on-Chip Application

532
Mapping, Microprocessors and Microsystems, Vol. 37, Issue 1, pp. 65-78,
ISSN: 0141-9331, Elsevier, February 2013

[Smi05] SMITH J. E., NAIR R. – Virtual Machines, Elsevier (Morgan


Kaufmann Publishers), 2005

[Uhr10] S. UHRIG, B. SHEHAN, R. JAHR, T. UNGERER – The Two-


dimensional Superscalar GAP Processor Architecture, International Journal
on Advances in Systems and Measurements, ISSN 1942-261x, vol. 3, no. 1
& 2, pp. 71-81, 2010

[Vin00] VINȚAN N. LUCIAN – Arhitecturi de procesoare cu paralelism la


nivelul instrucţiunilor, Editura Academiei Române, Bucureşti, 2000

[Vin00b] VINȚAN N. LUCIAN, FLOREA ADRIAN – Microarhitecturi


de procesare a informaţiei, Editura Tehnică, Bucureşti, 2000

[Vin01] VINȚAN L., SBERA M., FLOREA A. – Pre-computed Branch


Prediction, Acta Universitatis Cibiniensis, Technical Series. Computer
Science and Automatic Control, pg. 91-100, vol. XLIII, ISSN 1221-4949,
Ed. Universității “L. Blaga” din Sibiu, 2001

[Vin02] VINȚAN N. LUCIAN – Predicție și speculație în


microprocesoarele avansate, Editura Matrix Rom, București, 2002

[Vin03] VINŢAN N. LUCIAN – Organizarea și proiectarea


microarhitecturilor. Note de curs, Editura Alma Mater, Sibiu, 2003
(disponibil online la
http://webspace.ulbsibiu.ro/lucian.vintan/html/Organizarea.pdf)

[Vin05] VINȚAN L., FLOREA A., GELLERT A. – Focalising Dynamic


Value Prediction to CPU’s Context, IEE Proceedings. Computers & Digital
Techniques, United Kingdom, Vol. 152, No. 4, ISSN 1350-2387, July 2005

533
[Vin07] VINȚAN N. LUCIAN – Prediction Techniques in Advanced
Computing Architectures (în limba engleză), Matrix Rom Publishing House,
Bucharest, 2007

[Vin07b] VINȚAN N. LUCIAN – Maeştri ai ingineriei calculatoarelor.


Pagini de istorie, Univers ingineresc, anul XVIII, nr. 16 (398), pg.4-5, 16-31
august 2007

[Vin08] VINȚAN L. N. – De la predicţia salturilor condiţionate la o


întrebare fundamentală: ce este aleatorul? (From Branch Prediction to a
Fundamental Question: What is Random?), Educaţia matematică –
Mathematical Education, Vol. 4, Nr. 1, pg. 15 – 31, Univ. “L. Blaga” din
Sibiu, 2008

[Vin08b] VINȚAN L. N., FLOREA A., GELLERT A. – Random Degrees


of Unbiased Branches, Proceedings of The Romanian Academy, Series A:
Mathematics, Physics, Technical Sciences, Information Science, Volume 9,
Number 3, pp. 259 - 268, Bucharest, 2008

[Vin09] VINȚAN N. LUCIAN – Direcţii de cercetare în domeniul


sistemelor multicore / Main Challenges in Multicore Architecture Research,
Revista Română de Informatică și Automatică, ISSN: 1220-1758, ICI
București, vol. 19, nr. 3, 2009

[Vin13] VINȚAN N. LUCIAN – Automatic Multi-Objective Optimization of


Mono-Core and Multi-Core Architectures using Domain-Knowledge (62 PPT
slides), Invited Scientific Presentation, CONTINENTAL R&D Sibiu Branch,
March 4th 2013 – disponibilă online la adresa
http://webspace.ulbsibiu.ro/lucian.vintan/html/Conti.pdf

[Vin15] VINȚAN L., CHIȘ R., MD. ALI ISMAIL, COȚOFANĂ C. –


Improving Computing Systems Automatic Multi-Objective Optimization
through Meta-Optimization, IEEE Transactions on Computer-Aided Design

534
of Integrated Circuits and Systems, vol. 35, issue 7, ISSN: 0278-0070, DOI
10.1109/TCAD.2015.2501299, 2016 (online 2015, print 2016)

[Vin15b] L. N. VINŢAN – Educația universitară în ingineria


calculatoarelor: spre o abordare cultural-științifică (Academic Education in
Computer Engineering: Towards a Cultural-Scientific Approach), Revista de
politica științei și scientometrie – serie nouă, ISSN-L 1582-1218, Vol. 4, No.
3, pg. 204-208, septembrie 2015

[Vin16] L. N. VINŢAN – Fundamente ale optimizării multi-obiectiv a


sistemelor complexe de calcul, Buletinul AGIR, an XXI, ISSN-L 1224-7928,
Online: ISSN 2247-3548, Editura AGIR, Bucuresti, 2016 (trimis la
22.06.2016)

[Wol07] WOLF W. – High Performance Embedded Computing, Morgan


Kaufmann (Elsevier), 2007

535
GLOSAR DE TERMENI TEHNICI UTILIZAȚI

În cele ce urmează se prezintă, pe baza glosarului de termeni din referința


[Vin00] (o lucrare a autorului acestei cărți), revăzut și adăugit în prezenta
versiune, într-un mod succint şi pragmatic, doar acei termeni de specialitate
folosiţi relativ frecvent în această lucrare şi al căror înţeles – crede autorul –
merită explicitat cititorului. Pragmatismul descrierii termenilor se referă la faptul
că autorul i-a definit exclusiv în accepţiunea strictă, care le-a fost dată în această
carte, chiar dacă majoritatea acestor termeni au interpretări şi semantici multiple,
în spectrul larg al literaturii tehnico-ştiinţifice din domeniul calculatoarelor și
tehnologiei informaţiei. Am folosit şi termeni în limba engleză, datorită faptului
că unii dintre aceştia sunt deja consacraţi şi, totodată, pentru a obişnui cititorul
cu terminologia acestui domeniu, a cărui geneză şi dezvoltare se regăsesc – din
păcate – practic exclusiv în literatura de specialitate scrisă în limba engleză.
Precizăm faptul că orice încercare de acest fel are și o componentă subiectivă
inerentă, datorată, în special, lipsei de rigoare a domeniului arhitecturilor de
calcul, dar și propriei noastre înțelegeri.

Anti-alias (Disambiguation) – analiză prin hardware sau/şi software a


adreselor de acces la memoria de date (cazul instrucțiunilor cu referire la
memorie), în vederea procesării paralele, prin procesări Out of Order ale
instrucţiunilor cu referinţe la memorie dintr-un program. Se face de obicei de
către compilator.

Basic – block (unitate secvenţială de program) – secvenţă de instrucţiuni


mașină care nu conţin salturi şi care nu constituie destinaţii pentru nicio
instrucţiune de salt.

Benchmark – program destinat testării şi evaluării performanţei unei


anumite arhitecturi de calcul. Sunt în general standardizate de către organismele
internaţionale de profil, gen SPEC, EEMBC, SPLASH-2 etc.

536
BTB (Branch Target Buffer) – mică memorie cache integrată în procesor,
care memorează automatele de predicţie şi adresele destinaţie aferente
ramificaţiilor de program (instrucțiunilor de salt condiționat). Este utilizată în
predicţia dinamică a instrucțiunilor de salt condiționat.

Buffer de reordonare (Reorder Buffer) – structură hardware de tip FIFO


circular, implementată în cadrul procesoarelor superscalare, în vederea facilizării
execuţiei Out of Order a instrucțiunilor mașină, prin redenumire dinamică a
resurselor hardware precum şi a implementării unui mecanism precis de tratare a
excepţiilor.

Bypassing (Forwarding) – proces de transfer al rezultatului aflat la ieşirile


unei unităţi de execuţie, direct la intrarea unei alte unităţi de execuţie (sau chiar
a aceleiași unități), care are nevoie de acel rezultat, eliminându-se deci
aşteptările legate de înscrierea rezultatului în setul de regiştri logici respectiv
citirea acestuia din acelaşi set logic.

Cache – concept arhitectural care desemnează o memorie de capacitate mai


mică decât cea a memoriei principale, care este mai rapidă şi mai scumpă per
octet decât memoria principală, situată din punct de vedere logic între procesor
şi aceasta. A fost inventată de prof. Maurice Wilkes de la Universitatea din
Cambridge, în anul 1965. Memoria cache este gestionată – în general prin
hardware – astfel încât probabilitatea statistică de accesare în cache a unei date
de către procesor (rata de hit), să fie cât mai mare. Se diminuează deci timpul
mediu de acces la memoria principală. Astfel, se reduce din “prăpastia
semantică” între viteza de procesare tot mai mare a microprocesoarelor şi
respectiv latenţa prea ridicată a memoriilor principale (DRAM) actuale.
Memoriile cache sunt realizate în tehnologii SRAM de mare performanţă, fiind
uzual integrate în procesor. Conceptul este însă de o mult mai largă generalitate,
având aplicaţii atât în sistemele de operare cât şi în aplicaţiile software (vezi
memoria virtuală în acest sens).
- Princeton (non – Harvard) – arhitectură de memorii cache unificate
(comune) pe spaţiile de instrucţiuni şi date.

537
- Harvard – arhitectură de memorii cache fizic separate pe spaţiile de
instrucţiuni respectiv date.
Victim cache - o memorie cache de capacitate foarte mică (5-16 blocuri),
de obicei complet asociativă, plasată între primul nivel de cache (de obicei, cu
mapare directă) şi memoria principală (sau nivelul următor de cache). Blocurile
înlocuite din cache-ul principal datorită unui miss sunt temporar memorate în
victim cache. Dacă sunt referite din nou, înainte de a fi înlocuite din victim
cache, ele pot fi extrase direct din victim cache, cu o penalitate mai mică decât
cea aferentă memoriei principale. Concept introdus de Dr. Norman Jouppi în
anul 1990.
Selective Victim Cache – un Victim Cache special, în care blocurile aduse
din memoria principală sunt plasate selectiv, fie în cache-ul principal cu mapare
directă, fie în Selective Victim Cache (SVC), folosind un algoritm de predicţie
euristic, bazat pe istoria folosirii blocului. Blocurile care sunt mai puţin probabil
să fie accesate în viitor sunt plasate în SVC şi nu în cache-ul principal. Predicţia
blocurilor este, de asemenea, folosită în cazul unui miss în cache-ul principal și
hit în SVC, pentru a determina dacă este necesară o interschimbare a blocurilor
conflictuale (cache principal – SVC).

Cale critică (Critical path) – calea de instrucțiuni dependente RAW cu


latenţă maximă, din cadrul unui graf al dependenţelor de date

CISC (Complex Instruction Set Computing) – termen generic, care


desemnează o arhitectură de procesor situată în general în contradicţie
conceptuală cu arhitectura de tip RISC. Până în anii ‘80, procesoarele CISC
(Intel, Motorola, Zilog, DEC VAX 11/780, IBM – 360 etc.) erau caracterizate de
o disjuncţie între structura hardware şi setul de instrucţiuni maşină, respectiv
aplicaţiile software scrise în limbaje evoluate, care rulau pe aceste procesoare.
Proiectarea cvasi-independentă a maşinii fizice respectiv a softului a condus la
un “baroc” al structurii hardware în aceste cazuri, caracterizată, în final, de o
eficienţă și performanță reduse, precum şi de un grad scăzut de utilizare a
resurselor. Având în vedere necesitatea actuală de satisfacere simultană a
criteriilor antagoniste de compatibilitate şi performanţă, se constată frecvent o
fuziune a conceptelor de CISC şi RISC.

538
Coerenţă a cache -urilor – cerinţa ca atunci când un procesor citeşte o dată
din cache să citească ultima valoare (copie) înscrisă a acelei date. Această
cerinţă pune probleme serioase, cu deosebire în sistemele multiprocesor
(multicore), unde se implementează algoritmi de menținere a coerenței.

Excepţie – eveniment care determină întreruperea secvenţei de program


executată curent şi intrarea într-o rutină de tratare corespunzătoare. După
executarea acestei rutine de tratare se reia programul principal, cu execuţia
instrucţiunii care a provocat excepţia (deruta, spre exemplu, în cazul unui
eveniment de tip Page Fault) sau cu execuţia instrucţiunii următoare celei care a
fost întreruptă de un eveniment (întrerupere).
- precisă – excepţie care permite structurii pipeline să fie oprită şi – în
urma execuţiei rutinei de tratare – programul întrerupt să fie reluat în condiţii
deterministe.
- imprecisă – excepţie care nu permite reluarea activităţii procesului
întrerupt în condiţii complet deterministe, datorită unor caracteristici ale
procesării pipeline.

Execuţie speculativă – execuţia unor instrucţiuni situate după o


instrucţiune de salt, înaintea acesteia sau chiar în paralel, prin anumite
transformări ale programului. Și procesarea out of order a instrucțiunilor
determină execuții speculative ale acestora.

Forwarding – vezi “bypassing”.

Hazard – situaţie care poate să apară în cadrul procesării pipeline şi care


determină blocarea fluxului de procesare pentru un anumit număr de cicli.
- structural – hazard determinat de conflictele mai multor procese la o
resursă hardware comună.
- de date – hazard care apare atunci când există o dependenţă de date între
două instrucţiuni din structura pipeline.

539
- RAW (Read After Write) – hazard de date care apare atunci când o
instrucţiune j, succesivă unei instrucţiuni i, citeşte o sursă înainte ca
instrucţiunea i să o modifice.
- WAR (Write After Read) – hazard de date care apare atunci când o
instrucţiune j, succesivă unei instrucţiuni i, scrie o destinaţie înainte ca aceasta
să fie citită, pe post de sursă, de către instrucţiunea i.
- WAW (Write After Write) – hazard de date care apare atunci când se
inversează ordinea de scriere a aceleiaşi resurse, în cazul a două instrucţiuni
succesive.
- de ramificaţie – hazarduri determinate de calculul întârziat al adresei de
salt şi al direcţiei de salt, în cazul instrucţiunilor de ramificaţie.

I.L.P. (Instruction Level Parallelism) – domeniu al ştiinţei şi ingineriei


calculatoarelor care se ocupă cu determinarea unor metode şi tehnici hardware –
software, în vederea exploatării paralelismului existent la nivelul instrucţiunilor
în cadrul microprocesoarelor cu execuţii multiple şi pipeline - izate ale
instrucţiunilor.

Interferenţă (instrucțiuni de salt condiționat) – fenomen care apare pe


parcursul predicţiei ramificaţiilor de program, dacă mai multe salturi din
program accesează aceeaşi locaţie din tabela de predicţie.

Memorie virtuală (MV) - reprezintă o tehnică de organizare a memoriei,


prin intermediul căreia programatorul dispune de un spaţiu virtual de adresare de
capacitate foarte mare (mai mare decât capacitatea memoriei principale) şi care,
fără ca programatorul să “simtă”, este mapat dinamic în memoria principală.
Uzual, spaţiul virtual de adrese corespunde suportului disc magnetic (memoria
secundară), programatorul având impresia, prin mecanismele de memorie
virtuală, că are la dispoziție o memorie unică, de capacitatea hard-discului şi nu
de capacitatea, mult mai redusă, a memoriei principale. De asemenea, MV oferă
funcții de protecție a accesului la memorie, prin implementarea unor drepturi de
acces la memorie ale programelor. În urma comparării acestor drepturi cu
nivelul de privilegiu al programelor aflate în rulare, acestea primesc, sau nu,
accesul la o anumită informație stocată în memorie.

540
Microcontroler – reprezintă, practic, un calculator (mai simplu decât cele
de uz general) implementat pe un singur cip. Conține CPU, memorie și interfețe
I/O. Este destinat controlului în timp real a unor procese, aplicații dedicate etc.

MMU (Memory Management Unit) – modul funcţional, uzual integrat în


microprocesor, cu rolul de translatare a adresei virtuale, emisă de procesor, într-
o adresă fizică de acces la memoria principală respectiv de asigurare a
mecanismelor de control şi protecţie a accesului la memorie (vezi şi memoria
virtuală).

Model de programare (PM – Programming Model) – reprezintă


conceptualizarea semantică a mașinii virtuale pe care programatorul o utilizează
în codarea aplicațiilor (concurente). În particular, un PM concurent specifică
modul în care firele de execuție (părți ale programului) rulează în paralel
respectiv comunică între ele și se sincronizează, pentru a rezulta o coordonare
globală eficientă a întregii procesări.
Modelul de programare concurentă, cu memorie globală partajată sau
cu adrese de memorie partajate (Shared address) – se bazează pe o memorie
logică globală, posibil a fi partajată de către toate procesoarele (thread-urile)
componente. O anumită locație din această memorie are aceeași adresă și același
înțeles pentru toate procesoarele (firele concurente).
Modelul de programare concurentă, pe calculatoare distribuite
interconectate (Message Passing) – sistem paralel-distribuit de calcul compus
din calculatoare distribuite, interconectate (practic, rețele de calculatoare), care
comunică prin transmiterea / recepționarea de mesaje (message passing), în mod
explicit, prin funcții (primitive) specifice, de tip send/receive.

Multicore - reprezintă un sistem multiprocesor (calculator paralel) integrat


pe un singur cip (capsulă), adică un set de procesoare (sau chiar calculatoare)
care cooperează și comunică (prin intermediul unei rețele de comunicații) pentru
a accelera o aplicație sofware concurentă, prin procesări paralele. Concurența
este o caracteristică statică intrinsecă a aplicației, iar paralelismul reprezintă

541
maparea dinamică (run-time) a concurenței pe diferitele unități hardware de
procesare.

Pasul unui vector – diferenţa numerică dintre adresele de memorie la care


se află două elemente scalare succesive ale vectorului.

Predicare – tehnică de procesare statică (prin compilator, scheduler), care


urmăreşte execuţia speculativă a instrucţiunilor şi reducerea ramificaţiilor de
program, prin utilizarea instrucţiunilor cu execuţie condiţionată.

Predictor corelat – predictor de ramificaţii care se bazează în procesul de


predicţie dinamică, pe lângă “istoria” saltului condiționat respectiv, şi pe o
informaţie de corelaţie, care constă în comportamentul anterior al salturilor
precedente saltului curent.

Predicţia ramificaţiilor – procesul de predicţie, desfăşurat pe parcursul


fazei de aducere a instrucţiunii, a următoarelor trei informaţii: dacă instrucţiunea
adusă este una de ramificaţie sau nu (1), în caz că este ramificaţie, dacă se face
sau nu (2), iar dacă se face - adresa destinaţie la care se va face (3). Predicţia
ramificaţiilor este foarte necesară în procesoarele super – pipeline – izate şi
superscalare.

Prefetch – mecanism de aducere anticipată a instrucţiunilor și datelor,


desfăşurat în paralel cu execuţia unor instrucţiuni care au fost deja aduse în
procesor şi memorate într-o coadă FIFO, numită buffer de prefetch.

Procesare (a instrucţiunii) – totalitatea acţiunilor desfăşurate de procesor


în vederea aducerii, decodificării şi execuţiei unei instrucţiuni maşină.
- pipeline (de instrucţiuni) – tehnică de procesare prin care fazele
succesive aferente instrucţiunilor succesive sunt suprapuse în timp, în vederea
obţinerii unui paralelism temporal, care conduce la o performanţă teoretică
maximală de o instrucţiune per ciclu.

542
- vectorială – procesarea informaţiei numerice sub formă de vectori.
Provocarea esenţială aici constă în elaborarea unor tehnici hardware – software
eficiente de vectorizare a buclelor de program.
- In Order – execuţia instrucţiunilor se face în ordinea scrierii lor în cadrul
programului static.
- Out of Order – execuţia instrucţiunilor se face în afara ordinii lor din
program, în scopul unei procesări paralele a unor instrucţiuni independente din
cadrul programului.

Procesor (CPU – Central Processing Unit) – structură digitală complexă,


compusă dintr-o unitatea de comandă, unităţi de execuţie, regiştri şi magistrale
de interconectare, având ca principal rol aducerea instrucţiunilor din memorie,
decodificarea şi execuţia acestora.
- scalar – procesor care lansează în execuţie cel mult o singură instrucţiune
la un moment dat (ciclu).
- MEM (maşină cu execuţii multiple) – care poate lansa în execuţie mai
multe instrucţiuni maşină independente, simultan.
- superscalar – procesor MEM în care verificarea independenţei
instrucţiunilor şi rutarea acestora spre unităţile de execuţie multiple se realizează
direct prin hardware, pe timpul rulării.
- VLIW (Very Long Instruction Word) – procesor MEM care aduce şi
execută mai multe instrucţiuni RISC primitive şi independente, simultan. Aceste
instrucţiuni primitive sunt “împachetate” într-o aşa numită instrucţiune multiplă,
de către o componentă a compilatorului numită reorganizator sau planificator de
instrucțiuni (scheduler). Rutarea instrucţiunilor primitive spre unităţile de
execuţie se face automat, funcţie de poziţia pe care o ocupă fiecare instrucțiune
primitivă în cadrul instrucţiunii multiple.
- EPIC (Explicitly Parallel Instruction Computing) – concept introdus de
compania Intel în cadrul arhitecturii sale IA-64 (Itanium), bazat pe rafinarea
conceptului de procesare VLIW, în special în sensul mascării unor caracteristici
hardware (latenţe instrucţiuni, tipuri unităţi de execuţie etc.) pentru
reorganizatorul de cod. Filosofia EPIC are la bază execuţia condiţionată
(gardată) a instrucţiunilor, în scopul eliminării, pe cât posibil, instrucţiunilor de
salt (predicare) şi respectiv execuţiei speculative a instrucţiunilor (înainte de

543
evaluarea condiţiei instrucțiunii de ramificare), ambele cu repercursiuni pozitive
asupra eficienţei structurii de procesare.
- TTA (Transport Triggered Architectures) – expresie limită a
arhitecturii de tip RISC, desemnează un procesor cu o singură instrucţiune
condiționată (MOVE), bazată în esenţă pe separarea transportului datelor de
procesarea (prelucrarea) lor efectivă. În cadrul TTA declanşarea procesării
efective se face printr-un transport în registrul trigger aferent unităţii de
execuţie. Geneza acestui concept provine de la Universitatea Tehnică din Delft,
Olanda.

Rata de hit – raportul dintre numărul acceselor la memorie cu succes în


cache respectiv numărul total al acceselor la memorie din partea procesorului.

Rata de miss – complementara ratei de hit (Rhit+Rmiss=100%).

Rata medie de procesare (Average Issue Rate) – metrică globală de


evaluare a performanţelor CPU, reprezentând numărul total de instrucţiuni
executate, raportat la numărul total al impulsurilor de tact necesare execuţiei
acestor instrucţiuni, exprimată în instrucţiuni / tact.

Reţea neuronală (RN) – structură alcătuită prin interconectarea unor


neuroni artificiali prin legături ponderate, având capacităţi de învăţare
(supervizată sau nesupervizată), generalizare, sinteză şi auto-organizare. RN au
aplicaţii interesante și utile în recunoaşterea formelor, precum şi în alte
probleme de inteligenţă artificială (clasificare/clustering, predicție etc.) Autorul
le-a utilizat aici în problematica predicţiei dinamice a salturilor condiționate în
cadrul procesoarelor pipeline.

RISC (Reduced Instruction Set Computing) – arhitectură optimizată de


procesor, bazată pe pragmatism şi eficienţă. În opinia autorului, esenţa
novatoare a acestui concept constă în optimizarea hardware – software, bazată
pe statistici ale programelor şi simulări laborioase şi având ca scop final
procesarea eficientă a unor aplicaţii reprezentative (benchmark – uri) scrise în
limbaje evoluate. Dintre caracteristicile principale ale arhitecturilor RISC se

544
amintesc aici: set relativ redus de instrucţiuni maşină, model ortogonal de
programare, instrucţiuni codificate pe un singur cuvânt, doar instrucţiunile
LOAD şi STORE accesează memoria de date, procesare pipeline a
instrucţiunilor maşină etc. Conceptul îşi are geneza la începuturile anilor ‘80,
din cercetări efectuate atât în medii industriale (IBM) cât şi academice
(Universităţile Berkeley şi Stanford, S.U.A.)

Scoreboarding – tehnică de detecţie a hazardurilor de date într-o structură


pipeline de procesare, bazată pe asocierea unui bit de scor fiecărui registru, cu
semnificaţia registru utilizabil / neutilizabil. În cazul detecţiei unui hazard de
date, structura pipeline de procesare se opreşte, până la rezolvarea acestuia.

Scheduler (Reorganizator, planificator, optimizator de program) –


program care, în general, reprezintă o componentă a compilatorului, destinat
reorganizării programului obiect în vederea procesării sale într-un timp cât mai
scurt (dar și cu un consum minimal de energie electrică) pe o anumită maşină
hardware. Este o componentă software esenţială a oricărei arhitecturi paralele,
constituind de altfel principala provocare a arhitecturilor paralele.

Secțiune critică de cod (critical section) – desemneaza o secţiune de cod


care poate fi controlată (procesată) la un moment dat numai de către un singur
proces. Utilizată în sistemele paralele de procesare a informației.

Staţie de rezervare (SR) – structură de date implementată în hardware în


cadrul procesoarelor superscalare, cu scopul facilizării procesării Out of Order a
instrucţiunilor. Lansarea în execuţie a unei instrucţiuni rezidentă în bufferul de
prefetch înseamnă mutarea acesteia în SR aferentă. De aici, instrucţiunea se
lansează efectiv în execuţie atunci când toţi operanzii săi sursă sunt disponibili
(valorile acestora). Prin SR și structuri de tip ROB se efectuează o redenumire
dinamică a regiştrilor, în vederea eliminării hazardurilor de date tip WAR şi
WAW. De asemenea, aceste staţii accelerează execuţia programului, prin
captarea agresivă a unor rezultate aşteptate de către instrucţiunile dependente
RAW, aflate în stare de aşteptare în stațiile de rezervare.

545
Task (proces) – reprezintă un program dinamic căruia sistemul de operare
i-a alocat resurse de memorie (cod, stivă, date) și timp de CPU, în vederea
rulării. Cu alte cuvinte, reprezintă o instanță a unei aplicații active (secvența
dinamică de execuție a unui program static) având un spațiu virtual de memorie
și unul sau mai multe fire de control (threads), care partajează zone de date ale
acestui spațiu virtual.

Thread (fir de execuție, de control) – reprezintă o modalitate explicită


prin care un program se împarte pe sine în două sau mai multe sub-task-uri
concurente, care se pot deci procesa în paralel. Thread-urile multiple de execuție
partajează starea procesului, zona de date a acestuia precum și alte resurse, în
mod direct. Firele pot interacționa prin intermediul zonei de date partajate.
Această interacțiune este controlată de operații specifice de sincronizare. Din
punct de vedere al sistemului de operare un thread este secvențial (nu există
paralelism în cadrul acestuia) și atomic, în sensul că este cea mai mică entitate
căreia acesta îi acordă resurse.

Trace – un “fişier” care conţine instrucţiunile unui program, contigue din


punct de vedere al execuţiei lor şi scrise în locaţii succesive ale fişierului.
Instrucţiunile dintr-un trace au ataşate toate informaţiile rezultate în urma
execuţiei lor propriu zise (PC, adrese acces memorie date, adrese destinaţie salt
etc.)
- Trace procesor – un procesor MEM care exploatează reutilizarea
secvenţelor dinamice de instrucţiuni anterior executate şi memorate într-o
memorie specială, numită trace-cache. Se măreşte astfel atât rata de aducere a
instrucţiunilor, la mai multe basic-block-uri simultan, cât şi rata de execuţie a
acestora, faţă de paradigma superscalară clasică (generaţia a III-a de
microprocesoare din punct de vedere arhitectural).

Unitate de comandă (control) – dispozitiv numeric de tip automat


secvențial sincron, care generează, secvenţiat în timp, toate impulsurile de
comandă necesare în structura hardware pentru aducerea, decodificarea şi
execuţia instrucţiunilor maşină.

546
- cablată – U.C. implementată sub forma unui automat secvenţial (sincron)
complex. Proiectarea acestuia se face pe baza descrierii în limbaje specializate
(HDL – Hardware Description Languages) a fiecărei faze, aferente fiecărei
instrucţiuni, la nivelul de “transfer registru”. U.C. cablate sunt specifice
procesoarelor în filosofie RISC, fiind principial mai rapide de cât cele
microprogramate.
- microprogramată – U.C. care generează comenzile pe baza unui aşa
numit microprogram de emulare a instrucţiunilor, stocat într-o memorie (ROM)
de microprogram. Acest microprogram conţine microrutine înlănţuite, în
corespondenţă cu fazele de execuţie ale fiecărei instrucţiuni (aducere,
decodificare, execuţie etc.) Comenzile hardware se generează prin decodificarea
câmpurilor aferente microinstrucţiunilor. Microprogramarea este mai flexibilă
decât proiectarea cablată, permiţând o dezvoltarea facilă a setului de instrucţiuni
de la o versiune la alta (prin înscrierea microcodului afent noii instrucțiuni în
memoria de microprogram). Uzual, o UC microprogramată este mai lentă decât
una cablată, prin faptul că, principial, fiecare microinstrucţiune se procesează în
doi cicli: aducere şi execuţie (generare comenzi). Desigur, aceștia se pot
suprapune în timp (overlapping). Această metodă de proiectare a fost introdusă
în anul 1951 printr-un articol al lui Maurice Wilkes (Universitatea din
Cambridge, Marea Britanie) şi adoptată pe plan comercial de către IBM abia la
începuturile anilor ‘60. A fost şi mai este încă utilizată pe modelele de
procesoare CISC.

547
Dl. Lucian N. VINȚAN este profesor universitar (2000) și (primul)
conducător de doctorate (2001) în domeniul „Calculatoare și tehnologia
informației”, la Universitatea „Lucian Blaga” din Sibiu. Profesorul VINȚAN
este expert în arhitectura sistemelor de calcul, optimizare multi-obiectiv și
metode de text-mining. A publicat peste 140 de articole științifice în reviste
prestigioase de specialitate (ex. IEEE Transactions on Computer-Aided Design
of Integrated Circuits and Systems, Concurrency and Computation: Practice
and Experience, Journal of Systems Architecture, IET Computers & Digital
Techniques, Microprocessors and Microsystems etc.) precum și în conferințe
internaționale de referință. Peste 40 dintre acestea sunt indexate/cotate (ISI)
Thomson Reuters. Lucrările sale au înregistrat până în anul 2016 circa 600 de
citări internaționale independente. A publicat 6 cărți științifice, dintre care
două au fost redactate în limba engleză.
A obținut titlul onorific de Visiting Research Fellow de la Universitatea
din Hertfordshire, Anglia (2002). În anul 2005 i s-a acordat Premiul “Tudor
Tănăsescu” al Academiei Române. În anul 2005 a fost ales membru
corespondent, iar în anul 2012 membru titular al Academiei de Științe
Tehnice din România. Din anul 2005 este expert activ al Comisiei Europene
în domeniul sistemelor de calcul. A fost membru al comitetelor științifice de
program a peste 130 conferințe internaționale, în această calitate realizând sute
de recenzii.

548
549

S-ar putea să vă placă și