Microarchitetture ad Alte Prestazioni: Gestione delle «Control Dependencies» Davide Bertozzi
Acknowledgement Most of this material is inspired/adapted/translated from the online courses of prof. Onor Mutlu at CMU: https://people.inf.ethz.ch/omutlu/teaching.html
Architettura di Riferimento Notare che con questa implementazione il branch è risolto pienamente solo alla fine della fase di MEM
Branch Hazards Ipotesi: Solo alla fine di MEM so decidere quale sarà il nuovo program counter! Branch Penalty Potenziale: 3 cicli! Intanto tre nuove istruzioni sono già entrate nella pipeline! La penalità aumenta in modo significativo con pipeline lunghe e con processori che fanno il fetch di più istruzioni contemporaneamente.
Branch Hazards Terminologia: Branch target (address): indirizzo a cui il branch salta Branch taken: il controllo viene trasferito ad un indirizzo diverso (target) da quello dell’istruzione successiva Branch not taken: viene eseguita l’istruzione successiva Branch prediction accuracy: percentuale di branch accuratamente predetti Fall-Through Path: percorso preso se il branch è NOT TAKEN CHE FARE IN PRESENZA DI UN BRANCH? Quali istruzioni caricare? Che fare con le istruzioni già caricate nel caso si rivelino essere quelle sbagliate? LA GESTIONE DI UN BRANCH PUO’ RIGUARDARE L’HARDWARE, IL SOFTWARE OPPURE ENTRAMBI
Control Dependencies E’ essenziale mantenere la pipeline piena con la corretta sequenza delle istruzioni “dinamiche” (a tempo di esecuzione). Soluzioni potenziali per gestire salti condizionali (che sono delle control-flow instructions): Stallare la pipeline finchè il branch non è risolto Predirre il prossimo fetch address (branch prediction) Branch ritardati (branch delay slot) Fare qualcos’altro (fine-grained multithreading) Eliminare le istruzioni di controllo (predicated execution) Fare il fetch da entrambi i percorsi possibili (multipath execution), se i relative indirizzi sono noti
Soluzione banale…ma non così banale! Idea: stallare la pipeline finchè il branch non viene risolto Time add $s4, $s5, $s6 Beq $s1, $s2, 40 add $s4, $s5, $s6 STALL Beq $s1, $s2, 40 add $s4, $s5, $s6 STALL STALL Beq $s1, $s2, 40 add $s4, $s5, $s6 STALL STALL STALL Beq $s1, $s2, 40 add $s4, $s5, $s6 or $s7, $s8, $s9 STALL STALL STALL Beq $s1, $s2, 40 or $s7, $s8, $s9 STALL STALL STALL Ma c’è un problema enorme: Per mettere uno stallo dopo un branch, il processore dovrebbe sapere che sta facendo il fetch di un branch! Ma non lo può sapere se non dopo la fase di decode! Se proprio vogliamo seguire questo approccio, Il compilatore deve mettere tre NOP (no-operation) dopo ogni branch (se l’ISA prevede il NOP, oppure istruzioni equivalenti come sll $zero, $zero, 0), oppure si fa una invalidazione e la macchina inserisce 2 stall. In alternativa, occorre usare la pipeline al 50% (fetch, poi capisci se è un branch,…)
Si Può Fare Meglio? Perchè non predirre sempre che next_PC = PC+4 così da poter fare il fetch di istruzioni ad ogni ciclo? E’ una predizione saggia? Cosa perdi se la predizione non è corretta? ~20% del mix di istruzioni è una istruzione di controllo ~50 % del “forward” control flow (i.e., if-then-else) è taken ~90% del “backward” control flow (i.e., loop back) è taken Complessivamente, circa il 70% dei salti è taken e il 30% not taken (Lee and Smitch, 1984) Ci attendiamo “next_PC = PC+4” circa nell’86% delle istruzioni in fetch, ma che ne è del rimanente 14%?
Next_PC = PC + 4 Idea: Predirre sempre che la prossima istruzione della sequenza è la prossima istruzione che deve essere eseguita In pratica, nessuna predizione, perchè questo è quello che accade di solito, salvo poi implementare il recovery da misprediction. Come si può migliorare? Idea: Massimizzare la probabilità che la prossima istruzione della sequenza sia davvero la prossima istruzione da eseguire. Come? Software: cambia il control flow graph in modo che la prossima istruzione più probabile sia nel not-taken path del branch Hardware: si può fare qualcosa di simile mediante una Trace Cache, ma non la vediamo in questo corso.
Soluzione Software Software: compilare un control flow graph tale che la prossima istruzione più probabile sia sul percorso “not-taken”. Supponiamo che grazie al «profiling del codice», Il compilatore sappia le probabilità di taken e not-taken Beq $s0,$s1,.. taken Not-taken Beq $s0,$s1,.. Codice se $s0 = $s1 Codice se $s0 ≠ $s1 taken Not-taken Codice se $s0 = $s1 Codice se $s0 ≠ $s1 80% 20% Bne $s0,$s1,.. taken Not-taken Codice se $s0 ≠ $s1 Codice se $s0 = $s1 Nome della tecnica: «Profile-guided code positioning» 20% 80%
Next_PC = PC + 4 In quale altro modo si può migliorare la performance? Idea: sbarazzandosi delle istruzioni di controllo (o meglio, minimizzandole) Come? Sbarazzandosi delle istruzioni di controllo non necessarie combinando i predicati (predicate combining) 2. Convertendo dipendenze di controllo in dipendenze dati predicated execution
Predicate Combining Condizioni complesse sono di solito convertite in branch multipli: if ((a == b) && (c < d) && (a > 5000)) { … } Vengono fuori almeno 3 salti condizionali…. Problema: questo aumento il numero di control dependencies, e il loro potenziale penalty. Idea: combinare le condizioni in modo da esporre solo un branch In pratica (assumiamo nuove pseudo-istruzioni CMPEQ, CMPLT, CMPGT, e una sintassi estesa per le istruzioni and e beq): CMPEQ condizione1, a, b #confronta a e b, e se uguali setta condizione1 a 1 CMPLT condizione2, c, d #se c è inferiore a d, setta condizione2 a 1 CMPGT condizione3, a, 5000 #se a è maggiore di 5000, setta condizione3 a 1 and condizione4, condizione1, condizione2 #accumula le condizioni and condizione 4, condizione3, condizione4 #accumula le condizioni BEQ condizione4, $zero, OFFSET #salta se tutte le condizioni sono verificate
Predicate Combining Condizioni complesse sono di solito convertite in branch multipli: if ((a == b) && (c < d) && (a > 5000)) { … } Vengono fuori almeno 3 salti condizionali…. Problema: questo aumento il numero di control dependencies, e della loro potenziale penalty. Idea: combinare le condizioni in modo da esporre solo un branch Alcuni ISA mettono a dispozione dei condition registers per memorizzare i predicati (es., IBM RS6000, IBM POWER). Un predicato corrisponde con il settaggio di un bit (vero/falso) del condition register Supporto per fast condition manipulation così da risparmiare istruzioni (es., and) Un singolo branch controlla il valore dei predicati combinati * Vantaggio: meno branch nel codice meno mispredictions/stalls * Svantaggio: esiste la possibilità che si compia lavoro non necessario -- se il primo predicato è falso, non c’è alcun motivo di elaborare gli altri predicati, eppure questi vengono elaborate.
Predicated Execution Idea: convertire dipendenze di controllo in dipendenze dati Assumiamo di avere una “Conditional Move instruction CMOV”: CMOV condizione, R1 R2 La cui semantica è: “R1 = (condizione == true) ? R2 : R1” Utilizzata nella maggior parte degli ISA moderni (x86, Alpha) (presuppone un’altra istruzione: “CMPLT condizione, R1, value” La cui semantica è “condizione = (R1 < value)? 1:0” Esempio di codice C compilato con branch oppure CMOV if (a < 5) {b = 4;} else {b = 3;} slti $s0, $s2, 5 beq $s0, $zero, Else li $s3, 4 J Exit Else: li $s3, 3 Exit: …. CMPLT condizione, a, 5 CMOV condizione, b 4; CMOV !condizione, b 3;
Predicated Execution Elimina i branch permette “straight line code” (cioè, “basic blocks” di codice sequenziale più lunghi) Vantaggi: La predizione “Always-not-taken” funziona meglio (branch più rari) Il compilatore ha più possibilità per ottimizzare il codice (branch più rari, blocchi più lunghi, più opportunità per il riordinamento delle istruzioni, risolvendo così ad esempio data hazards) Codice potenzialmente più corto e/o più efficiente Svantaggi: Svolgimento di lavoro inutile: fetch ed esecuzione di alcune istruzioni che poi non hanno effetto (negativo soprattutto per branch facili da predirre) Occorre fare il fetch sia del percorso “taken” sia di quello “untaken” Codice potenzialmente più lungo Necessario il supporto nell’ISA In questo modo, è possibile eliminare tutti i branch? No, i “loop (backward) branch” non possono essere eliminate, ma solo i “forward branch”. Permette di eliminare solo i forward branches, non i backward brances (dunque, non funziona per i loop, perché ad un certo punto bisogna decidere se saltare indietro oppure no.
Control Dependencies E’ essenziale mantenere la pipeline piena con la corretta sequenza delle istruzioni “dinamiche” (a tempo di esecuzione). Soluzioni potenziali per gestire salti condizionali (che sono delle control-flow instructions): Stallare la pipeline finchè il branch non è risolto Predirre il prossimo fetch address (branch prediction) Branch ritardati (branch delay slot) Fare qualcos’altro (fine-grained multithreading) Eliminare le istruzioni di controllo (predicated execution) Fare il fetch da entrambi i percorsi possibili (multipath execution), se i relative indirizzi sono noti
Delayed Branching Cambiamento della semantica di una istruzione di branch: Esegui il Branch dopo N istruzioni, oppure Esegui il Branch dopo N cicli di clock Idea: Ritardare l’esecuzione di un branch. Le N istruzioni (delay slots) che seguono un branch sono SEMPRE eseguite a prescindere dalla direzione (taken/not-taken) del branch. Problema: Come trovare istruzioni per riempire il(i) delay slot(s)? Regola: la condizione dei branch deve essere indipendente dalle istruzioni inserite nel delay slot Nota bene: per I salti incondizionati è molto più facile riempire i delay slots
Delayed Branching Esempio. Assumiamo una pipeline a due stadi (fetch, execute): Normal code: Timeline: Delayed branch code: Timeline: A A if ex if ex B C C A BC X A B A B C A BC X C B D BC C D E BC C E B BC F -- BC F G B X: G G -- X: G 6 cicli 5 cicli
Come Riempire il Delay Slot Se l’istruzione precedente non può essere schedulata, perché la condizione del branch dipende dal suo risultato: Il delay slot viene schedulato con una istruzione indipendente prima del branch. Se possibile, è il caso ideale, perché fornisce sempre speed-up Lo slot è schedulato dal target del branch. Si assume che il branch sia TAKEN con elevata probabilità. Altrimenti nel fall-through path devi compensare la sub con una add (fixup code) Lo slot è schedulato con una istruzione dal cammino di branch UNTAKEN. Se il branch è taken, occorre annullare l’effetto della sub con una add (fixup code) L’istruzione del delay slot è eseguita in ogni caso. Se la predizione è sbagliata, il lavoro fatto è stato inutile, ma va garantito il corretto funzionamento del programma attraverso operazioni di annullamento degli effetti.
Delayed Branching Vantaggi: + Tiene la pipeline piena con istruzioni utili ed in modo semplice, assumendo: 1. Numero di delay slots == numero di istruzioni per mantenere la pipeline piena finchè il branch non è risolto 2. Tutti i delay slot possono essere riempiti con istruzioni utili (!) Svantaggi: -- Non è facile riempire i delay slots (neppure con pipeline a 2 stadi!) 1. il numero di delay slot aumenta con la profondità della pipeline, e con il numero di istruzioni contemporaneamente in fetch (per architetture avanzate) -- vincola strettamente l’ISA alla microarchitettura 1. SPARC, MIPS, HP-PA: 1 delay slot cosa succede se cambia la profondità della pipeline nella prossima generazione dell’architettura?
Control Dependencies E’ essenziale mantenere la pipeline piena con la corretta sequenza delle istruzioni “dinamiche” (a tempo di esecuzione). Soluzioni potenziali per gestire salti condizionali (che sono delle control-flow instructions): Stallare la pipeline finchè il branch non è risolto Predirre il prossimo fetch address (branch prediction) Branch ritardati (branch delay slot) Fare qualcos’altro (fine-grained multithreading) Eliminare le istruzioni di controllo (predicated execution) Fare il fetch da entrambi i percorsi possibili (multipath execution), se i relative indirizzi sono noti
Multithreading a Grana Fine Idea: l’hardware ha contesti multipli (o hardware threads). Ad ogni ciclo, il motore di fetch fa il fetch da diversi contesti. Per tutto il tempo che ci mette un branch a risolversi, non c’è alcuna necessità di fare il fetch di un’altra istruzione dello stesso thread La latenza per la risoluzione del branch è sovrapposta con l’esecuzione di istruzioni degli altri thread Vantaggi + nessuna logica per la gestione di hazard dati e di controllo all’interno dello stesso thread Svantaggi -- Penalizza la “Single-thread performance” -- Hardware addizionale per memorizzare il “context” (es., un RegFile ed un PC per ogni thread) -- Se non ci sono thread a sufficienza, non si riesce a mascherare la latenza di un thread, e non aumenta neppure il throughput Stadi di pipeline
Multithreading a Grana Fine Idea: Commutare su un’altro thread hardware ad ogni ciclo in modo che nella pipeline non ci possano essere contemporaneamente due istruzioni dello stesso thread. Alle istruzioni di un thread, la macchina appare «non-pipelined» Trade-off system throughput vs. single-thread performance E’ la tecnica di riferimento per le GPU Ogni thread dedicato al processing di una porzione diversa di un’immagine
Multithreaded Pipeline Slide from Joel Emer Obiettivo: evidenziare il costo di tenere contesti multipli in hardware L’architettura SUN Niagara ha una pipeline multi-threaded Lettura Consigliata: Kongetira et al., “Niagara: A 32-Way Multithreaded Sparc Processor,” IEEE Micro 2005.
Control Dependencies E’ essenziale mantenere la pipeline piena con la corretta sequenza delle istruzioni “dinamiche” (a tempo di esecuzione). Soluzioni potenziali per gestire salti condizionali (che sono delle control-flow instructions): Stallare la pipeline finchè il branch non è risolto Predirre il prossimo fetch address (branch prediction) Branch ritardati (branch delay slot) Fare qualcos’altro (fine-grained multithreading) Eliminare le istruzioni di controllo (predicated execution) Fare il fetch da entrambi i percorsi possibili (multipath execution), se i relative indirizzi sono noti
Branch Prediction: Predizione della Prossima Istruzione per il Fetch PC 0x0004 0x0008 0x0007 0x0005 0x0006 ?? Assumiamo che il branch sia risolto nello stadio di WB I-Mem DEC RF WB 0x0001 LD R1, MEM[R0] D-mem 0x0002 ADD R2, R2, #1 Senza branch prediction 0x0003 BRZERO 0x0001 12 cicli al decode della LD 0x0004 ADD R3, R2, #1 0x0005 MUL R1, R2, R3 0x0006 LD R2, MEM[R2] Branch prediction 0x0007 LD R0, MEM[R2] 8 cicli
Misprediction Penalty PC Flush!! I-$ DEC RF WB 0x0001 LD R1, MEM[R0] 0x0007 0x0006 0x0005 0x0004 0x0003 0x0002 D-$ ADD R2, R2, #1 0x0003 BRZERO 0x0001 0x0004 ADD R3, R2, #1 0x0005 MUL R1, R2, R3 0x0006 LD R2, MEM[R2] 0x0007 LD R0, MEM[R2] Occorre invalidare le istruzioni negli stadi di fetch, decode, RF ed EXE (FLUSH DELLA PIPELINE)
Branch Prediction Che cosa esattamente devo predirre? Devo predirre che l’istruzione in fetch è un Branch Devo predirre la direzione del branch (taken, untaken) Devo eventualmente predirre il branch target address A Branch condition, TARGET Sono tutte le domande cui devo rispondere per poter fare il fetch della prossima istruzione dopo A: B1 B3 Pipeline Fetch Decode Rename Schedule RegisterRead Execute D B1 A B1 A D E F B1 A D E F B3 B1 A D E F B1 A D E F B1 A D E F B1 A D E F B1 A D B1 A D E F B1 A D E A B1 A D E F B1 A D E F E What to fetch next? Target Misprediction Detected! Flush the pipeline Fetch from the correct target Verify the Prediction F
Flush della Pipeline Rappresenta l’invalidazione di tutti gli stadi di pipeline che precedono la risoluzione di un branch Esempio. Assunzioni: 1- Branch risolto nel secondo stadio 2 – Misprediction penalty: 1 ciclo (cioè, 1 istruzione da invalidare)
Flush della Pipeline Rappresenta l’invalidazione di tutti gli stadi di pipeline che precedono la risoluzione di un branch Branch in corso di risoluzione: Misprediction! Istruzione predetta (in modo sbagliato)
Flush della Pipeline Rappresenta l’invalidazione di tutti gli stadi di pipeline che precedono la risoluzione di un branch Branch risolto che procede lungo la pipeline Segnali di controllo a zero per propagare la bolla Branch target corretto in fase di fetch Segnale di Flush con cui viene resettato il registro IF/ID Stadio in cui l’istruzione viene invalidata
Branch Prediction: Come si Implementa? Osservazione: assumendo che la direzione sia correttamente predetta (problema che per ora lasciamo aperto), Il Target address di un salto condizionale rimane lo stesso attraverso le invocazioni successive a tempo di esecuzione Target Address = PC+4+offset Perchè non memorizzarlo? Idea: Memorizzare il target address del branch la prima volta che viene eseguito, ed accedervi le volte successive tramite il PC se si predice “Taken” Questa memoria di appoggio prende il nome di “Branch Target Buffer (BTB)” o “Branch Target Address Cache”
Fetch Stage con BTB e Predizione della Direzione Direction predictor taken? PC + 4 Next Fetch Address Program Counter hit? Branch Target Buffer Indirizzo della Istruzione corrente (è un branch, ma la microarchitettura non può saperlo) Il Fetch mi fornisce sia l’istruzione da eseguire sia il prossimo valore del PC target address Instruction Memory Instruction fetch (branch)
Fetch Stage con BTB e Predizione della Direzione Direction predictor taken? PC + 4 Next Fetch Address Program Counter hit? Branch Target Buffer Indirizzo della Istruzione corrente (è un branch, ma la microarchitettura non può saperlo) Ma come faccio a sapere se è un branch? Un HIT nel BTB significa predirre che è un branch! target address Instruction Memory Instruction fetch (branch)
Fetch Stage con BTB e Predizione della Direzione Direction predictor La predizione «always untaken» non necessita di BTB, perché Next_PC=PC+4 taken? PC + 4 Next Fetch Address Program Counter hit? Branch Target Buffer Indirizzo della Istruzione corrente (è un branch, ma la microarchitettura non può saperlo) Grazie al BTB posso implementare ance una predizione «always taken», poiché ho il branch target address! target address Instruction Memory Instruction fetch (branch)
Performance Analysis La predizione della direzione determina la performance Ipotesi: predizione “always untaken” (cioè, PC_next=PC+4) Predizione corretta nessuna penalty ~86% delle volte Predizione incorretta 2 stalli (dipende da dove il branch è risolto) Assumiamo: no data hazards 20% di salti condizionali 70% di queste istruzioni sono “taken” Clocks-per-instruction (CPI) = [ 1 + (0.20*0.7) * 2 ] = = [ 1 + 0.14 * 2 ] = 1.28 Penalità per misprediction Probabilità di misprediction Possiamo ridurre questi due parametri?
Riduzione della Penalty Risolviamo la condizione e il target address prima possibile, ad esempio già nella fase di DECODE Calcolo del Branch Target Address Aggiungo logica di confronto in DECODE Aumenta la lunghezza del timing path in DECODE (cala la frequenza di clock?) Seleziono il prossimo PC sulla base della condizione risolta
Riduzione della Penalty Risolviamo la condizione e il target address prima possibile, ad esempio già nella fase di DECODE Un registro usato nella condizione potrebbe essere in viaggio lungo la pipeline (non ancora aggiornato). La forwarding unit lo deve inoltrare alla logica di confronto. Si complica la forwarding logic
Riduzione della Penalty Risolviamo la condizione e il target address prima possibile, ad esempio già nella fase di DECODE Il branch ha bisogno in DECODE di un registro che deve ancora essere «calcolato» da EXE (es., una istruzione R precede il branch, e calcola un registro che serve al branch per verificare la condizione). Occorre stallare la pipeline finchè EXE non calcola il registro. Si innescano più data hazards. Si complica la detection unit.
Branch Prediction Statica Come posso invece intervenire sulla probabilità di misprediction? Predizioni del tipo “Always Taken” oppure “Always Untaken” sono esempi di predizioni statiche = la predizione è fissa per ogni esecuzione dello stesso branch (es., anche con dati diversi o con una «storia» diversa con cui si è arrivati al branch in esame) Tecniche “Compile time (statiche)” Always not taken (non necessita di BTB) Always taken (in generale porta a meno CPI) BTFN (Backward taken, forward not taken) Profile based (likely direction) Program analysis based (likely direction) Direction predictor taken? La predizione statica può essere realizzata in hardware, oppure «suggerita» dal compilatore
Suggerimento «Statico» del Compilatore Il compilatore potrebbe dare suggerimenti all’hardware! Richiede che le istruzioni di branch abbiano un bit riservato che possa essere modificato dal compilatore Bit a «1» se ritiene probabile l’esito Taken Bit a «0» altrimenti Il compilatore può decidere il valore del bit in base al tipo di istruzione oppure al profiling del programma Esempio di predizione statica BTFN: Si calcola OFFSET = BTA – PC a tempo di compilazione Se OFFSET > 0, si predice Not-Taken Se OFFSET <0, si predice Taken (probabilmente è un ciclo)
Branch Prediction Dinamica Come posso invece intervenire sulla probabilità di misprediction? Accoppiate con predizioni del compilatore, le predizioni statiche possono raggiungere buona performance per pipeline corte. Ma per pipeline più lunghe, dove la misprediction penalty è maggiore? Meglio tentare predizioni dinamiche. Idea: Predirre la direzione dei branch sulla base di informazioni dinamiche (raccolte a tempo di esecuzione)
Branch Prediction Dinamica Vantaggi + Predizione basata sulla storia dell’esecuzione dei branch e/o sul suo “contesto” + Capacità di adattamento della politica di predizione a variazioni dinamiche del comportamento dei branch. + Non serve alcun profiling statico del codice Svantaggi -- Maggior complessità (è richiesto hardware aggiuntivo) Tecniche “Run time (dinamiche)” Last time prediction (single-bit) Two-bit counter based prediction Two-level prediction (global vs. local) Hybrid Direction predictor taken?
Last Time Predictor Last time predictor Richiede un singolo bit per ogni branch Indica in che direzione è andato il branch in esame l’ultima volta che è stato risolto Esempio di storia di un branch: TTTTTTTTTTNNNNNNNNNN 90% accuratezza di predizione predict taken not actually not taken Macchina a stati di un Last Time Predictor
Last Time Prediction Aggiornamento del branch prediction buffer: 1-bit Branch Prediction Buffer oppure estensione del BTB Branch Target Buffer Ultima volta? 0x00FA83 TAKEN Predico che sarà TAKEN anche questa volta Inverti il bit nel buffer E’ TAKEN? Flush delle istruzioni caricate NO SI Tutto come previsto!
Problema del Last Time Predictor Consideriamo un loop: Prediction: TAKEN! 1 MISPREDICTION quando esco dal loop: Predico «taken» invece è «untaken» 1 MISPREDICTION quando entro nel loop: Predico «untaken» invece è «taken» 2 MISPREDICTION PER OGNI LOOP, MA SOLO UNA E’ INEVITABILE Problema: sbaglia sempre due volte la predizione di un loop branch (alla prima iterazione e all’ultima iterazione). Accuratezza per un loop con N iterazioni = (N-2)/N + OK con loop branches dove il loop ha un gran numero di iterazioni -- Non va bene quando il numero di iterazioni è ridotto: TNTNTNTNTNTNTNTNTNTN 0% accuratezza di predizione COME SI PUO’ RISOLVERE? GUARDANDO ALLA STORIA, NON SOLO ALL’ULTIMA VOLTA, OPPURE NON ESSENDO COSI’ PRECIPITOSI NEL CAMBIARE STRATEGIA.
Miglioramento del Last Time Predictor Problema: Il “last-time predictor” cambia la sua predizione da TNT o da NTT troppo velocemente e questo nonostante il branch possa essere “per lo più” taken o not taken. Idea: Aggiungere “isteresi” al predittore in modo che la predizione non cambi alla prima misprediction. Utilizzare 2 bit per tenere conto della storia delle predizioni per un branch anzichè un singolo bit Questo predittore ha due stati la cui uscita è Taken, e due stati la cui uscita è Untaken. Lettura per gli appassionati: Smith, “A Study of Branch Prediction Strategies,” ISCA 1981.
Nuovo Predittore: Contatore a 2-bit Questa FSM inverte la predizione dopo due mispredictions consecutive Ci vogliono due bit per codificare 4 stati, da associare ad ogni branch. I due bit sono di solito memorizzati nel/gestiti dal BTB. Ogni entry del BTB ha i 2-bi associati per la predizione di direzione. Strongly taken: 00 Weakly taken: 01 Codifica degli stati: Strongly !taken: 11 Weakly !taken: 10
Predizione Basata su 2-Bit Counter Ogni branch è associate con un contatore a 2-bit Il bit in più fornisce l’isteresi cercata: la predizione non cambia più troppo velocemente. Vantaggi + Miglior accuratezza di predizione Accuratezza per un loop branch con N iterazioni = (N-1)/N TNTNTNTNTNTNTNTNTNTN 50% accuratezza (contatore inizializzato a “weakly taken”) Svantaggi -- Aumenta il costo hardware
E’ sufficiente? ~85-90% accuratezza per molti programmi mediante la predizione con contatore a 2-bit (predizione bimodale) E’ possibile fare ancora meglio….
Limiti della Predizione Dinamica a 2-bit Le predizioni sono effettuate con la storia di un singolo branch C’è influenza dagli altri branch? Sperimentalmente, si trova che un dato branch è influenzato dai branch che sono stati eseguiti di recente (global branch correlation) Ia correlazione del branch con sé stesso non è considerata Il contatore a 2-bit guarda solo ad una piccola parte della storia recente di un branch: «l’ultima volta»! Sperimentalmente, si trova che un dato branch è influenzato dalla sua storia (local branch correlation) In generale, si ottengono predizioni più accurate correlando la predizione di un branch al contesto (correlating predictors) Un branch può avere un pattern di 5 stadi.
Global Branch Prediction a 2 Livelli I Livello: Global branch history register (N bits) Registra la direzione presa dagli ultimi N branch eseguiti II Livello: Tabella di 2-bit counters per ogni possibile pattern della storia globale Indica la direzione presa dal branch l’ultima volta che la stessa storia è stata vista Pattern History Table (PHT) -recente +recente 00 …. 00 1 1 ….. 1 0 00 …. 01 2 3 GHR (global history register) 00 …. 10 index A me sembra ci sia un PHT per ogni branch. E’ roba in più memorizzata nel BTB. Secondo me, ogni entry del BTB ha tutta la PHT Oppure si deve assumere che si sia solo un PHT e che tutti i branch debbano avere lo stesso comportamento data una certa storia globale. 1 Yeh and Patt, “Two-Level Adaptive Training Branch Prediction,” MICRO 1991. 11 …. 11 Quando il branch viene risolto, si aggiorna sia il GHR sia l’entry nella PHT
Two-Level Global History Predictor Direzione dei precedenti Branch (globali) Tabella di 2-bit counters taken? Global branch history PC + inst size Next Fetch Address Program Counter hit? Indirizzo dell’istruzione corrente target address BTB: Branch Target Buffer
Miglioramento dei Predittori Globali Idea: non è vero che tutti i branch, quando vedono la stessa storia globale, si comportano allo stesso modo. Soluzione: bisogna considerare anche il PC, non solo il GHR, per la predizione. Gshare Predictor: GHR “hashed” (combinato) con il PC del Branch + Più informazioni di contesto + Miglior utilizzo della PHT -- Aumenta la latenza di predizione McFarling, “Combining Branch Predictors,” DEC WRL Tech Report, 1993. Direction Prediction PC GHR
Two-Level Gshare Predictor Direzione dei precedenti Branch (globali) Tabella di 2-bit counters taken? Global branch history PC + inst size Next Fetch Address XOR Program Counter hit? Indirizzo dell’istruzione corrente target address BTB: Branch Target Buffer
Local Branch Prediction a 2 Livelli I Livello: Un insieme di Local History Registers (ciascuno ad N bit) che riportano la storia di ogni branch. La selezione del registro giusto avviene sulla base del PC del branch II Livello: Tabella di 2-bit counters per ogni configurazione del Local History Register Indica la direzione del branch l’ultima volta che esso ha avuto una certa storia Pattern History Table (PHT) 00 …. 00 PC 1 1 ….. 1 0 00 …. 01 2 3 00 …. 10 index 1 Local history registers 11 …. 11 Yeh and Patt, “Two-Level Adaptive Training Branch Prediction,” MICRO 1991.
Two-Level Local History Predictor Direzione presa dalle precedenti esecuzioni di *un dato* branch 2-bit counters taken? PC + inst size Next Fetch Address Program Counter hit? Indirizzo dell’istruzione corrente target address BTB: Branch Target Buffer
Hybrid Branch Predictors Idea: Utilizzare più di un tipo di predittore (come fossero algoritmi di predizione diversi) e selezionare la miglior predizione a tempo di esecuzione Es., predittore ibrido con 2-bit counters (1 livello), local predictor (2 livelli) e global predictor (2 livelli) Vantaggi: + Miglior accuratezza: diversi predittori sono migliori per branch diversi + Riduzione del tempo di warmup (predittori con warmup veloce sono usati fintantochè quelli più lenti non hanno raggiunto il warmup) Svantaggi: -- Necessitano di metodologie di scelta tra i predittori (selettori) -- Aumenta la latenza di predizione Riferimento: McFarling, “Combining Branch Predictors,” DEC WRL Tech Report, 1993. Last time predictor gli basta l’ultima volta; warmup veloce Predittori a 2 livelli hanno bisogno di incontrare un branch diverse volte
Alpha 21264 “Tournament Predictor” Branch penalty tipica: 11+ cicli Soluzione ibrida: predittore globale + predittore locale Predittore Locale a 2 livelli con contatori a 3 bit (maggiore isteresi) Predittore Globale a 2 livelli con contatore a 2 bit La scelta avviene sulla base della storia globale: “l’ultima volta che ho visto questa storia globale, il predittore più accurate è risultato quello locale/globale” Riferimento: Kessler, “The Alpha 21264 Microprocessor,” IEEE Micro 1999. PHT PHT Questo predittore ci metto tanto a fare il warmup. Allora ci potrebbe essere un bimodale intanto che non
Idea Finale I processori avanzati hanno diversi predittori dinamici, e di volta in volta devono scegliere quello ritenuto più affidabile per il branch di cui stanno facendo il fetch. Il warmup dei predittori è un fattore importante di questa scelta.