Scheduling - WinDizio

Transcript

Scheduling - WinDizio
http://www.windizio.altervista.org/appunti/
File distribuito con licenza Creative Commons BY-NC-SA 2.5 IT
Copyright © 2008 - Michele Tartara
Parte I
Scheduling
I computer moderni possono eseguire più parti di dierenti istruzioni contemporaneamente. Le istruzioni adiacenti di un singolo programma possono essere decodicate ed eseguite simultaneamente. Ciò viene detto parallelismo a livello di istruzione
(instruction-level parallelism, ILP).
Una macchina a pipeline esegue, nello stesso ciclo di clock, la lettura degli operandi di un'istruzione, il calcolo dell'istruzione
precedente e la scrittura del risultato di quella precedente ancora.
Una very-long-instruction-word permette di eseguire più istruzioni nello stesso ciclo di processore. Il compilatore deve
assicurarsi che non siano data-dependent le une rispetto alle altre.
Una macchina superscalare può eseguire più istruzioni in contemporanea ed è compito dell'unità hardware di decodica
vericare le dipendenze dei dati.
Una macchina a scheduling dinamico riordina automaticamente le istruzioni mentre vengono eseguite in modo tale da poter
eseguire contemporaneamente istruzioni prive di dipendenze.
Diverse istruzioni richiedono tempi e unità funzionali del processore dierenti per essere eseguite.
È necessario schedulare le istruzioni in modo tale che non richiedano la stessa unità nello stesso momento. In questo modo,
possiamo usare la pipeline del processore per eseguire più istruzioni allo stesso istante.
Sulle architetture superscalari questo è un problema dei progettisti hardware, in quanto il riordino delle istruzioni viene
eseguito dinamicamente in hardware.
I conitti di risorse (unità funzionali, registri) non sono l'unica cosa che dobbiamo controllare: bisogna anche vericare che
le dipendenze tra i data (data dependencies) siano rispettate. Se un'istruzione deve precedere un'altra, possiamo schedularle in
modo tale che la seconda sia caricata prima che il risultato della prima sia pronto, ma assicurandoci che esso sarà disponibile al
momento in cui dovrà essere eettivamente utilizzato.
Alcuni pseudo-conitti (come write-after-write o write-after-read) possono essere eliminati rinominando le variabili, in modo
da utilizzare locazioni di memoria dierenti.
L'operazione di scheduling delle istruzioni si occupa di (ri)ordinare le istruzioni in modo da ridurre il tempo di esecuzione
e/o il consumo di potenza.
1 Scheduling dei blocchi basici
È dicile produrre uno scheduling ottimale per un intero programma. Si preferisce solitamente concentrarsi su uno scheduling
localmente ottimale, perchè abbiamo soluzioni pratiche per unità di codice più semplici e più piccole.
Il nostro obiettivo è minimizzare i buchi nella pipeline.
Le dipendenze tra i dati di un blocco basico sono rappresentate da un grafo orientato aciclico (DAG). Si può denire uno
scheduling tramite un ordinamento topologico delle istruzioni del blocco basico: il risultato prodotto è lo stesso, ma il tempo di
esecuzione è minimizzato.
1.1 Impedire gli stalli della pipeline
Se un'istruzione deve attendere il risultato di un'altra per poter essere eseguita, c'è il rischio che si debba attendere alcuni cicli,
privi di istruzioni utili, anchè esso sia disponibile.
Per non sprecare tali cicli, è possibile inserire delle istruzioni che non abbiano alcuna data-dependence irrisolta, posticipando
l'inserimento dell'istruzione che provocherebbe lo stallo. In tal modo, si prosegue ugualmente l'esecuzione del programma,
riducendo i tempi di esecuzione complessivi.
1.2 Attesa nei salti condizionati (branch delay )
Quando si deve eseguire un salto condizionato si sprecano alcuni cicli di clock attendendo di sapere quale ramo dovrà essere
eseguito, perchè la pipeline rimane parzialmente vuota. Tali stadi della pipeline possono essere sfruttati:
1. Inserendo istruzioni indipendenti dal risultato del salto condizionato (ad esempio, istruzioni successive che non abbiano
dipendenza dai dati elaborati da un ramo piuttosto che dall'altro).
2. Ipotizzando (tramite euristiche) quale sarà il ramo scelto e cominciando ad eseguirlo immediatamente (in caso di scelta
errata si rischia di aver sprecato risorse di elaborazione inutilmente)
1
http://www.windizio.altervista.org/appunti/
File distribuito con licenza Creative Commons BY-NC-SA 2.5 IT
Copyright © 2008 - Michele Tartara
3. (per macchine parallele) Eseguendo entrambe le istruzioni successive e, successivamente, considerando valida solo quella
che viene indicata come successore dal salto condizionato.
1.3 Scheduling dei salti condizionati per macchine con pipeline
Con branch scheduling si intende la capacità di:
1. riempire gli stadi della pipeline dopo una branch con istruzioni utili
2. riempire gli stati tra l'esecuzione di una compare e il momento in cui si è in gradi di eseguire una branch basandosi sul suo
risultato.
Ciò può essere fatto:
1. posticipando un'istruzione del blocco basico che è terminato dall'istruzione di branch
2. anticipando l'esecuzione di una (eventuale) istruzione che appaia come radice del DAG sia del blocco target (quello su cui
si va in caso di risposta true ) sia del blocco fall-through (quello in cui si cade in caso di risposta false )
3. eseguendo un'istruzione del blocco target, ed eventualmente annullandola tramite il suo bit di annullamento (nullication
bit ) se il risultato della branch dovesse portare al blocco fall-through.
Riempire gli stati di attesa dopo una chiamata a funzione In molte architetture, lo stesso problema si verica, oltre che
con le istruzioni branch, anche con le call di chiamata a funzione. Preferibilmente lo scheduler tenta di posticipare un'istruzione
precedente la chiamata.
Non ci sono molte istruzioni candidate, perchè l'analisi inter-procedurale non viene solitamente eseguita. La scelta migliore
è quindi, normalmente, quella di eseguire lo riempimento con le istruzioni che inseriscono i valori nei registri per il passaggio di
parametri.
1.4 Scheduling di sequenze (list scheduling )
Algoritmo per lo scheduling di operazioni che hanno relazioni di precedenza ma non cicli.
Lavora su un grafo orientato aciclico (DAG) che rappresenta le priorità (dipendenze) tra le istruzioni.
Ogni nodo ha un peso che ne rappresenta la latenza.
Uno schedule fattibile è un qualunque ordinamento topologico (sempre esistente, data l'assenza di cicli). Quando esistono
più possibili ordinamenti, bisogna sceglierne uno. La scelta avviene calcolando la somma dei pesi per ogni percorso dal nodo
corrente alla ne, e scegliendo il più lungo, perchè sarà quello che richiederà più tempo e quindi ha senso farlo cominciare per
primo.
2 Scheduling dei loop
I loop presentano più possibilità di parallelizzazione rispetto ai basic block.
2.1 Idealistico - Algoritmo di Aiken-Nicolau
Non ci sono conitti di risorse (risorse innite), solo dipendenze tra i dati. Ottiene parallelizzazione ottimale tramite gli algoritmi
di software pipelining (cioè attua via software un comportamento simile a quello di una pipeline: diverse iterazioni del loop
possono essere visti come diversi stadi di una pipeline hardware, esecuzioni simultanee di dierenti iterazioni di due istruzioni
possono essere visti come esecuzioni simultanee di stadi diversi di due istruzioni).
Algoritmo
1. Srotolare il loop per individuare le dipendenze tra le istruzioni con numero di iterazione dierente (allo scopo, può essere
utile costruire un grafo delle data-dependence, con archi tratteggiati per dipendenze tra diverse iterazioni e archi solidi per
dipendenze all'interno della stessa iterazione del ciclo oppure, se si è già srotolato il loop, un grafo DAG)
2. Schedulare ogni istruzione prima possibile
2
http://www.windizio.altervista.org/appunti/
File distribuito con licenza Creative Commons BY-NC-SA 2.5 IT
Copyright © 2008 - Michele Tartara
3. Rappresentare le istruzioni in un piano cartesiano, con il numero dell'iterazione sull'asse delle ascisse e il tempo su quello
delle ordinate
4. Trovare i gruppi di istruzioni collocate a diverse pendenze (slopes )
5. Prendere la pendenza più verticale e distribuire su di essa le istruzioni collocate a pendenze più orizzontali (in pratica,
si ritardano il più possibile nel tempo le istruzioni no a far loro raggiungere quelle con la pendenza più verticale, pur
mantenendo ogni istruzione all'interno della proprio numero di iterazione). In questo modo, tutte le istruzioni del corpo
del ciclo procedono alla stessa velocità
6. Ri-arrotolare il loop, creando un nuovo pipelined schedule, con un corpo del ciclo preceduto da un prologo e seguito da un
epilogo.
Teoremi
ˆ Se ci sono K istruzioni nel ciclo, la ripetizione di un gruppo di istruzioni (il corpo vero e proprio del ciclo) separato dagli
altri apparirà sempre entro K 2 iterazioni (e solitamente molto prima)
ˆ Possiamo aumentare la pendenza dei gruppi di istruzioni più orizzontali, eliminando quindi la distanza dalle altre istruzioni,
o almeno rendendola piccola e costante (perchè altrimenti tenderebbe a crescere), senza violare la dipendenza dei dati
ˆ Il tableau risultante ha un insieme ripetuto di m cicli identici, che possono costituire il corpo di un loop sulla pipeline
ˆ Il loop risultante ha uno scheduling ottimale (viene eseguito nel minor tempo possibile).
2.1.1 Realistico
Un algoritmo di scheduling realistico, limitato dalle risorse, deve ricevere in input:
1. Il programma da schedulare
2. La descrizione delle risorse che ogni istruzione usa in ogni suo stadio di pipeline
3. La descrizione delle risorse disponibili sulla macchina (tipo e numero delle unità funzionali, numero di istruzioni eseguibili
contemporaneamente, ecc)
È improbabile che lo scheduling limitato dalle risorse sia eciente, in quando si tratta di un problema NP-completo. Usiamo
perciò un algoritmo di approssimazione che funziona ragionevolmente bene nel caso tipico.
Algoritmo - Iterative modulo scheduling È un algoritmo pratico ma non ottimale. Si basa su backtracking iterativo per
trovare un buono schedule che obbedisca ai vincoli di risorse e di dipendenza dei dati. Non è garantito che trovi soluzioni (anche
se esistono) e può trovare soluzioni per le quali sia impossibile fare assegnamento dei registri. Tuttavia è noto funzionare bene
in pratica.
A partire da un semplice loop, ciò che si vuole ottenere è uno scheduling che richieda il numero minimo di cicli per l'esecuzione
del loop.
Chiamiamo initiation interval (∆min ) il limite inferiore del numero di cicli macchina a cui può essere ridotto il corpo del
loop.
Possiamo determinare ∆min a partire da due indicatori:
1. Dipendenze dai dati (data dependencies )
∆min non può essere minore della latenza (MDD , n° di istruzioni consecutive con dipendenze dai dati) delle istruzioni del
loop.
Esempio:
Latenza MDD = 3
2. Risorse
Si conta il numero di operazioni della ALU o di LOAD/STORE necessarie alle istruzioni del loop. Lo si divide per il n° di
#OP s
, l'indicatore del massimo permesso dalle risorse.
unità funzionali e si ottiene MRES = #units
3
http://www.windizio.altervista.org/appunti/
File distribuito con licenza Creative Commons BY-NC-SA 2.5 IT
Copyright © 2008 - Michele Tartara
Si può inne calcolare ∆min come ∆min = max(MDD , MRES ).
L'algoritmo tenta di disporre tutte le istruzioni del corpo del loop in uno schedule di ∆ cicli. ∆ viene inizializzato a ∆min e
incrementato man mano, se l'algoritmo non è in grado di trovare uno schedule fattibile.
Nell'eettuare l'assegnamento delle istruzioni ai vari cicli macchina, si considera l'idea chiave del modulo scheduling: se
un'istruzione viola il limite imposto dal numero di unità funzionali all'istante t, lo violerà anche all'istante t + ∆ o a qualunque
t0 tale che t0 modulo ∆ = t.
Per eseguire lo scheduling delle istruzioni, le si dispone all'interno di una tabella con un numero di colonne pari al numero
massimo di istruzioni eseguibili in contemporanea e un numero di rige pari a ∆ (cioè, inizialmente, ∆min ).
All'interno di questa tabella si dispongono man mano tutte le istruzioni (l'ordine con cui vengono disposte dipende da una
qualche euristica: ad esempio, si potrebbero inserire prima le istruzioni che richiedono più risorse o che sono in posizioni critich
di data-dependence, e poi riempire gli spazi vuoti con le altre).
Ogni volta, l'istruzione a più altra priorità h viene disposta come segue:
1. nel primo time slot che rispetta tutti i vincoli di dipendenza rispetto ai predecessori di h già inseriti e rispetta tutte i vincoli
di risorse
2. se non c'è un tale spazio in ∆ cicli consecutivi, non ci potrà mai essere. h viene allora inserito ignorando i vincoli di risorse
(ma rispettando quelli di dipendenze).
In questo caso, in seguito, altre istruzioni vengono rimosse (come in un algoritmo backtracking) per rendere nuovamente
legale lo schedule: successori di h che non rispettano la data-dependence o istruzioni che hanno conitti di risorse con h.
L'algoritmo trova una soluzione rapidamente o facilmente non la troverà mai: si assegna quindi un budget di c · n tentativi di
scheduling (con c = 3 o simili), dopo di che, se non si è trovata una soluzione, si passa ad analizzare il ∆ successivo.
Se un arco def-use associato a una variabile j diventa più lungo di ∆, diventa necessario avere più di una copia di j , ad
iterazioni dierenti, con istruzioni MOVE che copiano a catena una variabile nell'altra quando necessaria.
Per individuare i conitti di risorse, l'algoritmo fa uso di una tabella (resource reservation table ) che ha una colonna per ogni
risorsa e una riga per ogni istante di tempo (con un massimo di ∆). Ciò permette di individuare conitti in tempo costante.
Eetti sull'allocazione dei registri La posizione delle istruzioni all'interno del ciclo fa variare la lunghezza degli archi di
data-dependence tra esse: cambiando l'istante in cui un'istruzione viene calcolata, cambiano gli archi, e quindi le interferenze
di liveness tra le variabili. Ciò può portare al successo o al fallimento dell'algoritmo di allocazione dei registri. L'allocazione
dei registri dovrebbe essere considerata (da un algoritmo ottimale) contemporaneamente allo scheduling. Il Modulo Scheduling,
tuttavia, la pone in una fase successiva, sperando che tutto vada per il meglio.
3 Trace scheduling
Si tratta di un algoritmo molto in voga anni fa, che ora sta perdendo importanza.
Esegue lo scheduling sull'intero programma, e non su semplici loop o blocchi basici.
Per trace, si intende una sequenza di istruzioni, inclusi i salti condizionati ma esclusi i loop, che viene eseguita per qualche
dato in ingresso. Se c'è un loop, il trace viene interrotto e ne comincia uno nuovo (come per i blocchi basici con i salti e le
etichette).
L'algoritmo prova a capire qual è il trace eseguito più di frequente e lo schedula come se si trattasse di un basic block.
Viene aggiunto del codice di compensazione all'ingresso e all'uscita di ogni trace per compensare gli eetti negativi della
out-of-order execution. Il corpo dei cicli viene normalmente srotolato diverse volte prima di essere schedulato.
Il processo di scheduling e compensazione viene ripetuto nché tutte i trace sono stati schedulati oppure è stata raggiunta
una soglia di frequenza di esecuzione. Qualunque trace rimanente è quindi schedulato con tecniche più semplici.
L'algoritmo è molto utile ed eciente solo nel caso in cui vi sia netta prevalenza di un cammino rispetto agli altri. Si hanno
cattive performance se le frequenze di esecuzione dei trace sono bilanciate.
4 Esecuzione predicativa (predicative execution )
Si può applicare quando all'interno di un loop c'è un'istruzione di salto condizionato.
Vengono eseguite le istruzioni di entrambi i rami e si sceglie poi solo il risultato di esatto, che sarà mantenuto. Questo porta
a permettere uno scheduling più eciente del programma.
È una soluzione non adatta per i dispositivi a batteria, in quanto consuma più energia.
4
http://www.windizio.altervista.org/appunti/
File distribuito con licenza Creative Commons BY-NC-SA 2.5 IT
Copyright © 2008 - Michele Tartara
Non è molto adatta anche nel caso in cui il risultato usato meno di frequente sia particolarmente pesante da calcolare, in
quanto si sprecherebbe molta potenza di calcolo.
Non può essere usato se uno dei rami dell'if ha un side eect, perchè esso deve essere applicato solo se la condizione è quella
esatta.
5 Predizione dei salti condizionati (branch prediction )
Con questo metodo, quando c'è un salto condizionato si prova a predire quale ramo verrà preso e si comincia ad eseguirne le
istruzioni appena possibile, per evitare stalli della pipeline. Il risultato dell'elaborazione non è in un registro ma in un buer.
Verrà spostato nel registro del risultato se la speculazione ha avuto successo.
Questa tecnica è usata anche a livello hardware, specialmente nelle macchine Pentium recenti, che hanno un instruction
set esterno (x86, mantenuto per ragioni di compatibilità) e uno interno, di tipo RISC, per ragioni di prestazioni. Siccome le
istruzioni devono essere tradotte, esse vengono anche rischedulate, e facendo questo si usa la branch prediction.
La predizione può essere fatta con diversi metodi: la predizione dinamica, ad esempio, è basata sulle decisioni prese nelle più
recenti esecuzioni di tale salto.
Vantaggi dello scheduling statico (eseguito dal compilatore)
ˆ Non usa risorse hardware della CPU: meno potenza
ˆ Può analizzare lunghi cammini di data-dependence
ˆ Può usare algoritmi di scheduling computazionalmente pesanti (non ha vincoli real-time).
Vantaggi dello scheduling dinamico (eseguito dalla CPU)
ˆ A runtime sono disponibili più informazione (ad es. sui cache miss)
ˆ Sfrutta le nano architetture (come quella interna degli Intel recenti) sfruttando più registri di quelli disponibili nell'ISA
ˆ Utile per l'emulazione di vecchi ISA su architetture moderne
ˆ Può essere interessante su sistemi multiprocessore dove i processori superano il numero di thread in esecuzione.
5.1 Euristiche per la brench prediction (statica)
Puntatori se un loop esegue un confronto di uguaglianza tra puntatori, predire la condizione come falsa (perchè, dato il numero
di indirizzi di memoria disponibili, è più probabile che siano diversi piuttosto che uguali).
Chiamate a procedure è meno probabile che venga scelto un ramo che domina una chiamata a funzione (le funzioni spesso
sono chiamate per gestire situazioni eccezionali).
Return è meno probabile che venga scelto un ramo che domina un return di una procedura.
Cicli è più probabile che venga scelto il ramo (se esiste) che è la testata di un ciclo contenente l'istruzione di scelta per cui si
sta facendo la previsione.
Cicli è più probabile che venga scelto il ramo (se esiste) che è la pretestata di un loop se essa non post-domina l'istruzione di
scelta.
Guardia Se un valore r è usato all'interno del test dell'istruzione di scelta, allora è più probabile la scelta di un ramo in cui r
è viva e che non postdomina l'istruzione di scelta.
5