Nel passaggio dal datapath a singolo ciclo al datapath a pipeline, sono stati aggiunti diversi elementi per supportare la suddivisione in stadi e gestire il flusso corretto di dati e segnali di controllo:


SUDDIVISIONE IN STADI

Registri di pipeline:

I registri di pipeline sono stati aggiunti tra ogni stadio per memorizzare i dati e i segnali di controllo necessari per il ciclo successivo:

  • IF/ID: (64 bit) Registra l’istruzione prelevata e l’indirizzo del Program Counter (PC).
  • ID/EX: (128 bit) Memorizza i dati letti dai registri, i segnali di controllo e l’istruzione decodificata.
  • EX/MEM: (97 bit) Contiene i risultati dell’ALU e i segnali di controllo per la memoria.
  • MEM/WB: (64 bit) Trasporta i dati letti dalla memoria e i risultati finali verso lo stadio di scrittura (Write Back).

Gestione del numero del registro di destinazione:

Bus utili a trasportare il numero del registro di destinazione di un’istruzione attraverso gli stadi della pipeline fino al MEM/WB, fondamentali per garantire che i dati siano scritti nel registro corretto nello stadio WB, soprattutto per le istruzioni load.

Bus blu basso nell’immagine.


DIAGRAMMI DELLE PIPELINE

Immaginiamo le seguenti istruzioni:

lw x10, 40(x1)
sub x11, x2, x3
add x12, x3, x4
lw x13, 48(x1)
add x14, x5, x6

Ci sono due modi di rappresentare l’esecuzione di questo programma con pipeline:

Diagramma a più cicli di clock

Diagramma a singolo ciclo di clock

Corrispondente al quinto ciclo di clock del diagramma a più cicli


ESEMPIO DI ESECUZIONE DI LOAD

**CC 1

**CC2

**CC3

**CC4

**CC5


IMPLEMENTAZIONE DI CU E ALU CONTROL

Control Unit (CU):

Stessi segnali dell’implementazione a single cycle ma che in questo caso vengono utilizzati in stadi diversi:

FaseALUOpALUSrcBranchMemReadMemWriteMemToRegRegWrite
IF
ID
EX
MEM
WB
Dato che in fase IF e in fase ID si verificano sempre le stesse azioni ad ogni ciclo di clock (qualsiasi istruzione sia), i segnali di controllo devono essere generati durante lo stadio di ID, per essere trasportati agli stadi successivi (EX, MEM, WB) insieme all’istruzione a cui si riferiscono.
Quindi, come per le istruzioni, anche i segnali vengono mano a mano registrati nei registri tra stadi (ID/EX, EX/MEM, MEM/WB), in modo da essere propagati da ID al loro stadio di appartenenza:

ALU Control:

Stessa funzione dell’implementazione a single cycle ma con il vincolo di posizione nello stadio EX, dove appunto si trova l’ALU.


DATAPATH COMPLETO SENZA GESTIONE DI HAZARD


IMPLEMENTAZIONE CON FORWARDING

Una CPU a pipeline reale tiene conto anche della gestione del forwarding.

Esempio:

Prendiamo ad esempio il seguente script:

sub x2, x1, x3   # Produce x2
and x12, x2, x5  # Usa x2
or x13, x6, x2   # Usa x2 
add x14, x2, x2  # Usa x2 
sw x15,100(x2)   # Usa x2 

sub x2, x1, x3 modifica il valore di x2 da 10 a -20 allo stadio WB (CC5) Utilizzando un diagramma con più cicli di clock:

sub x2, x1, x3and x12, x2, x5: 1.a hazard (legge ancora 10). sub x2, x1, x3or x13, x6, x2: 2.b hazard (legge ancora 10). sub x2, x1, x3add x14, x2, x2: NO hazard (legge -20). sub x2, x1, x3sw x15,100(x2): NO hazard (legge -20).

Vediamo come identificare e risolvere questi hazard sui dati:


1. Identificazione degli hazard

leggi prima qui Riprendendo l’esempio iniziale:

sub x2, x1, x3   # Produce x2
and x12, x2, x5  # Usa x2
or x13, x6, x2   # Usa x2 
add x14, x2, x2  # Usa x2 
sw x15,100(x2)   # Usa x2 

Confronto tutte le istruzioni con la prima (che ha il registro x2 come rd):

1. Hazard tra sub x2, x1, x3 e and x12, x2, x5: (si verifica quando and si trova in ID/EX e sub si trova in EX/MEMhazard 1) ID/EX.rs1 di and x12, x2, x5 = x2 EX/MEM.rd di sub x2, x1, x3 = x2 EX/MEM.RegWrite = 1 rd ≠ 0 hazard su rs1 (x2) → hazard 1.a

2. Hazard tra sub x2, x1, x3 e or x13, x6, x2: (si verifica quando or si trova in ID/EX e and si trova in MEM/WBhazard 2) ID/EX.rs2 di or x13, x6, x2 = x2 MEM/WB.rd di sub x2, x1, x3 = x2 MEM/WB.RegWrite = 1 rd ≠ 0 hazard su rs2 (x2) → hazard 2.b

3. Hazard tra sub x2, x1, x3 e add x14, x2, x2: (si verifica quando add si trova in ID/EX e sub ha già scritto il risultato → NO hazard)

4. Hazard tra sub x2, x1, x3 e sw x15, 100(x2): (si verifica quando sw si trova in ID/EX e sub ha già scritto il risultato → NO hazard)


2. Propagazione dei dati corretti

Diagramma a più cicli

Una volta identificati i data hazard nei vari casi procediamo a bypassarli con la tecnica del forwarding:

Il valore aggiornato di x2 calcolato da sub x2, x1, x3 (pari a -20) viene prelevato direttamente dal registro di pipeline (es. EX/MEM) e inoltrato (forwarded) alla componente che lo richiede, evitando così l’uso del valore obsoleto (pari a 10) presente nel file di registri, e anche l’uso di stalli.

A livello di hardware:

N.B manca il mux di ALUSrc sull’uscita del mux di PropagaB

Vanno aggiunte 3 componenti principali nello stadio EX: Due Mux (multiplexer) sui due input dell’ALU (A e B) e una Forwarding Unit (unità di controllo propagazione).


MUX A:

Selettore:

segnale PropagaA proveniente dalla Forwarding Unit.

Output:

  • PropagaA = 00 → ID/EX.ReadData1 (valore standard letto dai registri)
  • PropagaA = 01 → EX/MEM.ALUResult (forwarding da istruzione 1 ciclo prima)
  • PropagaA = 10 → MEM/WB.WriteData (forwarding da istruzione 2 cicli prima)

MUX B:

Selettore:

segnale PropagaB proveniente dalla Forwarding Unit.

Output:

  • PropagaB = 00 → ID/EX.ReadData1 (valore standard letto dai registri)
  • PropagaB = 01 → EX/MEM.ALUResult (forwarding da istruzione 1 ciclo prima)
  • PropagaB = 10 → MEM/WB.WriteData (forwarding da istruzione 2 cicli prima)
  • (se ALUSrc == 1, invece va usato l’immediato)

Forwarding unit:

Input:

(condizioni per lo stallo)

  • ID/EX.RegisterRs1 e ID/EX.RegisterRs2
  • EX/MEM.RegisterRd, EX/MEM.RegWrite
  • MEM/WB.RegisterRd, MEM/WB.RegWrite

Output:

PropagaA, PropagaB (2-bit ciascuno) per controllare i mux


Tabella delle propagazioni
Segnale di selezioneOrigine del datoSpiegazione
PropagaA = 00ID/EX.ReadData1Il primo operando (rs1) viene letto direttamente dal file di registri (valore normale, senza hazard).
PropagaA = 01EX/MEM.ALUResultIl primo operando (rs1) viene forwardato dal risultato della ALU dell’istruzione immediatamente precedente (1 ciclo prima).
PropagaA = 10MEM/WB.WriteDataIl primo operando (rs1) viene forwardato dal risultato dell’istruzione di due cicli prima (MEM/WB), già pronto per il write back.
PropagaB = 00ID/EX.ReadData2Il secondo operando (rs2) viene letto direttamente dal Datapath Single-Cycle (valore normale, senza hazard).
PropagaB = 01EX/MEM.ALUResultIl secondo operando (rs2) viene forwardato dal risultato della ALU dell’istruzione immediatamente precedente (1 ciclo prima).
PropagaB = 10MEM/WB.WriteDataIl secondo operando (rs2) viene forwardato dal risultato dell’istruzione di due cicli prima (MEM/WB), già pronto per il write back.

Esempio:

// Forward A
if (EX/MEM.RegWrite == 1 && EX/MEM.rd != 0 && EX/MEM.rd == ID/EX.rs1)
    ForwardA = 10; // da EX/MEM
if (MEM/WB.RegWrite == 1 && MEM/WB.rd != 0 && MEM/WB.rd == ID/EX.rs1)
    ForwardA = 01; // da MEM/WB
else
    ForwardA = 00; // da registr
// Forward B → stessa logica ma con rs2

IMPLEMETNAZIONE CON STALL

Una CPU a pipeline reale tiene conto anche della gestione degli stalli.

Esempio:

Prendiamo ad esempio il seguente script:

lw x2, 20(x1)    # Produce x2 (lw -> No Forwarding)
and x4, x2, x5   # Usa x2
or x8, x2, x6    # Usa x2 
add x9, x4, x2   # Usa x2 
sub x1, x6, x7   # Non usa x2 

N.B. nell’esempio teniamo conto di una CPU con forwarding tra MEM e EX.

Il load (lw) è un caso speciale: Il dato da memoria non è disponibile subito, ma solo alla fine dello stadio MEM (MEM/WB) , quindi il risultato non può essere forwardato in tempo se serve già nello stadio EX del ciclo successivo → serve uno stallo (load-use hazard).

lw x2, 20(x1)and x4, x2, x5: Load-use hazard (stall). lw x2, 20(x1)or x8, x2, x6: 2.b hazard (forwarding no stall). lw x2, 20(x1)add x9, x4, x2: NO hazard (valore di x2 già in memoria) lw x2, 20(x1)sub x1, x6, x7: NO hazard (registri diversi)

Vediamo come identificare e risolvere questi hazard sui dati:


1. Identificazione degli hazard

leggi prima qui Riprendendo l’esempio iniziale:

lw x2, 20(x1)    # Produce x2 (lw -> No Forwarding)
and x4, x2, x5   # Usa x2
or x8, x2, x6    # Usa x2 
add x9, x4, x2   # Usa x2 
sub x1, x6, x7   # Non usa x2 

Confronto tutte le istruzioni con la prima (che ha il registro x2 come rd):

1. Hazard tra lw x2, 20(x1) e and x4, x2, x5: (lw non ha ancora superato MEM dunque l’hazard è Load-Use) (si verifica quando and si trova in ID/EX e lw si trova in EX/MEMhazard Load-Use) ID/EX.rs1 di and x4, x2, x5 = x2 IF/ID.rd di lw x2, 20(x1) = x2 ID/EX.MemRead == 1 rd ≠ 0 hazard su rs1 (x2) → hazard Load-Use

2. Hazard tra lw x2, 20(x1) e or x8, x2, x6: (lw ha superato MEM dunque l’hazard NON è Load-Use) (si verifica quando or si trova in ID/EX e lw si trova in MEM/WBhazard 2) ID/EX.rs1 di or x8, x2, x6 = x2 MEM/WB di lw x2, 20(x1) = x2 EX/MEM.RegWrite == 1 rd ≠ 0 hazard su rs1 (x2) → hazard 2.a

3. Hazard tra lw x2, 20(x1) e add x9, x4, x2: (si verifica quando add si trova in ID/EX e lw ha già scritto il risultato → NO hazard)

4. Hazard tra lw x2, 20(x1) e sub x1, x6, x7: ID/EX.rs1 di sub x1, x6, x7 = x6 ID/EX.rs2 di sub x1, x6, x7 = x7 x6 != x2 && x7 != x2NO hazard


2. Stallo della pipeline

Diagramma a più cicli

Una volta identificati i load-use hazard procediamo a correggerli con la tecnica dello stall:

Quando and entra in ID, l’hazard detection unit si accorge che: ID/EX.MemRead == 1 → l’istruzione precedente (attualmente salvata in ID/EX) è un lw ID/EX.RegisterRd == IF/ID.RegisterRs1 → e x2 viene usato ora → quindi si tratta di un hazard Load-Use e si genera uno stallo: tutti i segnali della CU vanno a 0 (inserimento di nop) e il PC non viene aggiornato:

ALUSrc      = 0
ALUOp       = 00
MemRead     = 0
MemWrite    = 0
RegWrite    = 0
MemToReg    = 0
Branch      = 0
PCWrite     = 0
IF/ID.Write = 0

Dunque l’istruzione and si blocca nella fase ID (diventando nop) ma rimane salvata in IF/ID, in questo modo la si può riprendere al ciclo di clock successivo (PCWrite = 0 e IF/ID.Write = 0) .

A livello di software:

lw x2, 20(x1) 
nop
and x4, x2, x5

*In un sistema con hazard detection unit, (come RISC-V) non serve scriverlo: l’hardware inietta automaticamente la bolla quando rileva un hazard load-use.

A livello di hardware:

Vanno aggiunte 2 componenti principali nello stadio EX: Un Mux (multiplexer) tra la CU e il registro ID/EX, e un’Hazard Detection Unit (unità di rilevamento degli hazard).


MUX:

Selettore:

segnale stall proveniente dall’Hazard Detection Unit.

Output:

  • stall = 0 → segnali della CU.
  • stall = 1 → 0 (tutti i segnali a 0).

Hazard Detection Unit:

Input:

(condizioni per lo stallo)

  • ID/EX.RegisterRs1 e ID/EX.RegisterRs1.
  • IF/ID.RegisterRd.
  • ID/EX.MemRead.

Output:

  • PcWrite (hazard load-use → PCWrite = 0, il Program Counter non avanza).
  • IF/IDWrite (hazard load-use → IF/ID.Write = 0, non si aggiorna il registro IF/ID)
  • stall (hazard load-use → stall = 1).

IMPLEMENTAZIONE CON ANTICIPAZIONE DEI SALTI

Una CPU a pipeline reale tiene conto anche della gestione dell’anticipazione del controllo sui salti condizionati.

Esempio:

Prendiamo ad esempio il seguente script:

beq x1, x0, 16     # Se x1 == x0 ⟶ lw x4, 100(x7)
and x12, x2, x5    # Se x1 == x0 ⟶ flush
or x13, x6, x2     # Se x1 == x0 ⟶ flush
add x14, x2, x2    # Se x1 == x0 ⟶ flsuh
lw x4, 100(x7)     # Target del branch

Se il salto è preso (x1 == x0) il PC punta direttamente all’istruzione lw x4, 100(x7).

beq x1, x0, 16and x12, x2, x5: control hazard (fetch prima del controllo). beq x1, x0, 16or x13, x6, x2: control hazard (fetch prima del controllo). beq x1, x0, 16add x14, x2, x2: control hazard (fetch prima del controllo).

Vediamo come ridurre questi hazard sul controllo:


1. Riduzione dei ritardi associati ai salti

Una volta identificati i control hazard (ogni volta che ho un salto condizionato) procediamo a ridurne il peso anticipando la decisione del salto e l’aggiornamento del PC:

Diagramma a più cicli:

ISTRUZIONE100ps200ps300ps400ps500ps600ps700ps800ps900ps1000ps1100ps1200ps1300ps1400ps1500ps1600ps1700ps1800ps
beq x1, x0, 16IFIF==ID==IDEXEX\\\\
and x12, x2, x5IF (flush)IF (flush)IDIDEXEX\\WBWB
or x13, x6, x2IFIFIDIDEXEX\\WBWB
add x14, x2, x2IFIFIDIDEXEX\\WBWB
lw x4, 100(x7)IFIFIDIDEXEXMEMMEMWBWB

Abbiamo:

  • anticipato il controllo della condizione di salto da EX a ID
  • anticipato l’aggiornamento del PC dal 4°CC a ID

(se si vuole evitare il flush su and x12, x2, x5 basta inserire uno stallo dopo beq x1, x0, 16).

A livello di hardware:

Vanno aggiunte 2 componenti principali nello stadio ID: Un Comparator tra il Register File e il registro ID/EX, e un’Adder per calcolare l’indirizzo di salto (PC + offset). Il Comparator ha la funzione di verificare che la condizione del salto sia verificata (per beq e bne) in quanto va a comparare i valori dei registri. L’adder calcola l’indirizzo corretto del PC e manda il segnale in input al MUX che seleziona tra: PC + 4 (incremento normale del PC) e PC + offset (indirizzo di salto). Inoltre viene aggiornata la CU includendo il segnale di controllo IF.Flush che va in input al registro IF/ID e genera il Flush dell’istruzione nel caso di temporaneo aggiornamento errato del PC (esempio di and x12, x2, x5).

*N.B. Oltre ai componenti per implementare l’anticipazione dei salti sono state aggiunte alcune propagazioni dagli stadi EX, MEM e WB. Questi forwarding di dati servono per eliminare eventuali data hazard che si andrebbero a verificare tra istruzioni precedenti ad un salto condizionato e l’istruzione del salto stessa (temporanea mancanza in memoria o tra i registri degli operandi del salto).


IMPLEMENTAZIONE CON GESTIONE DELLE TRAP

Si deve aggiungere:


SCAUSE e SEPC:

2 componenti principali nello stadio MEM: Il registro SCAUSE e il registro e SEPC (CSR Registers): SCAUSE: contiene il codice della causa della trap che si è verificata. SEPC: salva il Program Counter (PC) dell’istruzione che ha generato la trap, per poter riprendere l’esecuzione dopo la gestione. Entrambi vanno aggiunti in uscita dal registro di pipeline ID/EX.


Ingresso condizione trap:

Un ingresso al MUX che seleziona l’indirizzo di salto per gestire l’eccezione/interruzione (ad esempio, l’indirizzo della routine di servizio degli interrupt, indirizzo 1C090000).


Controllo Flush:

Nuovi segnali di Flush (ID.Flush, EX.Flush) e l’OR in EX che viene usato per la gestione del segnale di flush nello stadio EX Serve a combinare diversi segnali di flush che possono arrivare, ad esempio, sia da una eccezione sia da un hazard di controllo o da altre condizioni che richiedono di svuotare (flushare) la pipeline a partire dallo stadio EX. Quindi l’unità di controllo ora gestisce anche i segnali relativi a eccezioni/interrupt, decidendo quando fare il flush, salvare SEPC/SCAUSE, e deviare il PC.