Instruction Level Parallelism statico • • • • • • Concetti di base dell’ILP statico Pipeline scheduling e loop unrolling Multiple issue statico: l’approccio VLIW Le istruzioni predicative L’architettura IA-64: l’Itanium II L’architettura Trimedia TM32 1 ILP statico • Abbiamo fin’ora visto come lo sfruttamento del parallelismo presente fra le istruzioni di un programma possa avvenire durante l’esecuzione del programma stesso. • Scheduling dinamico della pipeline, register renaming, branch prediction, speculazione hardware, multiple issue (dinamico), sono tutte tecniche utilizzate a run-time. • E’ per questo che le tecniche che sfruttano in questo modo il parallelismo insito nelle istruzioni di un programma vengono raccolte sotto il nome di ILP dinamico 2 ILP statico • Ma un aiuto allo sfruttamento dell’ILP può venire anche dal compilatore, il quale può riordinare le istruzioni macchina che genera in modo da migliorare lo sfruttamento della pipeline e delle unità funzionali della CPU quando le istruzioni verranno eseguite. • Naturalmente, nel manipolare le istruzioni macchina generate, il compilatore deve lasciare inalterato il funzionamento del programma scritto dal programmatore • Proprio perché queste tecniche cercano di evidenziare il parallelismo presente nelle istruzioni di un programma prima che questo vada in esecuzione, tali tecniche vengono raggruppate sotto il nome di ILP statico 3 1 ILP statico • L’ILP statico è particolarmente importante nei processori embedded, che devono costare e consumare poco, e non si possono permettere l’enorme numero di transistors neccessari per implementare le tecniche di ILP dinamico. • Ma, come vedremo, l’ILP statico è anche alla base dell’ISA (architettura + set di istruzioni) IA-64, puramente RISC, sviluppata da Intel e HP a partire dal 2000 e su cui sono basati i processori Itanium e Itanium II • Di fatto, il confine tra ILP statico e dinamico non è netto, e in generale molte architetture moderne usano una qualche combinazione dei due gruppi di tecniche. 4 ILP statico: idea di base • Idea di base dell’ILP statico: tenere il più possibile occupata la pipeline (quindi evitare gli stall) cercando, nella fase di compilazione del programma, (sequenze di) istruzioni fra loro indipendenti che possano sovrapporsi nella pipeline. • Per evitare gli stall, una istruzione B che dipende da una istruzione A deve essere separata da A di una “distanza” in cicli di clock pari almeno al numero di cicli di clock necessari ad A per produrre il risultato che dovrà essere usato da B. • Per massimizzare la produttività della pipeline, il compilatore cerca di inserire tra A e B altre istruzioni, utilizzando lo stesso criterio adottato per A e B. 5 ILP statico: idea di base • E’ evidente che la capacità di un compilatore di compiere questa forma di scheduling (ossia di riordino) delle istruzioni dipende: – dalla quantità di ILP presente nel programma in compilazione – dalla lunghezza della pipeline, dal numero e tipo di unità funzionali disponibili, dai tempi di esecuzione delle varie istruzioni – dal fatto che il compilatore conosca queste informazioni sulla CPU per cui sta generando il codice. • due CPU con identico livello ISA ma diversa microarchitetture (ossia un diverso datapath, un diverso set di unità funzionali, un diverso numero di stage della pipeline), possono fornire prestazioni completamente diverse su un programma generato con un compilatore che sfrutta l’ILP statico, e che opera tenendo presente una ben precisa microarchitettura. Notate una differenza con l’ILP dinamico? 6 2 Tecniche di base di Compilazione • Consideriamo un compilatore che genera codice per una CPU con pipeline a 5 stage e le seguenti caratteristiche: – Le varie UF sono a loro volta pipelined, e una qualsiasi istruzione può essere lanciata ad ogni ciclo di clock – I branch vengono eseguiti in due cicli di clock – Prima che una istruzione B possa usare il risultato prodotto da una istruzione A, occorre attendere i seguenti cicli di clock (H-P3, Fig. 4.1): (A) instruction producing result (B) instruction using result latency (clock cycles) FP ALU op another FP ALU op 3 FP ALU op Store double 2 double ALU op branch 1 Load double FP ALU op 1 7 Tecniche di base: pipeline scheduling • consideriamo questo for, che somma uno scalare ad un vettore di 1000 elementi: for ( i = 1000; i > 0; i = i-1 ) x[i] = x[i] + s; • e la sua traduzione in codice MIPS (assumiamo che il valore iniziale di R1, ed R2, siano stati calcolati in precedenza): LOOP: LD F0, 0 (R1) // F0 = array element FADD F4, F0, F2 // scalar is in F2 SD // store result F4, 0 (R1) DADD R1, R1, #-8 // pointer to array is in R1 (DW) BNE // suppose R2 precomputed R1, R2, LOOP 8 Tecniche di base: pipeline scheduling • ecco una “esecuzione” del loop senza nessuna forma di scheduling: LOOP: LD stall FADD stall stall SD DADD stall BNE stall F0, 0 (R1) F4, F0, F2 F4, 0 (R1) R1, R1, #-8 R1, R2, LOOP clock cycle issued 1 2 3 4 5 6 7 8 9 No branch prediction 10 • Ossia, i tre RAW hazard costringono a vari stall della pipeline, e sono 9 necessari 10 cicli di clock per eseguire un ciclo del for 3 Tecniche di base: pipeline scheduling • ma ecco come un compilatore “intelligente”, conoscendo i valori della tabella precedente, e sapendo di poter sfruttare la tecnica del delayed branch, può applicare una forma di pipeline scheduling (statico): ossia riordinare le istruzioni in modo da diminuire il numero di stall: LOOP: LD F0, 0 (R1) DADD R1, R1, #-8 FADD F4, F0, F2 stall BNE R1, R2, LOOP SD F4, 8 (R1) clock cycle issued 1 2 // one stall “filled” 3 4 5 // delayed branch 6 // altered & interchanged with DADD 10 Tecniche di base: pipeline scheduling LOOP: • Quali modifiche sono state fatte? 1 2 3 4 5 6 LD DADD FADD stall BNE SD F0, 0 (R1) R1, R1, #-8 F4, F0, F2 R1, R2, LOOP F4, 8 (R1) – con la DADD spostata in seconda posizione, si sono eliminati gli stall (e quindi i ritardi) tra LD e FADD e tra DADD e BNE – spostando la SD dopo la BNE si elimina lo stall di un ciclo dovuto alla BNE e si riduce di uno lo stall tra la FADD e la SD – siccome SD e DADD sono stati scambiati, l’offset nella SD deve ora essere 8 anziché 0 – Notate che questa forma di scheduling è possibile solo se il compilatore conosce la latenza in cicli di clock di ogni istruzione che genera (ossia se conosce i dettagli interni dell’architettura per cui genera codice) 11 Tecniche di base: loop unrolling • Abbiamo ottenuto un miglioramento notevole, ma possiamo notare che dei 6 cicli di clock necessari per eseguire una iterazione del for: – 3 sono usati per operare sugli elementi dell’array (LD,ADD,SD) – 3 sono semplice loop overhead (DADD, BNE, stall) • Un approccio alternativo al pipeline scheduling è aumentare il numero di istruzioni che effettivamente operano sull’array, rispetto a quelle che servono solo a gestire il ciclo. Come? • Possiamo raggruppare le istruzioni di iterazioni consecutive che operano sull’array all’interno di un unico macrociclo, gestito da un unico controllo di terminazione (un’unica BNE). Operiamo cioé sul ciclo una forma di loop unrolling (srotolamento del loop) statico. 12 4 Tecniche di base: loop unrolling ad esempio, se il numero di iterazioni nel loop è divisibile per 4, il loop originale può essere “srotolato” 4 volte ottenendo: LD FADD SD LD FADD SD LD FADD SD LD FADD SD DADD BNE F0, 0 (R1) F4, F0, F2 F4, 0 (R1) F6, -8 (R1) F8, F6, F2 F8, -8 (R1) F10, -16 (R1) F12, F10, F2 F12, -16 (R1) F14, -24 (R1) F16, F14, F2 F16, -24 (R1) R1, R1, #-32 R1, R2, LOOP // drop DADD & BNE // drop DADD & BNE // drop DADD & BNE 13 Tecniche di base: loop unrolling • Ignoriamo per un momento il fatto che il compilatore abbia anche dovuto gestire una forma di rinominazione dei registri. • Se il codice del lucido precedente viene eseguito così com’è, l’esecuzione richiede 28 cicli di clock, a causa delle latenze tra le istruzioni , che impongono alcuni stall della pipeline. Dalla tabella vista infatti, abbiamo: – 1 ciclo di clock di stall tra ogni LD e la relativa FADD – 2 cicli di clock di stall ogni FADD e la relativa SD – 1 ciclodi clock di stall tra la DADD e la BNE • come risultato, ora ogni iterazione del for dura in media 7 cicli di clock: meglio della situazione iniziale, ma peggio della tecnica di 14 pipeline scheduling appena vista. Tecniche di base: loop unrolling & pipeline scheduling • l’eliminazione di alcuni branch attraverso il loop unrolling permette di considerare parte di un unico macrociclo istruzioni che prima appartenevano a iterazioni successive del for. • Inoltre, la rinominazione dei registri ha generato, all’interno del macrociclo, un maggior numero di operazioni fra loro indipendenti, che possono essere riordinate in modo vantaggioso. • In altre parole, possiamo cercare di combinare il loop unrolling e il pipeline scheduling per ottimizzare ulteriormente il codice. 15 5 loop unrolling & pipeline scheduling LD LD LD LD FADD FADD FADD FADD SD SD DADD SD BNE SD F0, 0 (R1) F6, -8 (R1) F10, -16 (R1) F14, -24 (R1) F4, F0, F2 F8, F6, F2 F12, F10, F2 F16, F14, F2 F4, 0 (R1) F8, -8 (R1) R1, R1, #-32 F12, 16 (R1) R1, R2, LOOP F16, 8 (R1) ecco come ora le istruzioni srotolate (loop unrolling) possono essere riordinate (pipeline scheduling) per eliminare tutti gli stall presenti lo spostamento di queste istruzioni serve per eliminare gli ultimi due stall // 8 – 32 = -24 (?) 16 Tecniche di base: loop unrolling & pipeline scheduling • Con un po’ di pazienza, si può verificare che questo codice è equivalente all’esecuzione di quattro iterazioni del for iniziale. • Ma ora, 4 iterazioni successive del for iniziale vengono eseguite in 14 cicli di clock, ossia 3,5 cicli di clock per iterazione, contro i 10 della versione iniziale 17 Tecniche di base di compilazione • Un compilatore dotato di sofisticate tecniche di analisi è in grado di produrre codice ottimizzato come abbiamo visto nell’esempio precedente, usando le informazioni sulla microarchitettura per cui il codice deve essere generato. • Ciò si paga evidentemente con un maggior tempo necessario per generare codice così ottimizzato. • Ma si può pagare anche in termini di prestazioni del codice in esecuzione, se viene fatto girare su macchine con lo stesso ISA ma microarchitetture diverse. Ad esempio, tempi diversi per la pipeline. • Secondo voi, su altre architetture (ma stesso ISA), il codice ottimizzato potrebbe addirittura non funzionare correttamente? 18 6 Tecniche di base di compilazione • In più vi sono vari problemi che abbiamo volutamente ignorato, ma che il compilatore deve considerare: – quanti nomi di registri diversi si possono usare nel loop unrolling? (ossia quanti ne sono disponibili a livello ISA?) Se si esauriscono tutti i registri, può essere necessario usare la RAM come deposito temporaneo, il che è molto inefficiente. – Come si possono gestire i loop quando non si sa a priori quante volte saranno eseguiti (ad esempio, un while-do o un repeat-until)? – Il loop-unrolling genera codice più lungo di quello originale. Quindi aumenta la probabilità di cache miss nella Instruction Memory, eliminando parte dei vantaggi dell’unrolling. 19 Multiple issue statico • Tralasciando questi e altri problemi, i vantaggi che ci possono essere da uno sfruttamento dell’ILP a livello di compilazione sono ancora maggiori nel caso di processori multiple issue. • Consideriamo di nuovo la versione MIPS superscalare che avevamo usato in un esempio del capitolo precedente, che era in grado di lanciare due istruzioni per ciclo di clock: – una operazione di load / store / branch / integer ALU e – una qualsiasi operazione FP • Supponiamo che l’unrolling dell’esempio precedente sia stato fatto per 5 cicli, e assumiamo le stesse latenze tra le istruzioni riportate nella tabella del lucido 7. 20 Multiple issue statico • Ecco il risultato dell’esecuzione: sono necessari 12 cicli di clock, con 2,5 cicli di clock per ciclo del for, rispetto ai 3,5 dello stesso processore senza multiple issue (H-P3, Fig. 4.2): loop: int. instruction FP instruction LD F0, 0 (R1) clock cycle 1 LD F6, -8 (R1) 2 LD F10, -16 (R1) FADD F4, F0, F2 3 LD F14, -24 (R1) FADD F8, F6, F2 4 LD F18, -32 (R1) FADD F12, F10, F2 5 SD F4, 0 (R1) FADD F16, F14, F2 6 SD F8, -8 (R1) FADD F20, F18, F2 7 SD F12, -16 (R1) 8 DADD R1, R1, #-40 9 SD F16, 16 (R1) 10 BNE R1, R2, Loop 11 SD F20, 8 (R1) 12 21 7 Multiple issue statico: l’approccio VLIW • Nei processori superscalari, che implementano l’ILP dinamico, il numero di istruzioni da avviare per ciclo di clock viene deciso a run time. Questi processori usano compilatori meno sofisticati, ma hanno bisogno di più hardware, per minimizzare le alee e massimizzare il numero di istruzioni che avviano all’esecuzione. • I processori con multiple issue statico invece, avviano un numero prefissato di instruzioni da eseguire, ed è responsabilità del compilatore presentare le istruzioni alla CPU in un ordine adatto a massimizzare le prestazioni, usando le tecniche appena viste (e altre più sofisticate) • Di fatto, il compilatore genera una serie di pacchetti contententi un numero prefissato di istruzioni ciascuno (anche se, eventualmente, 22 alcune istruzioni posso essere delle no-op). Multiple issue statico: l’approccio VLIW • Ognuno di questi issue packet, contiene istruzioni fra loro indipendenti, che possono quindi essere eseguite in parallelo nella pipeline. • E’ il compilatore che genera i pacchetti riordinando le istruzioni (come nell’esempio visto), verificando ed eliminando le dipendenze, per cui il lavoro non deve più essere fatto dalla CPU. • La CPU ha quindi una architettura più semplice, e la fase di issue delle istruzioni può avvenire in minor tempo, perché non è più necessario controllare a run-time eventuali dipendenze fra le istruzioni: questo lavoro è già stato fatto da compilatore. 23 Multiple issue statico: l’approccio VLIW • Questo approccio prende il nome di VLIW (Very Long Instruction Word), in quanto le prime architetture che lo adottavano (inzio anni ‘80), usavano in realtà istruzioni macchina molto lunghe (128 bit o più) ognuna delle quali specificava più operazioni fra loro indipendenti che potevano essere eseguite in parallelo dalla CPU. • Il termine EPIC (Explicitly Parallel Instruction Computer) usato dall’Intel nel contesto dell’architettura IA-64 si riferisce allo stesso approccio. 24 8 Multiple issue statico: l’approccio VLIW • Quanto più è alto il numero di istruzioni che un processore può avviare contemporaneamente, tanto più l’approccio VLIW è vantaggioso. • Infatti, l’overhead che un processore superscalare deve sopportare per controllare a run-time le dipendenze fra le istruzioni da avviare in parallelo cresce quadraticamente col numero di istruzioni. (ogni istruzione deve essere confrontata con tutte le altre potenzialmente avviabili) 25 l’approccio VLIW: un esempio • Consideriamo un processore VLIW che possa avviare 5 istruzioni contemporaneamente, con le seguenti unità funzionali: – una unità intera (ALU) in grado di gestire anche i branch – due unità floating point – due unità per i riferimenti in memoria • Naturalmente, per sfruttare a pieno queste unità il codice da eseguire deve manifestare sufficiente parallelismo tra le istruzioni, e il compilatore deve essere in grado di metterlo in evidenza, con le tecniche di loop unrolling e pipeline scheduling viste in precedenza. • Consideriamo l’esempio già visto, assumendo un unrolling di 7 cicli del for. Ecco come potrebbe essere eseguito il codice, suddiviso in pacchetti indipendenti dal compilatore e eseguiti dalla CPU 26 l’approccio VLIW: un esempio H-P3, Fig 4.5: pack et / n. clock memory ref. 1 memory ref. 2 LD F6,-8(R1) FP op. 1 FP op. 2 integer / branch op. 1 LD F0,0(R1) no-op no-op no-op 2 LD F10,-16(R1) LD F14,-24(R1) no-op no-op no-op 3 LD F18,-32(R1) LD F22,-40(R1) FADD F4,F0,F2 FADD F8,F6,F2 no-op 4 LD F26,-48(R1) no-op FADD F12,F10,F2 FADD F16,F14,F2 no-op 5 no-op no-op FADD F20,F18,F2 FADD F24,F22,F2 no-op 6 SD F4,0(R1) SD F8,-8(R1) FADD F28,F26,F2 no-op no-op 7 SD F12,-16(R1) SD F16,-24(R1) no-op no-op DADD R1,R1,#-56 8 SD F20,24(R1) SD F24,16(R1) no-op no-op no-op 9 SD F28,8(R1) no-op no-op no-op BNE R1,R2,Loop 27 9 l’approccio VLIW: un esempio • Notiamo alcune cose: • non tutti gli slot di ogni pacchetto sono occupati, e vengono riempiti da no-op. A seconda del codice e del tipo di architettura, non è sempre possibile sfruttare tutti gli slot di ogni pacchetto. Nel nostro esempio circa il 60% degli slot viene sfruttato. • Ci vogliono 9 cicli di clock per eseguire 7 cicli del for, con una media di 9/7 = 1.29 cicli di clock per ciclo del for. • Come già osservato in precedenza, una macchina con architettura diversa (ad esempio, diverse U.F. o diverse latenze per istruzione), fornirebbe prestazioni diverse eseguendo lo stesso codice. 28 Tecniche più sofisticate • Naturalmente, l’approccio VLIW usa anche tecniche di compilazione più sofisticate di quelle viste per mettere in evidenza in fase di compilazione l’ILP. Tra queste ricordiamo: – Static branch prediction. Assume che i branch abbiano sempre un certo tipo di comportamento, o analizza il codice per cercare di dedurne il comportamento – Loop Level Parallelism. Tecniche per evidenziare il parallelismo in iterazioni successive di un loop, quando i vari cicli non sono indipendenti fra loro – Symbolic loop unrolling. Il loop non viene “srotolato”, ma si cerca di mettere in ogni pacchetto istruzioni fra loro indipendenti, anche appartenenti a cicli diversi – Global code scheduling. Cerca di compattare codice proveniente da diversi basic block (quindi istruzioni separate da varie istruzioni condizionali, inclusi i cicli) in sequenze di istruzioni indipendenti 29 Supporti hardware all’ILP statico: istruzioni predicative • le tecniche usate dal compilatore per mettere in evidenza l’ILP funzionano bene soprattutto quando il comportamento dei branch è facilmente predicibile (come ad esempio nei cicli for). • Quando il comportamento dei branch non è ben chiaro invece, le dipendenze dal controllo limitano fortemente la possibilità di sfruttare a fondo il parallelismo fra istruzioni. • Per limitare questo problema è di estendere l’insieme di istruzioni macchina aggiungendo un insieme di istruzioni predicative o condizionali. 30 10 Supporti hardware all’ILP statico: istruzioni predicative • Queste istruzioni possono essere usate per eliminare alcuni branch, convertendo una dipendenza dal controllo in una dipendenza dai dati, e aumentando così potenzialmente le prestazioni del processore. • Di fatto le istruzioni predicative sono utili anche per le tecniche di ILP dinamico, perché in ogni caso permettono di eliminare alcuni branch. 31 istruzioni predicative o condizionali • Nota: nel seguito useremo indifferentemente le espressioni istruzione predicativa e istruzione condizionale. • Una istruzione predicativa è una istruzione che contiene una condizione che viene valutata come parte dell’esecuzione dell’istruzione stessa. – se la condizione è vera il resto dell’istruzione viene eseguito normalmente. – se la condizione è falsa l’istruzione viene trasformata in una no-op. • Molte architetture recenti contengono una qualche forma di istruzione condizionale. 32 istruzioni predicative o condizionali • l’esempio più semplice di istruzione predicativa è la move condizionale: sposta il contenuto di un registro in un’altro se la condizione associata è vera. Una tale istruzione si può usare per eliminare i branch in alcuni casi. • Consideriamo l’istruzione C “if (A == 0) S = T;” e assumiamo che R1, R2, R3 contengano rispettivamente i valori di A, S, e T. Il codice può essere convertito nelle seguenti istruzioni MIPS: BNEZ R1, Jump // branch if not zero ADD R2, R3, R0 // R0 always contains 0 Jump: 33 11 istruzioni predicative o condizionali • Usando invece una move condizionale, che viene completata solo se il terzo operando è uguale a zero, l’if potrebbe essere tradotto nella seguente istruzione: CMOVZ R2, R3, R1 // mov R3 in R2 if R1 == 0 • Notate che la dipendenza sul controllo certa (la ADD dipende dalla BNEZ) viene così trasformata in una dipendenza sui dati solo eventuale, dipendentemente dalle istruzioni che precedono la CMOVZ. • Eliminare questi tipi di branch può avere un effetto notevole sulle prestazioni delle pipeline reali, in cui l’esecuzione del branch richiede molti cicli di clock. Infatti, tali branch non sono facilmente predicibili e il più delle volte non lo sono affatto: una qualsiasi forma di 34 predizione rischia di sbagliare nel 50% dei casi! istruzioni predicative o condizionali • Nei processori multiple issue poi, è possibile che più branch possano dover essere eseguiti in un unico ciclo di clock, ma la predizione dei branch diviene ancora più aletoria, perché i branch possono dipendere l’uno dall’altro. if (A == B) {A = A+1} else if (C == D) {C = C+1} in questo esempio, il secondo if dipende dal primo, ed è possibile che tutte le istruzioni vengano avviate nello stesso ciclo di clock. Come si fa a sapere se eseguire o no il secondo if, mentre si sta valutando ancora il primo? • per risolvere il problema, molte architetture non permettono l’avvio di più branch in un unico ciclo di clock. • Le move condizionali permettono di eliminare alcuni branch, aumentando così l’issue rate della CPU. 35 istruzioni predicative o condizionali • Ad esempio, ecco come del codice controllato da un if-then-else (a) può essere trasformato in codice macchina con due salti (b) o in quattro istruzioni predicative (c) (Tanenbaum, Fig. 5.52): 36 12 istruzioni predicative o condizionali • Le semplici move condizionali non permettono però di eliminare i branch che controllano blocchi di istruzioni diverse dalle move. • Per questo, alcune architetture supportano la full predication: tutte le istruzioni possono essere controllate da un predicato. • per funzionare correttamente, queste architetture hanno bisogno di opportuni registri predicativi (ciascuno formato da un solo bit), che memorizzano il risultato di test effettuati da istruzioni precedenti, in modo che il valore possa essere usato per controllare l’esecuzione di istruzioni successive (che quindi divengono predicative). 37 istruzioni predicative o condizionali • Ad esempio, nell’Itanium, sono disponibili dei registri predicativi usati a coppie (P1, P2, ...) e delle istruzioni per settare tali registri. • Ad esempio, “CMPEQ R1, R2, P4” setta P4 = 1 e P5 = 0 se R1 == R2. Altrimenti, l’istruzione setta P4 = 0 e P5 = 1. • I registri predicativi sono poi usati per controllare l’esecuzione di altre istruzioni. Ecco un esempio (Tanenbaum, Fig. 5.53): 38 istruzioni predicative o condizionali • In generale, le istruzioni predicative sono particolarmente utili per implementare piccoli if-then-else eliminando branch difficilmente predicibili e quindi per semplificare le tecniche di ILP statico più sofisticate. • L’utilità della predicazione è comunque limitata da alcuni fattori: – le istruzioni predicative poi annullate hanno comunque utilizzato risorse della CPU, limitando l’esecuzione di altre istruzioni. – Combinazioni complesse di branch non sono facilmente convertibili in istruzioni condizionali – le istruzioni condizionali possono essere più lente delle corrispondenti non condizionali 39 13 istruzioni predicative o condizionali • Per queste ragioni, molte architetture usano solo alcune semplici istruzioni condizionali, e in particolare la move condizionale. • Le architetture delle macchine MIPS, SPARC, Alpha, PowerPC, Pentium, dual core e successivi, supportano la move condizionale. • l’architettura IA-64 supporta la full predication. 40 ILP statico vs ILP dinamico • Inizialmente molto differenti fra loro, i due approcci all’ILP si sono fra loro avvicinati con lo sviluppo della tecnologia software e hardware. • In ogni caso, gli approcci dinamici all’ILP sembrano più adatti ai processori general purpose, dove ci si può permettere un hardware più complesso, e quindi costi e consumi maggiori. • Al contrario, l’ILP statico rimane fondamentale nei processori per applicazioni embedded, dove è più importante che il processore consumi poco, e dove i programmi da usare sono spesso limitati e possono essere sviluppati in modo da poter sfruttare al massimo il parallelismo tra istruzioni usando appunto le tecniche dell’ILP statico. 41 L’architettura IA-64: l’Itanium II • “Intel is rapidly getting to the point where it has just squeezed every last drop of juice out of the IA-32 ISA and the Pentium 4 line of processors” A. Tanenbaum, Structured Computer Architecture, 5th ed. • Alla fine degli anni ‘90, in previsione di raggiungere quel punto l’Intel si è mossa in due direzioni: 1 Sviluppo dell’ISA e della microarchitettura Intel 64 2 dal 2000 insieme all’Hp, sviluppo di un ISA e di una microarchitettura completamente nuova, nota come IA-64 42 14 L’architettura IA-64: l’Itanium II • L’Itanium e l’Itanium II sono i primi processori Intel completamente RISC, e sono basati sull’ISA noto come IA-64. • L’ISA IA-64 non è compatibile con la vecchia IA-32 e con la nuova Intel 64. • Intel chiama EPIC la tecnologia usata per sfruttare l’ILP combinando le caratteristiche dell’architettura hardware con la compilazione dei programmi tipica dell’IA-64. 43 L’architettura IA-64: l’Itanium II • L’archiettura IA-64, nell’implementazione dell’Itanium II ha le seguenti caratteristiche: – 128 registri general-purpose a 64 bit lunghi ciascuno 65 bit (il bit in più – poison bit – viene usato per gestire le eccezioni) – 128 registri floating point a 82 bit (è lo standard IEEE per il FP) – 64 registri predicativi da 1 bit ciascuno – 128 registri “speciali” usati per vari scopi (passaggio dei parametri per le system call del SO, memory mapping, controllo del sistema, registrazione delle performance della CPU, ecc.) 44 L’architettura IA-64: l’Itanium II • dei 128 registri general purpose, i primi 32 sono sempre disponibili, gli altri 32-128 sono usati come stack di registri : ad ogni procedura chiamata viene assegnato un sottoinsieme di questi stack register. • L’Itanium II ha 3 livelli di cache, e viene prodotto in diverse configurazioni multi-core, con le seguenti caratteristiche: – CPU Tukwilla (2010): tecnologia a 65 nm, clock, fino a 1.73GHz, cache L3 fino a 24MB, fino a 4 core, Hyper-threading, circa 2 miliardi di transistors – CPU Poulson (dal 2012): tecnologia a 32 nm, cache L3 fino a 32 MB, fino a 8 core, Hyper-threading, circa 3 miliardi di transistors (fonte: AnandTech) 45 15 L’architettura IA-64: l’Itanium II • L’architettura IA-64 prevede 5 tipi di unità funzionali per l’esecuzione delle istruzioni (H-P3, Fig. 4.11): Exec. unit Instr. description type I-unit M-unit F-unit B-unit L+X example instr. A Integer ALU add, sub, and, or, compare I non-ALU integer int./ multimedia shift, bit test, moves A Integer ALU add, sub, and, or, compare M Memory access load / store for integer and FP registers F Floating Point floating point Intructions B Branches cond. branches, call, loop branches L+X Extended extended immediate, stop, no-ops 46 L’architettura IA-64: l’Itanium II • L’architettura IA-64 sfrutta l’approccio VLIW aggiungendo però una certa flessibilità nella formattazione delle istruzioni negli issuepackets. • Il compilatore di una architettura IA-64 inserisce le istruzioni in instruction groups: una sequenza di istruzioni indipendenti fra loro che: – non confliggono nell’uso delle unità funzionali – non confliggono nell’uso dei registri – non hanno dipendenze di tipo RAW e WAW – hanno un numero limitato di dipendenze di tipo WAR 47 L’architettura IA-64: l’Itanium II • La CPU è libera di schedulare le istruzioni all’interno di un group come preferisce, e quindi, per quanto possibile, di eseguirle in parallelo. • Instruction group consecutivi vengono invece eseguiti in maniera sequenziale, sebbene la CPU possa decidere di inziare l’esecuzione di un gruppo prima che tutte le istruzioni del precedente siano terminate, se la cosa non crea conflitti. • Ogni gruppo contiene un numero arbitrario di istruzioni, ma il compilatore deve indicare esplicitamente il limite fra un gruppo e il successivo, usando una istruzione speciale chiamata stop. 48 16 L’architettura IA-64: l’Itanium II • Le istruzioni dell-IA-64 Itanium II sono organizzate in bundle lunghi 128 bit. Ogni bundle contiene 3 istruzioni da 41 bit e un campo template di 5 bit (Tanenbaum, Fig. 5.50). 49 L’architettura IA-64: l’Itanium II • • I 5 bit del template di un bundle servono per specificare: – quali unità saranno usate dalle istruzioni contenute nel bundle – l’eventuale presenza di uno stop in un qualche punto del bundle Ad esempio, se il template di un bundle vale 10, indica che il bundle contiene: M – stop – M – I . Ossia: – la prima istruzione usa un’unità M – finisce un instruction group e ne incomincia un altro (notate quindi che un bundle può stare a cavallo di due group) – La seconda istruzione usa di nuovo una unità M – la terza istruzione usa un’unità I 50 L’architettura IA-64: l’Itanium II 51 17 L’architettura IA-64: l’Itanium II • Nell’architettura IA-64 sono definiti più di 100 tipi di istruzioni. Ognuna è lunga 41 bit, così suddivisi: • Operation group (4 bit): specifica il tipo di istruzione • Operation type (10 bit): specifica l’operazione richiesta • Registers (3 x 7 bit): specifica i 3 registri usati • Predicate register (6 bit) specifica il predicate register a cui è associata l’istruzione per gestire la predicazione • I registri predicativi vengono settati usando istruzioni di comparazione. Ad esempio, CMPEQ R1,R2,P4 setta P4 a true e P5 a false se R1=R2 (e viceversa se R1 <> R2) 52 L’architettura IA-64: l’Itanium II • Oltre alla full predication, l’Itanium II supporta una forma limitata di speculazione attraverso le load speculative: le advanced load. • Come noto, una load può produrre un lungo stall della pipeline se il dato indirizzano non è presente in cache. Le istruzioni che usano quel dato devono attendere il suo arrivo prima di poter passare alla fase EX. • Il compilatore è allora libero di spostare “indietro” la load, in modo che venga eseguita prima di quando il dato prelevato dalla load stessa dovrà essere usato da un’altra istruzione. • L’idea è di dare alla load il tempo necessario a prelevare il dato – anche se questo si trova in RAM – di modo che questo sia sicuramente presente nel registro di destinazione della load quando 53 dovrà essere usato da altre istruzioni. L’architettura IA-64: l’Itanium II • Supponiamo dunque che la load venga spostata indietro di un certo numero di istruzioni. Tra queste istruzioni “saltate” potrebbe trovarsi una store, che nella versione originale del programma sarebbe stata eseguita prima della load. Tuttavia, dopo lo spostamento operato dal compilatore questa store verrà eseguita dopo la load. E se vi fosse una dipendenza tra le due istruzioni? • Una advanced load (LDA) è una load che è stata speculativamente spostata prima di una store da cui potrebbe potenzialmente dipendere. • A run time, quando viene eseguita una LDA viene creata una nuova entry in una tavola speciale detta ALAT. La entry creata contiene: • il numero del registro di destinazione della load • l’indirizzo della locazione di memoria usata dalla load (che, 54 appunto, in generale è noto solo a run time) 18 L’architettura IA-64: l’Itanium II • Quando viene eseguita una STORE viene fatto anche un controllo associativo (tutte le entry vengono controllate in parallelo) sull’ALAT. • Se una entry ha lo stesso indirizzo di memoria usato dalla STORE, quella entry è marcata come invalida. • Prima che una qualsiasi istruzione usi il registro caricato da una LDA, l’entry della ALAT contenente quel registro viene controllata. • Se la entry è valida, la speculazione sulla load ha avuto successo, e l’entry dell’ALAT viene azzerato. • Se la entry non è valida, la LOAD viene rieseguita. 55 L’architettura IA-64: l’Itanium II • Come esempio, consideriamo la seguente situazione, in cui il compilatore decide di spostare indietro la LD, di modo che quando la MUL verrà eseguita, il contenuto della cella di RAM di indirizzo 0 (R1) sia già disponibile in R2. • Nel caso peggiore, 0 (R1) dovrà essere letto dalla RAM: il compilatore sa che sono necessari N cicli di clock per una lettura in RAM, e sposta all’indietro la LD di tante istruzioni quante ne sono necessarie a far passare Almeno N cicli di clock • Supponiamo che, in questo spostamento, la LD venga spostata prima di una SD ... ADD MUL ADD SD ... ... LD MUL ... R6, R20, R30 R11, R6, R4 R10, R15, R25 R11, 8 (R10) ... ... R2, 0 (R1) R3, R2, R4 56 L’architettura IA-64: l’Itanium II • Il compilatore non può sapere se vi è una relazione tra la SD e la LD: le due istruzioni sono legate se fanno riferimento alla stessa cella di RAM, ossia se 8 (R10) = 0 (R1) ma, in generale, questo si può sapere solo a run time. • se 8 (R10) = 0 (R1), questo vuol dire che, nel programma, la MUL deve usare per uno dei suoi argomenti il valore calcolato dalla “MUL R11, R6, R4”, immagazzinato temporaneamente all’indirizzo 8 (R10). • Ma con lo spostamento della LD, la MUL userebbe invece il valore contenuto in 0 (R1) prima che in quella cella sia stato scritto il risultato della “MUL R11, R6, R4” ... ADD MUL ADD SD ... ... LD MUL ... R6, R20, R30 R11, R6, R4 R10, R15, R25 R11, 8 (R10) ... ... R2, 0 (R1) R3, R2, R4 57 19 L’architettura IA-64: l’Itanium II • Il compilatore allora, esegue lo spostamento della LD, ma la trasforma in una advanced load (LDA). • Quando la LDA viene eseguita, nella ALAT vengono scritti il nome del registro, R2, e il valore dell’indirizzo della load, 0 (R1). • Quando viene eseguita la SD, viene confrontato l’indirizzo che usa con tutti gli indirizzi nella ALAT. Se c’è un match, l’entry della ALAT viene invalidata. • All’issue della MUL, poiché R2 occorre nella ALAT, se l’entry di R2 è invalida la “LD R2, 0 (R1)” viene rieseguita prima di eseguire la MUL. In ogni caso, l’entry della ALAT viene cancellata. ... ADD MUL LDA ADD SD ... ... MUL ... R6, R20, R30 R11, R6, R4 R2, 0 (R1) R10, R15, R25 R11, 8 (R10) ... ... R3, R2, R4 ALAT R2 0 (R1) i/v 58 L’architettura IA-64: l’Itanium II • L’Itanium II ha una pipeline di 8 stage (contro le 10 dell’Itanium) e 11 unità funzionali pipelined che gli permettono di avere contemporaneamente in esecuzione fino a 13 istruzioni. • Può avviare fino a 6 istruzioni per ciclo di clock, che diventeranno 12 nella versione Poulson. • Implementa una esecuzione in-order delle istruzioni, ma implementa alcune caratteristiche tipiche dell’ILP dinamico: • branch prediction • speculazione 59 L’architettura IA-64: l’Itanium II • Itanium è certamente un progetto controverso, già nel 2005 si parlava del suo abbandono, a causa dei costi elevati del processore, e della sua diffusione limitata solo per applicazioni di fascia alta. • Le prestazioni dell’Itanium poi sono sempre più avvicinate dai sistemi multi-core basati su ISA Intel 64, e che costano meno potendo contare su una economia di scala più marcata. 60 20 L’architettura Trimedia TM32 • Abbiamo detto all’inizio di questo capitolo che l’ILP statico è particolarmente adatto per le applicazioni embedded, dove non ci si può permettere un hardware troppo complesso / costoso / ad elevato consumo di energia • Un classico esempio di processore VLIW è il TriMedia TM32, progettato dalla Philips e usato per applicazioni multimediali in lettori e registatori CD, DVD, MP3 players, videocamere digitali, televisori. • E’ quindi un processore dedicato per applicazioni molto specifiche, al contrario di un processore come l’Itanium II. 61 L’architettura Trimedia TM32 • Il TM32 ha un clock compreso tra 266 e 300 MHZ, e esegue istruzioni VLIW ciascuna delle quali può specificare fino a 5 operazioni distinte che possono essere avviate ad ogni colpo di clock. Alcuni degli slot possono contenere delle no-op (Tanenbaum, Fig. 8.3). 62 L’architettura Trimedia TM32 • il TM3260 possiede 10 tipi di UF pipelined, più l’unità FP sqrt/div. non pipelined (Tanenbaum, Fig. 8.4): 63 21 L’architettura Trimedia TM32 • Il TM3260 ha 128 registri general-purpose a 32 bit, + 4 registri speciali (PC, PSW, 2 registri per gestire gli interrupt) • Supporta la predicazione di tutte le istruzioni, associandole ad un registro che, se vale 0 ne annulla l’esecuzione. • Non esegue nessun controllo a run-time per verificare se le operazioni inserite in una istruzione, e le operazioni di istruzioni eseguite consecutivamente sono compatibili: semplicemente le esegue. • E’ infatti responsabilità del compilatore schedulare le operazioni in ogni istruzione, e le diverse istruzioni in modo da limitare al massimo gli stall delle pipeline e massimizzare l’uso delle diverse U.F. 64 L’architettura Trimedia TM32 • In questo modo, si può limitare la complessità dell’hardware, e usare compilatori per generare codice estremamente ottimizzato per quell’hardware (del resto, il compilatore ha tutto il tempo che vuole...) • l’unico vero difetto di questo approccio è che il codice generato occupa comunque molto più spazio di un codice equivalente per un generico processore RISC • Per questa ragione, il codice che deve girare su questi processori (tipicamente memorizzato in ROM) è compresso: solo quando vengono prelevate dalla cache per essere eseguite le istruzioni vengono decompresse. • Anche così, il codice compresso occupa circa il doppio dello spazio che occuperebbe un codice identico per un generico processore RISC 65 (prossimo lucido: H-P3, Fig. 3.23) approcci usati in CPU multiple-issue name issue structure hazard detection scheduling chracteristic examples Superscalar (static) dynamic hardware static in-order execution Sun UltraSPARC II/III Superscalar (dynamic) dynamic hardware dynamic some out-oforder execution IBM Power2 Superscalar (speculative) dynamic hardware dynamic with speculation out-of-order execution with speculation Intel CPUs, MIPS R10K, Alpha 21264, HP PA 8500, IBM RS64III VLIW/LIW static software static no hazards between issue packets Trimedia, i860 EPIC mostly static mostly software mostly static explicit dependencies marked by compiler Itanium, Itanium II 66 22