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