Sei sulla pagina 1di 7

Torniamo un attimo sul loop unrolling per vedere come gestire una problematica.

Abbiamo un vettore che prevede 100 elementi. Facciamo uno srotolamento di questo vettore per 4
volte e, facendolo per 4 volte, avremo che il loop srotolato richieder 25 iterazioni. Se il vettore
avesse 101 elementi, come faccio a srotolarlo per 4? Questo un problema normale, nel senso
che nessuno ci garantisce che il programma principale ha, un numero di iterazioni, che pu essere
gestito secondo uno srotolamento, che potrebbe anche non esistere. Potrebbe anche capitare di
avere un loop parametrico Nel codice abbiamo un loop che, il compilatore si trova a tradurre, di
un'iterazione che va da 1 a N. Nel compilare un FOR da 1 a N, il compilatore non sa quanto vale N
al momento dellesecuzione, n per quanto bisogna srotolare. Quest operazione abbastanza
semplice da gestire perch, il compilatore, quando trova un loop di N generiche iterazioni e se ha
deciso di srotolarlo per 4, per esempio, crea un codice compilato dove la 1 parte si chiama:
gestione del residuo e poi c' una 2 parte che contiene il nostro loop srotolato. Riassumendo, il
nostro problema originale prevede N iterazioni; lo vogliamo srotolare per 4, ma non sappiamo
quanto vale N. Il compilatore, come 1 cosa, nel tradurre il codice, considera N e calcola il residuo
di N rispetto al fattore di srotolamento 4. N una variabile che pu valere un numero qualsiasi ad
ogni accesso nel programma, ma quando parto per eseguire literazione nota. Esseno nota,
quando inizializzo il valore sul loop, posso fare il resto della divisione di N/4. 4 una potenza del 2
e quindi devo considerare gli ultimi 2^k bit ( se k log in base 2 di 4). Dato N, se prendo gli ultimi 2
bit, abbiamo il resto della divisione N/4.
La 1 cosa che il sistema fa calcolare l'operazione attraverso una maschera. Supponiamo che
R6 contenga N.
ANDI R5 R6 3 // Sto mettendo in R5 il residuo. Devo fare un loop non srotolato di R5 iterazioni, se
quello fosse 103, il residuo verrebbe proprio 3. Facciamo il loop di 3 iterazioni non srotolato e
resteranno 100 che potremo srotolare.
Contemporaneamente faccio la divisione di R6 per 4 per sapere quante volte questo loop
srotolato dovr essere eseguito.
DIVI R4 R6 4 // sto mettendo in R4 N/4
Sappiamo che il vettore su cui dobbiamo operare, parte da un certo indirizzo e si sviluppa da
quell'indirizzo + R6 che contiene N *8 (se abbiamo un vettore di dati da 8 byte).
Calcoliamo R6*8 ( R3 la dimensione del vettore in byte su cui decido di operare per poi
spostarmi su questo vettore sempre di 8):
SLLI R3 R6 3 //se il loop gestisce un unico vettore, avremo che il vettore partir da un certo
indirizzo e si svilupper da quellindirizzo per la dimensione di R3.
Se dobbiamo gestire diversi vettori, per esempio 2 sorgenti e 1 risultato, abbiamo un altro indirizzo
di inizio, ma sempre R3 funger da puntatore ai vettori. In generale va bene qualsiasi numero di
vettori, purch siano omogenei Elementi sono tutti da 8 byte, possiamo usare un solo
puntatore. Non ha senso usare pi puntatori, non tanto perch spreco 2 registri, ma perch per
ogni iterazione dovr decrementare 2 registri. E pi intelligente ragionare con un unico puntatore
che viene gestito. Avremo, quindi, nel corpo del loop unistruzione che andr a puntare ad un certo
vettore a partire da 1000 con R3, unaltra istruzione per prelever un operando da un vettore,
sempre considerando R3, e via dicendo. Sottrarremo solo ad R3, 8 nel caso di loop non srotolato e
8x4 nel caso della gestione del puntatore nella parte srotolata. Questa parte che gestisce il loop
non srotolato conviene gestirla sulla parte terminale del vettore perch, se ho il vettore di 103
elementi, conviene gestire gli ultimi 3 come residuo e gli altri 25 gruppi da 4 come gestione del
loop srotolato. Se il vettore inizia da 1000, il nostro prelievo delloperando, che fa parte di questa
gestione del residuo, sar 1000-8 + R3. Se il vettore inizia da 1000, il 2 1008, ecc. Lultimo

1000-8+R3 ( se ha 1 solo elemento quindi R3=8, 1000-8+8 indirizzo ultimo elemento del vettore.
Quando abbiamo offset indirizzo del 1 elemento, lultimo non 1000 + dimensione, perch se lo
facciamo in questa maniera puntiamo al 1 elemento dopo che il vettore stato completato. Per
lultimo la formula giusta 1000-8 + dimensione)
SLLI R7 R4 5 // dimensione in termini di byte della parte srotolabile del vettore
Il loop per la gestione del residuo sar un loop di R5 iterazioni, ma potrebbe anche essere un loop
mai eseguito. Aggiungiamo uno scalare al vettore V[ ]F8+ V[1000]
Codice per la gestione del residuo:
L.D F2 992 R3 // Load dellultimo elemento del vettore. Prelevo dalla memoria loggetto per
caricarlo in F2
ADD.D F2 F2 F8
S.D F2 992 R3
A prescindere dalle operazioni, adesso dobbiamo decrementare il puntatore e l'indice di questa
operazione:
ADDI R3 R3-8
ADDI R5 R5-1
BNEZ R5 -6 //Fintanto che R5 diverso da 0 dobbiamo ritornare al loop gestione del residuo.
Questa cosa giusta, ma errata dal punto di vista che literazione eseguita, almeno, una volta. Ci
siamo calcolati questi valori, poi partiamo con il loop appena scritto (che preleva l'ultimo elemento
del vettore) e, fintanto che R5 non viene decrementato e scompare il resto, eseguiamo
literazione. La 1 iterazione del programma verr effettuata anche se R5 = 0. Se R5 0 ( ovvero
se N multiplo di 4) il loop appena scritto non finir; (diventa -1. Poi -2 fino allunder flow) e
non ammissibile. Per questa ragione, prima di partire con il loop per la gestione del residuo,
consigliato calcolare leventualit che il loop debba essere eseguito.
Posso quindi fare che BEQZ R5 5, salto dove inizia la parte della gestione del loop srotolato; se
invece R5 non uguale a 0, non salto da nessuna parte e eseguo il loop( possibile caso).
Ragioniamo sul codice gestito con 2 decrementi: R3 che mi deve sempre puntare allelemento da
processare alla prossima iterazione, sia R5 che mi serve per contare. Potremmo utilizzare questa
cosa impostando solo il decremento di R3 e facendo una condizione di uscita del loop su R3. Cio
dato R3, che la dimensione del vettore, possiamo calcolarci un R7 (numero non ancora usato)
che sar la dimensione della parte del vettore multipla di 4. Tipo: il vettore di 1003 elementi, la
dimensione del vettore 8024. La dimensione del vettore multiplo di 4 1000 elementi*8 e 24 la
dimensione del vettore residuo. Posso lavorare solo con R3, che decremento di volta per volta, e
saltare fin tanto che R3 non uguale a R7 Evito la gestione di un altro eventuale decremento
( dove R7 uguale a 8* (N/4) con N/4 numero intero non fp). Posso impostare l ritorno sulla BNE
di R3 con il registro che di puntatore allultimo elemento non del vettore ma di quella parte del
vettore che sarebbe multipla di 4. Per fare questo calcoliamo R7, che possiamo calcolare
prendendo R4 che sarebbe il numero di elementi del vettore multipli di 4, prendere R4 e
moltiplicarlo x4 x8. Posso fare SLLI R7 R4 32 // ho messo in R7 la dimensione in byte della parte
srotolabile del vettore Il numero di elementi del vettore multiplo di 4 ( 17 elementi? Il numero
16).
Avendo in R7 questa dimensione in termini di byte della parte del vettore, fin tanto che R3
decrementando di 8 non diventa uguale a R7, salto indietro. Quindi elimino ADDI R5 R5-1
E imposto BNE R3 R7-6 al posto di BEQZ R5. Alla fine avremo che R3 punta allultimo elemento
del vettore srotolato.

Inizia ora la parte del loop che durer R4 ( dove abbiamo messo la parte intera delliterazione)
iterazioni. Avendo R4 iterazioni inizieremo a scrivere un codice e poi avremo un'istruzione per
saltare all'altra iterazione. Attenzione per che questo loop che scriveremo, potr essere non
eseguito se per esempio volessi srotolarlo per un numero maggiore di volte rispetto gli elementi
che ho. Quindi, prima di scrivere il codice, dobbiamo verificare che effettivamente ci sia da fare
almeno uniterazione. Come? R4 non deve essere uguale a 0. Se avessi avuto N=2, R4 era uguale
a 0. Come abbiamo fatto BEQZ R5, faremo BEQZ R4 per saltare dopo il loop che scriveremo ora.
Ritorniamo a quello che abbiamo scritto prima, la schedulazione fatta non il massimo quindi
posso fare una serie di cambiamenti per ridurre gli stalli che staranno fra la LOAD e ADD, tra ADD
e STORE e tra la ADD e la BNE pi leventualit, dopo BNE di fare unistruzione che verr abortita.
L ADDI R3 R3-8 la posso spostare dopo la L.D, ottenendo 2 scopi: riempire uno stallo e
allontanarla dalla BNE. Devo modificare la STORE perch stata decurtata di 8
S.D F2 1000 R3. Invece per eliminare lo stallo tra la ADD e la BNE, prendo la S.D e la metto dopo
la BNE in modo tale da far si che sia presente unistruzione che vada sempre eseguita e non verr
abortita mai, dato che qualcosa che verr sempre eseguita. Ci porta a 2 vantaggi: istruzione
che non verr mai abortita e allontaniamo la STORE dalla ADD cos che tra la ADD, e la STORE
che deve usare la ADD, avremo 1 stallo in meno.
Quindi il programma un po pi ordinato sar:
1. LOAD 2. Decremento 3. ADD 4. BNE 5. STORE Quindi cambia anche l'offset della BNE che
diventa da -6 a -4
Abbiamo visto la gestione del loop per eseguire il calcolo sul residuo, scriviamo ora il codice che
gestisce il loop di una parte srotolata:
L.D F2 992 R3 // preleviamo il nostro operando
ADD.D F2 F2 F8
S.D F2 992 R3
F4 984 // stessa cosa altre 3 volte, in tutto deve essere fatta 4 volte. Non mi conviene usare
F2. Uso altri registri come F4 F6 F10. Ho scritto solo questo ma in realt per ognuna di queste 3
sto facendo una LOAD, una ADD e una STORE.
F6 976
F10 968
ADDI R3 R3-32 // decremento R3 8* numero fasi srotolato (4)
BNEZ R3 ( immediato che calcoleremo)
Questa non schedulata, quindi la scheduleremo in maniera da avere le istruzioni per riempire
alcuni buchi.
L.D F2 992 R3
L.D F4 984 R3
L.D F6 976 R3
L.D F10 968 R3
ADD.D F2 F2 F8
ADD.D F4 F4 F8
ADD.D F6 F6 F8
ADD.D F10 F10 F8

S.D F2 992 R3
S.D F4 984 R3
S.D F6 976 R3
S.D F10 968 R3
ADDI R3 R3-32
BNEZ R3 -14
Il codice funziona, vediamo se si pu fare una schedulazione migliore.
Abbiamo una serie di stalli che sono quelli dati dalle latenze dalle ADD, STORE, ecc. Cosa
dobbiamo fare inserire un'istruzione dopo la BNEZ. Di certo non possiamo prendere l ADDI
prima di lei perch la BNZ lavora proprio su R3. Se sposto S.D F10 968 R3, potrebbe andare
meglio e qualcosa deve cambiare nel suo immediato. Sposto l ADDI R3 R3-32, allontanandola
dalla BNEZ, dopo tutte le ADD.D per allontanare ancora di pi le ADD dalle STORE regalando un
colpo di clock a tutto il sistema che calcola il risultato prima di scriverlo in memoria. Sto creando
una latenza di 4 istruzioni tra la ADD che produce un registro e la STORE che lo vuole usare.
Sto anche allontanando questa ADD da BNEZ, in maniera che R3 calcolato prima del confronto.
Quindi S.D diventa F2 1024 R3, S.D F4 1016 R3, S.D F6 1008 R3 e infine F10 1000 R3.
Devo anche ricalcolare limmediato della BNEZ che da 14 diventa 13.
Vediamo un'altra tecnica che la pipeline da programma. Supponiamo di avere un loop:
AX+Y=Z
L.D x 1000 R1 // R1 inizializzato a 8*N
L.D y 2000 R2 // gi anticipato la LOAD per evitare uno stallo
MULT.D x' x A // abbiamo una latenza che ci impedisce di avviare subito la somma
ADD.D z x' y // altra latenza per arrivare alla STORE successiva
S.D z 3000 R1
ADDI R1 R1-8
BNEZ R1-7
Su questo codice si pu fare una schedulazione allontana la ADD R1 R1-8 dalla BNEZ e
posizionandola tra la MULT e ADD Riempio cos una latenza della MULT. Se faccio questo, 3000
diventa 3008 e inserisco unistruzione dopo la BNEZ, dove la miglior candidata la STORE. Quindi
BNEZ R1-7 diventa R1-6 Loop canonico non srotolato.
Abbiamo che ognuna di queste istruzioni si trova a operare sui dati prodotti dalle istruzioni
precedenti. E vero che tra unistruzione e laltra devo aspettare che una produca il risultato, ma se
invece di riempire questi stalli con altre istruzioni come abbiamo fatto con lo srotolamento del loop (
loop con molte istruzioni in modo da avere delle istruzioni da mettere al posto degli stalli); la
filosofia che voglio perseguire : se gli stalli vengono riempiti non da istruzioni del loop ma da
Istruzioni che fanno parte, dal punto di vista logico, da altre istruzioni del loop. Ovvero?
Supponiamo che quando sto producendo questo prodotto, il risultato del prodotto dovr essere
sommato alla y ma nelliterazione successiva del loop. Cio, quando sto eseguendo la ADD, sto
facendo la somma del registro che stato prodotto, dallistruzione, nelliterazione precedente. E
come se ogni volta che eseguo un'istruzione del loop, listruzione si trova a operare con operandi
che provengono da istruzioni di iterazioni precedenti. E' come se avessi una pipeline del
programma in cui ogni istruzione che parte in pipeline con quella successiva del loop ma la quale
agganciata a quella precedente. Posso immaginare una specie di diagramma:

Abbiamo strutturato la pipeline e abbiamo un transitorio di svuotamento e uno di riempimento.


Vediamo come viene scritto il codice:
Una prima parte transitoria:
L.D X
L.D Y
L.D X
L.D Y
ADD
LD
LD
ADD
MULT
Adesso parte il loop vero e proprio, questo viene organizzato in loop N-3 volte.
LD
LD
ADD
MULT
STORE
Dopo questo loop abbiamo
ADD
MULT
STORE
MULT
STORE
STORE
La struttura completamente differente dal loop unrolling, un transitorio di riempimento, un loop
che gira N-3 dove 3 sono le 3 iterazioni e infine un transitorio di riempimento dove si completano a
vicenda ( 3 2 1).

VEDERE DISPENSE DI CALCOLATORI A PARTIRE DA PAGINA 91, SONO LE SLIDE


PROIETTATE IN CLASSE CON SPIEGAZIONI.

Possiamo avere un CPI minore di 1? In maniera teorica possibile supponendo di poter prelevare
pi di unistruzione per colpo di clock. Se ne riesco a prelevare 1 sola, sono limitato a non poter
eseguire pi di unistruzione per colpo di clock. Facciamo finta di avere una memoria con banda
pi importante, riesco a fare un CPI minore di 1? Si, ma bisogna capire che cosa dobbiamo
gestire. Nel momento in cui abbiamo la possibilit di prelevare pi di unistruzione per colpo di
clock, dobbiamo anche eseguirle e devo far si di non avere conflitti di dati tra le istruzioni. Se sto
eseguendo contemporaneamente 2 istruzioni, e una vuole usare il risultato dellaltra, chiaro che
questa cosa non deve avvenire. La prima cosa poter prelevare, prendo 2 istruzioni:
i EX
i+1 EX
Queste 2 istruzioni le posso eseguire contemporaneamente, se non ho conflitti di dati e strutturale.
Nel senso che, se sto facendo la EX di una e dellaltra, non possono poter usare entrambe lALU, o
se lo devono usare, devono esserci necessariamente 2 ALU.
Ci viene in mente una certa cosa vista quando abbiamo definito il processore fp. Abbiamo una
parte di hardware completamente distinta dalla parte di hardware a virgola fissa. Le unit di calcolo
sono diverse e anche i registri erano registri diversi. Se preleviamo 2 istruzioni e queste sono una
che opera in virgola fissa, e una che opera a virgola mobile, abbiamo risolto, in 1 colpo, solo i
problemi enunciati poco fa(un risultato in virgola mobile non viene usato da quello a virgola fissa e
viceversa, tanto vero che sono su 2 banchi di registri differenti). C un eventuale conflitto solo se
entrambe vogliono fare accesso in memoria Questo avviene solo con operazioni di tipo
LOAD/STORE, ovvero non detto che tutte le istruzioni siano necessariamente di LOAD o STORE.
Immagino una situazione in cui un processore, se riesce a leggere 2 istruzioni dalla memoria, e
casualmente queste 2 istruzioni sono una virgola intera e una in virgola mobile, le 2 istruzioni
possono vivere simultaneamente senza dover stare a litigare n sui dati n sulle risorse di calcolo.
Se il compilatore stato bravo a scrivere il programma, che ha una serie di istruzioni ( alternanza
fissa mobile), e riesco ad andare in memoria, non a leggere un'istruzione ma a leggerne 2, trovo
allinterno del processore 2 istruzioni che, senza aver fatto cose complicate( come raddoppiare l
ALU), possono essere eseguite contemporaneamente senza conflitti di dato e strutturale.
Questo processore si chiama processore superscalare Ogni colpo di clock opera su qualcosa
che pi di uno scalare. E' chiaro che il compilatore non sempre riesce a fare un lavoro di questo
genere.
Un processore superscalare, quindi, parte prelevando le 2 istruzioni, fa immediatamente un
controllo per vedere se sono una fissa e una mobile, se lo sono vengono eseguite in
contemporanea. Se sono dello stesso tipo, far partire la 1 e laltra la terr posteggiata a partire
dal prossimo colpo di clock. Lefficienza di questo tipo di macchina si potr quantificare in
funzione del programma che la deve eseguire. Nel momento in cui ho un programma, in cui il
compilatore riuscito a schedulare in coppie di istruzioni di diverso tipo, siamo apposto; altrimenti
avr prelevato una coppia dalla memoria, ma il prossimo fetch viene stallato perch ho ancora da
eseguire lavvio della 2 istruzione. Vediamo come questo schema pu essere ancora pi
potenziato.
Dal punto di vista del conflitto strutturale, abbiamo un ulteriore margine di lavoro: vero che
all'interno abbiamo una dicotomia fra unit che gestiscono dati interi e fp, ma si detto che
all'interno della parte di processore del fp, le unit di calcolo sono separate e indipendenti (esiste
anche una certa ridondanza di queste unit Pi di una). Supponendo di avere a che fare con
istruzioni che non hanno conflitto di dati, posso pensare di avviare contemporaneamente una intera

e 2o3 in virgola mobile, che per non devono far uso di dati dipendenti ma non devono usare la
stessa unit funzionale. Possiamo immaginare un sistema che prelevi un certo numero di istruzioni
o, se vogliamo, un istruzione che conterr al suo interno le informazioni di pi di unistruzione,
quella che viene chiamata unistruzione molto lunga, ( a very long instruction word) e gestire un
processore che prelevi unistruzione che sia una vliw e, da questi, se le istruzioni che sono
contenute in questa vliw non sono conflittevoli tra loro, allora le posso eseguire
contemporaneamente. Per fare questo bisogna capire, per, come gestire e organizzare queste
istruzioni. Lidea avere un gruppo distruzioni che fa uso di unit strutturali diverse da quelle altre
istruzioni Se in un modo riesco ad averlo, posso pensare di avviare unistruzione che al suo
interno abbia pi di unistruzione.

Potrebbero piacerti anche