Mihai Budiu -- mihaib+@cs.cmu.edu
http://www.cs.cmu.edu/~mihaib/
11 noiembrie 1998
Procesoarele RISC au apărut, în prima jumătate a anilor '80; era începutul unei revoluții în arhitectura calculatoarelor. Ideea era că în loc de a oferi o sumedenie de operațiuni exotice, este mai eficace pentru un procesor să ofere un set restrîns de operațiuni, pe care le poate executa cu foarte mare viteză. Primele procesoare RISC conțineau între 25 de mii și 40 de mii de tranzistoare.
În ziua de astăzi Pentium II conține 15 milioane de tranzistoare! Văzut dinafară Pentium II nu este un RISC, dar arhitectura internă este de acest tip.
Întrebarea este: ce s-a întîmplat cu toate tranzistoarele astea, unde au fost ``înghițite''? Setul de instrucțiuni al unui procesor modern este practic identic cu al unui procesor de acum 15 ani (exceptînd ornamente de genul MMX), deci funcționalitatea oferită este neschimbată. Atunci la ce treabă au fost înhămați toți tranzistorii? Răspunsul este: pentru a crește performanța.
Acest articol prezintă una dintre tehnicile cele mai simple la îndemîna unui proiectant pentru a crește performanța unui microprocesor; practic toate procesoarele fabricate în ziua de azi o folosesc. Este vorba de tehnica ``benzii de asamblare'', numită și pipeline. Vom vedea că e vorba de o idee extrem de simplă și eficace. Vom vedea apoi că proprietățile unei benzi de asamblare ridică o grămadă de noi probleme, și că implementarea tehnicii este extrem de complicată, cerînd un suport arhitectural substanțial. Vom mai vedea și unele dintre metodele folosite pentru a rezolva problemele ivite.
Există două forțe motoare responsabile de creșterea spectaculoasă a puterii de calcul a microprocesoarelor. Una dintre aceste forțe este tehnologia de fabricație și miniaturizarea. Miniaturizarea unui circuit integrat digital se măsoară în microni; distanța care se indică este, grosolan vorbind, distanța între două sîrme adiacente pe o suprafață a circuitului. Tehnologia curentă dominantă în acest an este undeva între 0.25 și 0.35 microni. Pentru comparație, un fir de păr omenesc are cam 25 de microni, adică de 100 de ori mai mult!
Scăderea dimensiunilor înseamnă că distanța dintre circuite scade, deci semnalul electric poate parcurge mai multe dintre ele în aceeași unitate de timp. De asemenea înseamnă că frecvența ceasului poate fi crescută. Ceasul unui microprocesor este echivalentul insului de la tobă de pe o galeră romană din antichitate: bate ritmul cu care se sincronizează toate părțile componente (vîslașii). Frecvențele atinse la ora actuală de procesoarele comerciale sunt de 600Mhz: 600 de milioane de operații pe secundă, pentru procesoarele Alpha 21264.
Atît despre tehnologie. A doua metodă de creștere a performanței, care este oarecum o consecință indirectă a miniaturizării, este paralelismul. Dacă ai mai mulți vîslași barca merge mai repede. Cu cît vîslașii sunt mai mici, cu atît poți pune mai mulți în cală. (Desigur, analogia nu e perfectă, pentru că, spre deosebire de galere, pentru un microprocesor un vîslaș mai mic face la fel de multă treabă ca unul mare.)
Practic toate articolele din seria aceasta vor discuta numai despre această a doua metodă. Vom vedea că procesoarele moderne consacră o cantitate impresionantă de resurse pentru a stoarce o cît de mică îmbunătățire a performanței (acolo se duc cele 15 milioane de tranzistoare ale lui Pentium). În general o dublare a mărimii circuitului produce mult mai puțin decît o dublare a performanței, dar în ziua de azi și obținerea cîtorva procente este o realizare meritorie.
Introducem acum cititorului prima tehnică de prelucrare paralelă, numită ``tehnica benzii de asamblare''.
Averea considerabilă a lui Henry Ford se datora, cel puțin în parte, metodei sale inovatoare de a organiza munca la fabricile sale de automobile: lucrătorii stau așezați de-a lungul unei benzi, iar mașinile neterminate merg de la unul la altul. Fiecare execută asupra mașinii o singură operațiune, după care o pasează mai departe. Asta e banda de asamblare.
Pentru a face mai evidente beneficiile tehnicii, îmi permit să o ilustrez cu încă un exemplu. Să presupunem că vreți să investiți într-o spălătorie/uscătorie (Nufărul?). Ce i-ați recomanda proprietarului: să cumpere 10 mașini de spălat care centrifughează și usucă, sau, cu aceiași bani, 9 mașini care spală și 9 care usucă? Presupunem că un spălatul și uscatul durează la fel de mult (oricare mașină am folosi), o jumătate de oră.
Răspunsul este: cu cele 9 perechi de mașini eficiența este cu 80% mai mare! Iată de ce: să presupunem că avem un șuvoi constant de consumatori. Atunci cu mașinile de spălat putem face 10 încărcături de rufe în fiecare oră. Cu mașinile perechi însă putem face 9 încărcături la fiecare jumătate de oră, așa cum arată tabelul 1:
|
Ei bine, exact aceeași idee poate fi aplicată în cazul construcției microprocesoarelor! În acest caz banda de asamblare se numește pipeline, sau conductă. De aici și titlul articolului de față. Am oarecare dificultăți în a alege o traducere rezonabilă a termenului, așa că pe parcursul articolului voi folosi variați termeni, incluzînd pe cel de ``țeavă''.
Cum se aplică deci conceptul în cazul microprocesoarelor? Care e treaba unui microprocesor? Să execute, una cîte una, instrucțiunile programelor scrise de utilizatori. Dar execuția unei instrucțiuni se poate descrie ca o serie de pași succesivi; ceva de genul: adu instrucțiunea din memorie, uită-te ce fel de instrucțiune este (o operație aritmetică/logică, un apel de procedură, un salt, etc.), decide care date trebuie procesate (care sunt regiștrii1 care conțin acele date), extrage datele din regiștri, efectuează operația asupra datelor, pune rezultatul la loc unde trebuie, și o ia de la capăt cu instrucțiunea următoare. În figura 1 avem structura unui procesor ipotetic pe care indicăm cinci stagii prin care o instrucțiune trece în prelucrare.
![]() |
Ei bine, instrucțiunile joacă exact rolul rufelor: dacă avem în procesor cîte un circuit independent pentru fiecare din aceste numeroase sarcini, atunci putem pune aceste circuite să lucreze simultan pe instrucțiuni succesive. Astfel, în timp ce instrucțiunea 1 pune rezultatul la loc (stagiul de acces la memorie), instrucțiunea 2 operează asupra propriilor date (stagiul de execuție), instrucțiunea 3 tocmai extrage datele (stagiul de decodificare), iar instrucțiunea 4 tocmai este adusă din memorie (stagiul de citire).
Observați că -- aparent -- cîștigul pe care l-am obține transformînd procesorul într-un pipeline, este pe gratis: și un procesor obișnuit are nevoie de toate aceste circuite, însă atunci cînd folosea unul dintre ele, celelalte erau inutile. Că lucrurile nu stau chiar așa vom vedea în secțiunile următoare.
Cartea ``canonică'' pentru studiul arhitecturii calculatoarelor moderne este Hennessy and Patterson ``Computer Architecture -- a Quantitative Approach'', publicată de editura Morgan Kaufmann în ediția a doua în 1995. Această carte este de fapt versiunea pentru cursuri doctorale a unei alte cărți de aceiași autori. Cartea este excelent scrisă, iar capitolele 3 și 4 sunt dedicate în întregime tehnicii de pipelining, în total 250 de pagini. Dacă subiectul vă interesează cu adevărat, vă recomand să obțineți această carte; dacă nu, poate că articolul acesta este suficient de ilustrativ. Acest articol va discuta doar despre tehnicile elementare folosite în pipelining; despre tehnicile avansate (multiple-issue, out-of-order execution, etc.) voi scrie probabil un altul. Desenele din acest text, și diagrama procesorului ipotetic, sunt bazate pe prezentarea din această carte.
Dacă un procesor este implementat ca un pipeline, atunci între diferitele stagii ale țevii se află niște ``tampoane'', care izolează stagiile unul de altul. Arhitectural vorbind, tampoanele sunt de fapt tot niște regiștri, numiți pipeline registers. Fiecare din acești regiștri este comandat de ceasul microprocesorului, și încarcă în interior toate rezultatele procesării obținute din stagiul anterior: instrucțiunea, rezultatele parțiale, informații de stare, etc. Figura 2 arată segmentarea procesorului de mai sus.
![]() |
Vom vedea că regiștrii de separație au un rol important pentru blocajul țevii în anumite circumstanțe.
Ce cîștigăm cu ajutorul țevilor?
Păi, în primul rînd, fiecare din stagii este mai scurt decît întregul, deci se poate executa mai repede decît dacă am executa toate stagiile unul după altul. Putem deci mări frecvența ceasului; este exact același fenomen ca la mașinile de spălat de mai sus (încărcături la jumătate de oră în loc de o oră).
În al doilea rînd, așa cum am văzut, acum toate stagiile sunt folosite simultan, fiecare pentru altă instrucțiune. Aceasta este o sursă de paralelism, care implică o altă creștere a performanței.
Observați că succesul acestei metode se bazează pe faptul că avem de procesat un șir de instrucțiuni extrem de lung, continuu. Dacă la spălătorie vin rufe din două în două ore, atunci nu cîștigăm nimic din faptul că putem scoate o nouă serie la fiecare jumătate de oră.
Observați că durează o vreme de cînd prima instrucțiune intră în țeavă pînă cînd termină execuția: atîția cicli de ceas cîte stagii avem. Latența (latency), sau durata propagării unei instrucțiuni prin pipeline, este mai mare decît în cazul unui procesor fără pipeline, pentru că am adăugat durata stocării datelor în regiștrii de separație. Pe de altă parte, observați că după ce prima instrucțiune iese din țeavă, a doua se termină în ciclul imediat următor. Deci rata de execuție (throughput) este de o instrucțiune pe ciclu de ceas! Distincția între latency și throughput este extrem de importantă.
Acest fenomen apare într-o formă exacerbată în cazul rețelelor de calculatoare, în care există doi parametri independenți: durata propagării datelor între două calculatoare, și viteza de transmisiune a datelor. Putem avea linii cu durată de propagare mică (de exemplu cu latența de 2ms), dar cu viteză mică, cum ar fi un modem de 14.4Kbps. Putem avea însă o linie cu durată de propagare extrem de mare (500ms), dar cu o viteza foarte mare, cum ar fi un canal de transmisiune prin satelit, de 2Mbps. Interesant este că în cazul rețelelor de calculatoare produsul acestor cantități (latența * rata de transmisie) este cel mai important; cu cît produsul este mai mare, cu atît susținerea performanței rețelei este mai greu de obținut. Dar am divagat; sper să revin asupra acestei teme într-un alt articol. Înapoi la țevile noastre.
Să observăm că viteza la care putem pune ceasul este limitată de cel mai lent stagiu din țeavă. Asta pentru că toți lucrătorii trebuie să lucreze cu ritmul celui mai încet dintre ei. Din această cauză, o împărțire a sarcinilor la 5 circuite nu garantează o creștere a vitezei de 5 ori. Să presupunem că cele 5 stagii durează 3, 3, 3, 5, respectiv 3 nanosecunde, și că întîrzierea unui registru de separație este de 2 nanosecunde. Atunci circuitul fără țeavă execută o instrucțiune la fiecare 3+3+3+5+3=17 nanosecunde, și asta dă și viteza ceasului. Pe de altă parte, circuitul din ``felii'' execută o instrucțiune la 5+2 nanosecunde (5 pentru stagiul cel mai lent, plus două ns pentru propagarea prin registru). Iată deci cum, deși am împărțit sarcina la 5, din cauza imbalansului creșterea de viteză obținută este de numai 17/7 = 2.42 ori.
Trebuie să vă temperez și mai tare entuziasmul în ceea ce privește pipeline-urile: mai există o grămadă de probleme pe care le-am trecut cu vederea, dar care devin evidente de îndată ce ne aplecăm puțin asupra construcției.
Problema este că, foarte adesea, nu putem executa mai multe instrucțiuni consecutive chiar una după alta, pentru că anumite constrîngeri fac acest lucru imposibil. Acest gen de interferență între instrucțiuni consecutive se numește în engleză hazard. Voi folosi în româna termenul ``dependențe'', deși acesta nu este tocmai exact2. Despre ce fel de dependențe este vorba?
Prima problemă care poate apărea provine din faptul că una dintre aserțiunile mele de mai sus poate fi falsă; anume aceasta: ``cînd folosea unul dintre [stagii], celelalte erau inutile''. Iată un exemplu în care acest lucru nu este adevărat: un procesor trebuie după fiecare instrucțiune să incrementeze adresa de unde se ia următoarea instrucțiune (adresa este aflată în registrul numit ``Program Counter''). Pentru că incrementarea este o operațiune aritmetică, procesorul ar putea folosi pentru acest scop unitatea aritmetică-logică (în figura noastră am fi avut în loc de ALU și circuitul de incrementare un singur circuit). Aici avem deci un conflict: o altă instrucțiune, aflată în stagiul de calcul ar putea dori să folosească acea unitate în același timp pentru că trebuie să adune două numere.
Astfel de ``hazards'' sunt numite ``structurale'', pentru că structura procesorului nu permite executarea anumitor tipuri de instrucțiuni simultan în stagii diferite. În exemplul nostru, instrucțiunile care nu folosesc unitatea aritmetică (de pildă o instrucțiune de salt absolut) nu cauzează nici un fel de conflicte.
Putem da și alte exemple de dependențe structurale: mai multe instrucțiuni vor să acceseze simultan același registru, mai multe instrucțiuni vor să acceseze memoria (de pildă o instrucțiune care vrea să-și adune operanzii și tocmai îi citește și una care a terminat și vrea să scrie rezultatul), sau instrucțiuni a căror execuție durează mai mulți cicli de ceas.
Un exemplu de ultima speță sunt de pildă operațiile în virgulă flotantă (adică cu numere ``reale'', nu întregi) care durează uneori zeci de cicli de ceas, iar procesorul de obicei are o singură unitate de calcul în virgulă flotantă.
Teoretic un hazard structural se poate oricînd evita duplicînd unitățile funcționale care sunt în conflict; această soluție nu este însă întotdeauna fezabilă. De pildă, dacă o împărțire3 durează 10 cicli, atunci ar trebui să avem 10 împărțitoare pentru a permite execuția a 10 împărțiri succesive.
Tot pentru a evita dependențele structurale procesoarele moderne au, așa cum arătam în figură, două cache-uri L14 separate: unul pentru instrucțiuni (I-cache) și unul pentru date (D-cache): în acest fel se poate citi o instrucțiune simultan cu scrierea rezultatelor alteia.
Vom vedea un pic mai jos cum rezolvă un procesor astfel de dependențe.
O dependență mult mai subtilă este cea a datelor. Să presupunem că avem un program cu două instrucțiuni consecutive: una care scrie numărul 2 în registrul 3, iar următoarea care adaugă valoarea 5 acelui registru.
În tabelul 2 vedem cum progresează aceste instrucțiuni într-o țeavă ipotetică care seamănă cu cea descrisă mai sus. Am mai pus niște instrucțiuni noop, care nu fac nimic, în jur, pentru a ilustra mai bine
|
Diagrama din tabelul 2 este tipică pentru a descrie evoluția datelor într-un pipeline. Programul este scris pe verticală, ceasul este marcat pe orizontală. Starea țevii poate fi citită pe verticală, de sus în jos. Căsuța de la instrucțiunea 1 și ceasul 3 arată în care dintre fazele țevii se află acea instrucțiune la momentul 3, în cazul nostru în stagiul de acces la memorie (M).
Care e problema? Problema este că instrucțiunea de scriere pune datele în registrul 3 abia cînd atinge ultimul stagiu din țeavă. Pe de altă parte, instrucțiunea de acumulare citește valoarea registrului 3 atunci cînd este în faza de decodificare. Dar datorită suprapunerilor, decodificarea instrucțiunii de acumulare se face la momentul 3, iar scrierea la momentul 5! Din cauza suprapunerii, în execuție am inversat ordinea în timp în care se petrec două operațiuni. Dacă nu facem nimic, rezultatul final va fi desigur greșit, pentru că acumularea nu vede efectul scrierii.
Înainte de a vedea ce e de făcut să inspectăm o altă dificultate care poate apărea.
Un tip special de dependențe este cauzat de instrucțiunile de salt. O instrucțiune de salt indică o întrerupere în fluxul normal al programului. Neplăcerea apare din faptul că execuția instrucțiunii de salt se termină destul de tîrziu, abia cînd instrucțiunea a calculat adresa finală de destinație. Dar între timp în țeavă au intrat o grămadă de instrucțiuni, toate cele care urmau imediat. Evident, acestea nu trebuie executate (sau vor fi executate dacă saltul este condiționat de o condiție care este falsă).
Astfel de dependențe se numesc ``dependențe de control'', din cauză că sunt produse de modificări în ``controlul'' (ordinea de execuție) a programului.
Voi consacra un articol întreg acestui tip de dependențe, pentru că efectul lor este catastrofal asupra performanței, și pentru că sunt extrem de greu de reparat fără a pierde toate avantajele unui pipeline. Impactul salturilor este extrem de important din două motive:
Să estimăm costul unei astfel de situații pentru procesorul nostru de mai sus, cu întîrzieri de 3, 3, 3, 5, 3 nanosecunde. Atunci procesorul va executa 7 instrucțiuni fiecare la cîte un ciclu, după care timp de 4 cicli va umple din nou țeava golită de un salt. Asta înseamnă 7*6 + 4*6 = 66 cicli pentru 7 instrucțiuni, adică 66/7 = 9.42ns/instrucțiune în medie. Creșterea de performanță a procesorului față de modelul fără țeavă este acum de 17/9.42 = 1.8 ori. Și încă am presupus că celelalte feluri de dependențe nu cauzează nici o întîrziere!
Mai avem de-a face cu o ultimă neplăcere, care este mult mai greu de rezolvat decît cele indicate anterior, și despre care nici nu vom vorbi prea mult în acest articol.
Cînd anumite condiții excepționale se ivesc, procesorul trebuie să întrerupă complet fluxul execuției, să execute o rutină specială, iar apoi uneori să reia programul întrerupt din exact același punct. Evenimentele care cauzează această întrerupere intempestivă se numesc excepții. Există multe feluri de excepții, iar tratamentul lor depinde de tip. Exemple de excepții: împărțirea prin zero, accesul la o pagină de memorie virtuală care nu se află în memoria fizică, indicația terminării unui transfer de către un dispozitiv periferic (ex. discul), întîlnirea unui punct de oprire (breakpoint) pus de un program de depanare, etc.
Problema cea mai mare nu este în asemenea cazuri oprirea programului și saltul (care seamănă teribil cu o instrucțiune obișnuită de salt), ci repornirea. În momentul apariției unei excepții, în țeavă se pot afla o sumedenie de instrucțiuni, cine știe de unde de prin memorie (poate la una dintre ele s-a ajuns printr-un salt sau chiar o altă excepție), etc.
Dacă procesorul vrea să poată relua execuția după o excepție, atunci trebuie să posede o grămadă de circuite care mențin foarte multă informație despre întreaga stare a țevii, pentru a permite repornirea. Un astfel de pipeline se numește restartable, și este extrem de complicat.
Concluzia care se desprinde este că e mai ușor de zis decît de făcut un pipeline. Dar lupta pentru supremație în performanța se dă pe viață și pe moarte între marile companii (nu e o metaforă: cei mai mari concurenți ai lui Intel, Cyrix și AMD au trebuit să fie cumpărați de alte mari companii, IBM, respectiv National Semiconductors, pentru a supraviețui).
Ca atare trebuie găsite soluții. În restul articolului vom investiga niște soluții pentru problema dependențelor (dar nu și pentru cea a excepțiilor restartabile).
O posibilă soluție (care însă nu este folosită decît parțial) este ca în software să garantăm că astfel de lucruri nu se pot întîmpla. Compilatorul care generează cod pentru un microprocesor ar trebui să ne asigure că două instrucțiuni care se vor afla simultan în țeavă nu vor interfera una cu alta. Compilatorul poate obține acest efect umplînd spațiul dintre două astfel de instrucțiuni cu instrucțiuni care nu fac nimic (no-op: no operation).
Din păcate soluția aceasta nu este viabilă. Un motiv este că ar trebui ca compilatorul să aibă cunoștințe intime despre arhitectura internă a țevii (ca să știe ce depinde de cine). Dar Pentium, Pentium Pro și Pentium II au arhitecturi interne complet diferite, deși implementează același set de instrucțiuni; ne-ar trebui deci un compilator diferit pentru fiecare mașina; mai mult decît atît, programele de pe una n-ar mai merge pe alta, din cauză ca altele ar fi dependențele care trebuie evitate.
Pe de altă parte, compilatoarele moderne încearcă din răsputeri să ajute hardware-ul, fără a garanta neapărat generarea unui cod lipsit complet de dependențe. Operațiunea numită code scheduling (ordonarea codului) este extrem de importantă pentru a mări performanța programelor. Practic compilatoarele încearcă să aranjeze instrucțiunile codului în așa fel încît instrucțiuni care depind una de alta (cum sunt cele două de mai sus) sunt cît mai departe una de alta. De exemplu, dacă după cele două instrucțiuni de mai sus vine o instrucțiune care incrementează registrul 2, atunci ultimele două instrucțiuni pot fi schimbate între ele fără a modifica rezultatul programului, tocmai pentru că sunt independente. Îmi propun să discut într-un articol separat despre tehnicile de creștere a performanței folosite de compilatoare, așa că trec acum la prezentarea primei soluții reale folosite pentru a rezolva problema dependențelor.
Dacă o instrucțiune nu poate progresa în țeavă din cauză că-i lipsesc anumite resurse (în exemplul de mai sus, registrul 3 încă nu are valoarea necesară), atunci aceste instrucțiuni sunt pur și simplu ținute pe loc în țeavă în aceleași stagii, în timp ce cele care le preced sunt lăsate să continue. Oprirea unei instrucțiuni se numește blocaj, sau stall. În spatele instrucțiunii care continuă se formează un gol, numit ``bulă'' (cu b mic) (bubble). Bula este de fapt o instrucțiune noop: no operation, care nu are nici un efect.
Tabelul 3 prezintă evoluția programului de mai sus atunci cînd apare o bulă.
|
De îndată ce instrucțiunea care avea resursele cerute își termină execuția, instrucțiunile de după ele, care aveau nevoie de resurse, își pot continua execuția în mod obișnuit. Blocarea unei instrucțiuni în țeavă este relativ ușor de produs: registrul de separație de dinaintea acelui stagiu nu mai citește valorile produse de stagiul anterior, ci păstrează vechiul său conținut.
Soluția prin care compilatorul inserează instrucțiuni inutile se numește statică, pentru că programul apoi rămîne neschimbat. Prin contrast, atunci cînd bulele sunt create de microprocesorul însuși atunci cînd programul se execută, tehnica se numește dinamică. Observați că și în acest caz programul din memorie este neschimbat: bula apare doar în țeavă, și apoi dispare.
Soluția asta pare acceptabilă. Care este costul pe care trebuie să-l plătim?
În primul rînd trebuie hardware special pentru a detecta dependențele. Asta înseamnă practic o serie de comparatoare: un comparator compară registrul în care scrie instrucțiunea care se află în stagiul 4 cu regiștrii citiți de instrucțiunile din stagiile 3, 2. Un alt comparator se uită să vadă dacă tipurile de instrucțiuni din aceste stagii într-adevăr folosesc regiștri (de pildă o instrucțiune de salt imediat nu folosește nici un registru. Dacă, de pildă, instrucțiunea din stagiul 4 va scrie în registrul consumat de instrucțiunea din stagiul 2, atunci stagiul 2 este blocat, stagiile 3 și 4 avansează, iar în stagiul 3 se formează o bulă.
Un al doilea preț pe care-l plătim pentru blocaj este scăderea performanței. Din cauză că instrucțiunile nu avansează și țeava procesează nimicuri, rata efectivă cu care instrucțiunile sunt executate scade sub una pe ciclu. Cît de mult scade, depinde de o sumedenie de factori, începînd cu calitatea compilatorului și terminînd cu capacitatea țevii de a evita blocajele prin următorul mijloc pe care-l analizăm, înaintarea.
În lupta lor acerbă cu nanosecundele, proiectanții de microprocesoare au găsit încă o soluție pentru a rezolva dependențele. În exemplul de mai sus, o instrucțiune aflată în stagiul de decodificare avea nevoie de niște date pe care instrucțiunea aflată în stagiul de execuție tocmai le calculase. Cea din stagiul de decodificare însă trebuia să aștepte ca rezultatul calculelor să fie pus într-un registru, ceea ce se va întîmpla abia mai tîrziu. Ideea este atunci simplă: din moment ce tot am deja rezultatul, de ce să mai aștept să fie scris în registru și apoi să-l iau din nou? Ce-ar fi dacă producătorul ar trimite rezultatul pe o scurtătură direct la consumator?
Această tehnică se numește ``înaintare'' (forwarding), deși strict vorbind este o ``înapoiere'', pentru că datele sunt trimise înapoi în țeavă, de la producător la consumator. Asta permite instrucțiunii din stagiul de decodificare să-și continue execuția netulburată, fără a mai fi nevoie de introducerea unei bule. Înaintarea este ilustrată în figura 3
![]() |
Soluția este clar preferabilă, pentru că viteza de execuție rămîne aceeași, o instrucțiune pe ciclu. Costul plătit este însă în hardware: pe lîngă circuitele prezente în cazul blocării, înaintarea are nevoie de o mulțime de trasee speciale și circuite de selecție (multiplexoare) pentru a trimite datele pe scurtături. Dar budgetul în tranzistoare al proiectanților este atît de mare încît nu se dau în lături de la așa ceva.
Procesoarele reale folosesc deci o mixtură a celor trei tehnici prezentate anterior; nu orice se poate rezolva cu înaintare, așa că blocajul este practic întotdeauna necesar. (Un exemplu ar fi o instrucțiune care termină de calculat un rezultat abia în stagiul 4, dar care este consumat de instrucțiunea următoare în stagiul 2. În momentul în care a doua instrucțiune vrea valoarea, ea nici nu există, deci înaintarea nu este de nici un folos., deci trebuie folosit un blocaj.)
Încheiem aici incursiunea noastră în arhitectura procesoarelor-țeavă. În comparație cu alte tehnici, pe care sperăm că le vom putea prezenta în articole ulterioare, pipelining-ul este pînă la urmă o metodă destul de simplă și cu o eficacitate imediată. În procesoarele moderne nu numai fazele de execuție ale unei instrucțiuni sunt ``pipelined'', ci și operațiile aritmetice; de pildă unele unități aritmetice calculează o înmulțire pe 32 de biți în 5-6 cicli de ceas, dar pentru că înmulțitorul este pipelined, el poate accepta o nouă pereche de operanzi la fiecare ciclu, și poate produce rezultate cu aceeași frecvență.
Procesoarele moderne folosesc un întreg arsenal de alte tehnici, extrem de sofisticate, dintre care enumerăm aici:
Dacă vrem să rezumăm învățămintele acestui text într-o frază putem spune așa: atunci cînd ai destulă mînă de lucru și multe activități de făcut, e foarte economic să organizezi munca într-o bandă de asamblare; specializarea fiecărui lucrător garantează eficacitate sporită în muncă, iar durata medie de producere a unui rezultat este egală cu lungimea activității celui mai lent dintre lucrători.
Gata cu distracția, acum la treabă. Fiecare să-și reia locul în țeavă!