În acest laborator vom învăța cum funcționează un pipeline pentru instrucțiuni și vom implementa un pipeline simplu.
Execuția unei singure instrucțiuni poate fi împărțită în mai multe etape. Acestea pot fi oricât de multe sau puține și pot avea diverse funcții. O împărțire comună, utilizată de primele procesoare RISC, are 5 stagii:
O instrucțiune nu trebuie să folosească toate stagiile disponibile. Spre exemplu, pe o arhitectura load/store, doar instrucțiunile load și store accesează memoria, deci folosesc etapa MEM, restul folosesc doar registrele de procesor, deci folosesc etapa WB.
Execuția normală a instrucțiunilor are loc în mod secvențial. Deci execuția unei instrucțiuni va începe după terminarea ultimului stagiu al instrucțiunii precedente. Astfel, considerând modelul descris mai sus, pentru a executa 3 instrucțiuni avem nevoie de 15 cicli de ceas (considerând că toate instrucțiunile folosesc toate stagiile).
A executa instrucțiuni în pipeline înseamnă că există câte o instrucțiune în fiecare stagiu la orice moment dat. În timp ce o instrucțiune este în stagiul de execuție, alta este decodificată, alta este adusă din memorie etc. Asta duce la o reducere semnificativă a timpului de execuție al unui program. În același timp în care puteam executa doar 3 instrucțiuni fără pipeline, putem executa 11 instrucțiuni în pipeline.
Trebuie totuși să aplicăm o constrângere asupra stagiilor: fiecare stagiu trebuie să dureze la fel de mult timp.
O problemă introdusă de execuția în pipeline sunt hazardele. Un hazard este situația în care execuția următoarei instrucțiuni este imposibilă sau va produce rezultate greșite.
Un hazard de date se produce atunci când o instrucțiune are nevoie de un rezultat într-un stagiu al execuției, iar acel rezultat este calculat într-un stagiu al altei instrucțiuni ce va fi executat mai târziu.
Hazard | Exemplu | Registru afectat |
---|---|---|
Read After Write (RAW) | R2 <- R1 + R3 R4 <- R2 + R2 | R2 |
Write After Read (WAR) | R4 <- R1 + R5 R5 <- R1 + R2 | R5 |
Write After Write (WAW) | R2 <- R4 + R7 R2 <- R1 + R3 | R2 |
Aceste hazarde sunt rezolvate prin bubbling, execuția out-of-order sau pasarea operanzilor înainte (forwarding).
Hazardele structurale au loc atunci când două instrucțiuni trebuie să folosească aceeași unitate hardware în același timp. Un astfel de hazard are loc, spre exemplu, într-un sistem cu o singură memorie când se încearcă citirea unei instrucțiuni în același timp cu scrierea rezultatului altei instrucțiuni în aceeași memorie.
Aceste hazarde sunt rezolvate prin separarea unităților astfel încât această situație să nu se poată întâmpla (e.g. separarea memoriei de instrucțiuni față de memoria de date) sau prin bubbling.
Hazardele de control au loc după instrucțiunile de salt condițional. Problema aici este că procesorul trebuie să încarce următoarea instrucțiune înainte de a ști rezultatul saltului, deci s-ar putea să încarce instrucțiunea greșită. În cazul unei predicții eronate, instrucțiunea încărcată trebuie eliminată din pipeline.
Aceste hazarde pot fi rezolvate prin bubbling, dar de obicei sunt tratate de unități specializate de branch prediction.
Vom implementa un procesor extrem de simplu, ce execută instrucțiuni în 5 stagii: IF, ID, EX, MEM și WB.
Task 0 și Task 1 presupun realizarea unui procesor ce va fi capabil să execute următoarele instrucțiuni din memoria de program:
xor r0, r1 and r2, r3 or r4, r5 neg r6 add r7, r8 sub r7, r2 mod r7, r2 div r20, r2 mul r4, r2 div r6, r4 add r0, r7 add r4, r6 add r2, r20 add r0, r4 add r0, r2
Task 0 (3p) - Implementați logica procesorului făra pipeline în task01.v.
Cod instrucțiune (16 biți) | operand 1 (8 biți) | operand 2 (8 biți)
, unde fiecare operand reprezintă indexul unui registru.
e.g. instrucțiunea ADD R1, R1
este codificată astfel: 0000000000000001 00000001 00000001
.
Instrucțiunea NEG
are un singur operand (operand 1
).
Task 1 (6p) - Implementați logica procesorului cu pipeline în task11.v.
Variabilele care sunt folosite de componente consecutive ale pipeline-ului trebuie reținute și într-un registru special buffered. Spre exemplu, între memory și decoder avem variabila instruction folosită de ambele. Într-un ciclu de ceas, memory scrie în instruction, dar decoder are nevoie de valoarea de la ciclul de ceas anterior. Pentru aceasta, decoder va primi variabila instruction_buffer, care are valoarea lui instruction din ciclul anterior de ceas.
Task 2 (2.5p) - Completați memoria de program din task21.v astfel încât la final să se afle în registrul 0 valorea în zecimal 1234. (HINT: Ce valori se află inițial în registrele procesorului?)
Task 3 (0.5p) - Completați memoria de program din task31.v astfel încât la final să se afle în registrul 0 valorea în zecimal 1234. Fiți atenți la posibile hazarde!
Default.wcfg
. Acesta are toate semnalele incluse pentru debugging. Aveți grijă să nu suprascrieți Default.wcfg
când ieșiți din simulator.