Documente Academic
Documente Profesional
Documente Cultură
VINȚAN
ISBN: 978-606-25-0276-8
http://www.matrixrom.ro/romanian/editura/domenii/cuprins.php?cuprins=FA50
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:
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.
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:
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.
12
CUPRINS
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
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
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:
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
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:
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.
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)
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
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):
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ă.
33
Figura 1.5. Circuit DRAM
RAS (Row Address Strobe – strob adresă rând), CAS (Column Address
Strobe – strob adresă coloană)
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…).
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ă),
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ță.
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.
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.
39
Figura 1.6.2. Decodificatorul de memorie proiectat
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).
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.
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:
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
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.
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.
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
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
51
Module PWM (Pulse Width Modulation)
Controler de întreruperi
52
2. ARHITECTURA SISTEMULUI IERARHIZAT DE MEMORIE
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.
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:
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.
60
Figura 2.3. Cache complet asociativ
Î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.
Î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.
64
Scriere Hit Scriere dată în blocul din cache
(WB)
Tabelul 2.3.Tipuri de acces în cache
65
Aşadar:
I-CACHE=32, D-CACHE=128
60
miss rate %
50
40
30
20
10
0
256 512 1024 2048 4096
s-cache capacity
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:
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.
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].
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).
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
2 Simple Victim
Issue Selective Victim
1,5
Rate
1
0,5
0
AM
queens
tree
sort
perm
bubble
matrix
tower
puzzle
HM
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
sort
matrix
puzzle
tower
perm
AM
HM
tree
∑ 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.
∑(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
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
78
S-a notat ai = X i − X mediu și bi = Yi − Ymediu .
(a1x+b1)2+(a2x+b2)2+… +(anx+bn)2=0.
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
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.
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 × = clk1 + p ⋅ d
R R
M h + 3× Iv
Tv = clk1 + p ⋅ v + v
R R
81
M h + 3 × Is
Ts = clk1 + 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
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.
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:
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%.
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.
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).
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.
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.
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ă.
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ă.
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.
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.
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:
98
3. PROCESOARE PIPELINE SCALARE CU SET OPTIMIZAT DE
INSTRUCŢIUNI
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).
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:
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:
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:
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)
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
}
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:
107
Figura 3.5. Lucrul în ferestre de registre
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.
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.
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;
}
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
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 ().
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).
#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));
}
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
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:
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.
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).
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
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
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
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.
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.
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
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].
129
3.4.4.1. HAZARDURI STRUCTURALE (HS): PROBLEME IMPLICATE
ŞI SOLUŢII
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ă.
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
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.
132
Figura 3.16 Principiul de lansare procese într-o structură pipeline cu hazarduri
structurale
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.
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".
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ă.
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.
137
Figura 3.22. Implementarea "forwarding"-ului
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.
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.
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):
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:
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).
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
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)
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.)
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.)
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.
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
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!
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ă
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ă.
165
Figura 3.35. Structură de predicţie de tip PAg
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.
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:
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.
• 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
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
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]:
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.
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.
p = p ⋅ A (matricial) şi
N
∑a
j =1
ij = 1 (Relația de echilibru sau de normalizare)
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):
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].
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).
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
187
n
O( X ) = W ⋅ X = w0 + ∑ wk ⋅ xk , reprezintă ieșirea neuronului artificial (hiper-planul
k =1
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!)
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:
189
E(w0,w1) = 0.5[(1 – w0 – w1)2 + (–1 – w0 + w1)2] = w02+ (w1-1)2.
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
∂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
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]:
192
For each learning pair (xd, td), DO:
Compute the output Od
For each wk, do:
wk ← wk + α (t d − Od ) x dk
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
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
Sign O
Prediction
Figura 3.37.4. Schema bloc a predictorului perceptron
195
• Sumatoarele binare utilizate pentru predicție au timpul de sumare
suficient de mic pentru a îndeplini cerințele de timp real.
196
adaptiv (care învață). Schemele următoare arată modul de implementare a unor
asemenea abordări hibride.
197
3.4.5. PROBLEMA EXCEPŢIILOR ÎN PROCESOARELE RISC
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ă
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.
ST 4 ( Ri ), R1
LD R2, 8 ( Rj )
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;
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:
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.
205
clar, convingător.
206
4. PROCESOARE CU EXECUŢII MULTIPLE ALE
INSTRUCŢIUNILOR. MULTIPROCESOARE
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.
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:
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.
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
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.
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.
217
unitatea F dedicată instrucţiunilor în virgulă mobilă (FPP)
unitatea B (Branch) dedicată instrucţiunilor de ramificaţie program
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)
219
sload (ld_adr1,target1)
sload (ld_adr2,target2)
if (a>b)
scheck (target1, recovery_adr1)
else
scheck (target2, recovery_adr2)
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:
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:
Î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
222
În acest caz execuţia predicativă este neeficientă.
Cazul II
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)
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
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
227
4.2. MODELE DE PROCESARE ÎN ARHITECTURILE
SUPERSCALARE
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.
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ă).
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.
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.
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
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:
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
237
Tabelul 4.9. Contextul procesorului aferent buclei de program
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.
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
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:
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ă.
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.
250
1. Ce se întâmplă în <Tail_ROB> după decodificarea instrucțiunii curente?
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).
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).
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.
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.
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])
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.
PC=743644 : I1
I2
I3
I4
I5 (branch condiţionat)
PC=342234 : I6
I7
I8
I9 (branch condiţionat)
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.
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.
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.
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.
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
268
Time
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.
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
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.
275
Frontend Backend
Generare
Analiza Generare
format Optimizare
lexicală cod obiect
intermediar
Cod Sursă Cod Obiect
Analiza Analiza
sintactică semantică
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.
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:
278
Tabelul 4.11.Execuţia unui program neoptimizat pe un procesor
superscalar
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.
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):
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
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.
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.
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
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):
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.
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.
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
…
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.
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 ];}
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).
293
Figura 4.20. Graful de control aferent secvenţei de optimizat
294
Figura 4.21. Graful dependenţelor pentru trace-ul considerat
295
Tabelul 4.16. Ordinea de execuţie în urma optimizării
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).
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:
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
300
reprezintă o preocupare esenţială la ora actuală, implementată în majoritatea
compilatoarelor.
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:
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:
303
8: ADD sum, sum, R1
9: CLT count, a_ptr, end_ptr
10: JPMT count, LOOP
EXIT:
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:
305
În a doua fază, instrucţiunile selectate din cele trei iteraţii se grupează în
cadrul unei noi bucle, ca mai jos:
306
LD F0, - 16(R1); (i+2)/3/n; încărcare
SUBI R1, R1, #8;
BNEZ R1, LOOP;
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.
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.
Avantajele TTA
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:
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.
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.
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
1
Lb = ∑ kp (k ) = λ
k ∈[1, ∞ )
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.
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.
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.
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).
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).
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.
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
329
I1: R1 <- 1
I2: BRANCH <Cond>, I4; If <Cond>=True, salt la I4
I3: R1 <- 0
I4: R2 <- R1+4
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%.
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])
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ă.
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.
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:
n −1
∑ Pr ob(T
k =0
n −1 = kT ) = 1
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
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 )
ET (n) = T + T (n − 1)(1 − p )
nT n
S ( n) = = ≥1
ET (n) 1 + (n − 1)(1 − p )
1
S (∞) = lim S (n) =
n →∞ 1− p
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%.
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}.
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
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)
352
instrucțiunilor sau chiar al (micro)firelor de execuție concurente, după cum s-a
arătat.
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.
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
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
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.
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).
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);
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);
for i = 1 to 100 do
360
if(X(i) diferit de 0) then
X(i) = X(i) + Y(i);
endif
enddo
x = 0;
for i = 1 to 100 do
x = x + A(i) * B(i); 200 de operații:100 +, 100 *
enddo
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
for i = 1 to n do
X(i) = Y(i) + Z(i); (1)
Y(i+3) = X(i + 4); (2)
enddo
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.
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:
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ă)
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:
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:
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
------------------------------------------------------------------------------------------
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:
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:
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.
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
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
G M
a) Dacă ≤ ⇒ Koptim = 0 (monoprocesare)
C 2
G M
b) Dacă > ⇒ Koptim = M/2 (procesare paralelă omogenă)
C 2
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
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
ET1 = G×M
Î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
C N
ET = MaxG ⋅ Max(k i ), ∑ k i ⋅ (M − k i )
2 i =1
G C⋅M 2 G
≅ ⇒ N optim = ⋅
N 2 N C
375
În acest caz, putem scrie că timpul de procesare este:
ET N = G ⋅ Max(k i ) + C ⋅ 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
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
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ă
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.
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.
384
Figura 4.43. Sistem multiprocesor UMA cu memorie comună partajată
distribuită fizic (MFk=modul fizic memorie)
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
387
Figura 4.43.c. Problema mapării aplicației pe NoC
388
În vederea recepției unui mesaj sunt necesari următorii pași importanți:
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.)
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.
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.
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).
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.
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).
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.
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).
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).
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.)
400
Figura 4.50. Interconectare hipercub (4-dimensional)
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.
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)
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:
404
O nerespectare a unuia dintre cele trei principii enuntate mai sus, poate
conduce la incoerenţa sistemului de memorie.
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:
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)
Î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":
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)
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.
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.
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)
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.
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.
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.
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.
• 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
418
Exemplu (clasic):
(P1) (P2)
A=0; B=0;
---- ----
A=1; B=1;
L1: if(B==0)... L2: if(A==0)...
419
procesorului, să se ocupe în continuare de procesul de scriere efectivă a
datei în memorie, degrevând astfel procesorul de acest proces.
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].
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].
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.
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.
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ă.
428
Figura 4.53. Paralelism la nivelul thread-urilor, cu paralelism la nivelul
memoriei
private i, j, k;
shared A[N,N], B[N,N], C[N,N], N;
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. */
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:
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.*/
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:
Atomicitatea
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.
434
done = done -1; /*decrementează în mod atomic variabila partajată;
control-based synchronization*/
end critical /*unlock*/
return;
end procedure
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):
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
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ă
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*/
}
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.
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
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.
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/.
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
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
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
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.
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
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.
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).
456
Figura 4.53.c Puterea totală medie per core
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
458
multiplele posibilități de partajare a cache-urilor de nivel 1 respectiv 2, într-un
sistem de tip dual-core.
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.
464
Figura 4.53.2 Inconsistență a valorii datorată predicției acesteia
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.
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:
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ă
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:
473
Figura 4.54. Curba Pareto pentru o problemă de minimizare cu două obiective
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.
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
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
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
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.
µ A→ B ( x, y ) = min(1,1 − µ A ( x) + µ B ( y ) ) ; Lukasiewicz
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:
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):
∑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 )
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.
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:
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.
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:
Î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
490
superscalară / SMT, augmentată cu un predictor de valori pentru instrucțiunile
Load critice (cu miss în sub-sistemul de cache-uri) [Gel12].
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.
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.
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
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:
liniară.
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].
497
6. PROBLEME PROPUSE SPRE REZOLVARE
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.
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ă ?
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.
ST (R9), R6
LD R10, (R9)
ST 4(R5), R8
LD R9, 8(R6)
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.
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
501
Pe arhitectura dată se procesează următoarea secvenţă succesivă de 32
instrucţiuni, caracterizată de hazarduri RAW aferente.
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ă.
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.
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ă.
504
EPIC etc., în cadrul sistemelor de calcul de uz general (laptopuri, desk-top-
uri, servere etc.) ?
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
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;
1: R1<-- (R11)+(R12)
2: R1<-- (R1)+(R13)
3: R2 <-- (R3)+4
4: R2 <-- (R1)+(R2)
5: R1<-- (R14)+(R15)
6: R1<-- (R1)+(R16)
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.
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).
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.
510
c) AND R5, R7, R8.
dest
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).
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 ?
1: R1 ← (R11) + (R12)
2: R1 ← (R1) + (R13)
3: R2 ← (R3) + 4
4: R2 ← (R1) + (R2)
5: R1 ← (R14) + (R15)
6: R1 ← (R1) + (R16)
513
vederea eliminării acestei dificultăţi ? În ce constă noutatea «principială» a
predictoarelor corelate pe două nivele ?
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.
k k
C n şi An
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.
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?
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++;
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.
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.
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.
Se cere:
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:
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;
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)...
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ă.
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
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?
6.
a.) Definiți notiunea de sistem multiprocesor de tip UMA (Uniform
Memory Access).
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.
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++;
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.
K = X[i-4]+12;
L = Y[j+5] XOR K;
Z[K+2] = K AND L;
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
5.
a.) Definiți noțiunea de sistem multiprocesor de tip UMA (Uniform
Memory Access).
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.
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
529
BIBLIOGRAFIE SELECTIVĂ
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
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
532
Mapping, Microprocessors and Microsystems, Vol. 37, Issue 1, pp. 65-78,
ISSN: 0141-9331, Elsevier, February 2013
533
[Vin07] VINȚAN N. LUCIAN – Prediction Techniques in Advanced
Computing Architectures (în limba engleză), Matrix Rom Publishing House,
Bucharest, 2007
534
of Integrated Circuits and Systems, vol. 35, issue 7, ISSN: 0278-0070, DOI
10.1109/TCAD.2015.2501299, 2016 (online 2015, print 2016)
535
GLOSAR DE TERMENI TEHNICI UTILIZAȚI
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.
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).
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.
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.
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.
541
maparea dinamică (run-time) a concurenței pe diferitele unități hardware de
procesare.
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.
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.
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.)
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.
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