Loop Optimization
Introduzione
In questo capitolo verrano descritte trasformazioni di alto livello per l'ottimizzazione di programmi scritti in linguaggi imperativi per architetture ad alte performance (incluse macchine superscalari, vettoriali e multiprocessore).
Steps dell'applicazione di una trasformazione:
Decidere quale parte di programma da ottimizzare, e quale trasformazione applicare.
Verificare che la trasformazione puรฒ essere applicata.
Trasformare il programma (step su cui รจ presente il focus del corso).
Correttezza di una trasformazione
L'applicazione di una trasformazione deve lasciare il programma intatto.
Trasformazione legale: Una trasformazione รจ legale se i programmi originale e trasformato producono esattamente lo stesso output per ogni esecuzione identica.
Trasformazione legale (seconda definizione): Una trasformazione รจ legale se i programmi originale e trasformato producono esattamente lo stesso output per ogni esecuzione identica e sematicamente corretta.
Esecuzione identica: due esecuzioni sono identiche se hanno lo stesso input e se ogni coppia corrispondente di operazioni non deterministiche nelle due esecuzioni producono lo stesso risultato.
Operazione semicommutativa: Definiamo semicommutative trasformazioni che sono commutative algebricamente ma non dal punto di vista computazionale.
Scope di una trasformazione
Statement: solitamente espressioni aritmetiche
Basic Block
Innermost loop: parte del codice che รจ eseguita piรน di frequente, di conseguenza trasformazioni a questo livello sono molto efficaci dal punto di vista computazionale.
Perfect loop nest: ogni ciclo contiene solo un ciclo innestato e non altre istruzioni, eccetto il loop piรน interno.
General loop nest
Procedure
Data Dependencies
Possono essere control dependencies, derivanti da istruzioni di controllo del program flow, o data dependencies (RAW, WAR, WAW).
Queste ultime dipendono dall'uso delle stesse variabili (memerizzate in registri) da parte di istruzioni diverse:
Flow dependency: S4โ ha una flow dependence con S3โ (S3โโS4โ) quando S3โ deve essere eseguita prima perchรจ scrive un valore che รจ letto da S4โ.
Antidependence: S6โ una una antidependence con S5โ (denoted by S5โโS6โ) quando S6โ scrive una variabile letta da S5โ.
Output dependence: S8โ ha una output dependence con S7โ (denoted by S7โ ใ S8โ) quando entrambe le istruzioni scrivono la stessa variabile.
Note
Stride: distanza tra accessi consecutivi ad un array, ad esempio un loop che accede ogni 4 elementi di un array รจ uno stride-4 loop. Piรน basso รจ lo stride, migliore รจ la memory locality.
Mutiply/Add availability: molto frequenti nel processing di segnali, per questo motivo spesso sono previste negli ISP. Questa strategia comporta importanti incrementi prestazionali. Altri incrementi importanti sono forniti dal riutilizzo di cache lines.
Gli algoritmi di dependence analysis assumono che i loop abbiano incrementi unitari del contatore associato, altrimenti il compilatore potrebbe non riuscire a normalizzare l'indice per gestire le loop-carried dependencies. Esempio di loop-carried dependency:
Nel codice sopra non ci sono dipendenze all'interno delle singole iterazioni del loop, ma sono presenti dipendenze tra iterazioni contigue.
Un iterazione puรฒ essere definita come un vettore di d elementi I=(i1โ,โฆ,idโ) dove ogni indice ipโ resta nell'intervallo previsto. Il loop piรน esterno corrisponde all'indice piรน a sinistra nell'array.
Nel contesto di un loop, un istruzione in una generica iterazione J dipende da un istruzione in una generica iterazione I se e solo se almeno un accesso ad una variabile รจ in scrittura e IโบJโงโp:fpโ(I)=gpโ(J), cioรจ รจ presente una dipendenza quando i valori dei pedici delle variabili sono gli stessi in iterazioni differenti del ciclo.
Distance Vector: Per una dipendenza XโY il distance vector รจ definito come:
YโX=(y1โโx1โ,โฆ,ydโโxdโ)Nota che il distance vector fa riferimento a dipendenze tra iterazioni dei loop, non a elementi di un array.
Dependence Vector: A volte non รจ semplice capire la distanza esatta della relazione di dipendenza a compile time (la quale puรฒ anche variare tra iterazioni diverse dei loop). Definiamo il dependence vector per una dipendenza IโJ come il vettore W=(w1โ,โฆ,wdโ), dove:
wpโ=โฉโจโงโ<=>โipโ<jpโipโ=jpโipโ>jpโโ
Ottimizzazioni (47 in totale)
General Purpose
Loop-Based Strength Reduction
Scope fino al singolo statement, riduzione di un operatore utilizzando un operatore meno costoso. Gli incrementi prestazionali sono maggiori se applicato nel contesto dei loop.
ExprInitUseUpdatec * i
T = c
T
T = T + c
c^i
T = c
T
T = T * c
(-1)^i
T = -1
T
T = -T
x / c
T = 1 / c
x * T
Induction Variable Elimination
Definiamo l'induction variable come una variabile il cui valore deriva dal numero di iterazioni giร eseguite dal un loop che la contiene. L'uso di strength reduction puรฒ portare all'eliminazione di inductions variable, riducendo la pressione sull'uso dei registri.
Loop-Invariant Code Motion
Computazione all'interno di un loop, ma risultato loop invariant. La computazione puรฒ essere spostata all'esterno del loop.
Una definizione piรน generica di questo processo รจ code hoisting, cioรจ lo spostamento di una qualunque computazione in un punto piรน recente della procedura.
Loop Unswitching
Caso in cui un loop contiene un condizionale con una test condition che รจ loop independent. Il loop รจ replicato in entrambi i branch del condizionale, risparmiando il costo del confronto per ogni iterazione del ciclo.
Loop Reordering
Focus principale delle trasformazioni che riguardano i loop, dal momento che migliorando il parallelismo e la memory locality questa tipologia di trasformazioni puรฒ portare importanti incrementi prestazionali.
Loop Interchange
Scambia il livello di nesting di due loop in un perfect loop nest. Puรฒ essere utilizzata per:
Vettorizzazione
Migliorare performance tramite parallelizzazione.
Ridurre lo stride.
Aumentare il numero di espressioni loop invariant nel loop piรน interno.
Esempio:
Diventa:
Se
aรจ troppo grande per essere memorizzata in cache, lo scambio, riducendo il loop originariamente interno dal stride-n a stride-1, migliora le performance riducendo il numero di cache miss.Nella versione originale
tot[i]puรฒ essere memorizzato in un registro, migliorando le performance seaรจ abbastanza piccola da essere contenuta in un registro.Scambiare i bound dei loop รจ semplice se lo spazio delle iterazioni รจ rettangolare.
Loop Skewing
Utilizzato principalmente in combinazione con loop interchange, si applica a cicli innestati.
Skewing: aggiunta di uno skewing factor (indice del loop esterno moltiplicato per una constante f) ai bounds dell'indice del loop piรน interno, e sottrazione della stessa quantitร dagli utilizzi dell'indice nel ciclo interno.
Puรฒ tornare particolarmente utile nel caso si acceda sequenzialmente a matrici prima per riga e poi per colonna (o viceversa) in modo da rendere il loop piรน esterno parallelizzabile: nessuno dei due cicli dell'esempio puรฒ essere parallelizzato in forma originale, ma dopo lo skewing le diagonali possono essere parallelizzate.
Loop Reversal
Cambia la direzione in cui viene effettuata l'iterazione del loop (ad esempio da zero ad n diventa da n a zero). Il cambiamento puรฒ essere propagato nel dependence vector negando l'entry corrispondente al loop in questione.
Strip Mining
Modifica della granularitร di un loop. Puรฒ essere utilizzata in ambito di vettorizzazione del codice.
Ad esempio
do i = 1, ndividenin strips di 64 elementi (tn = floor(n/64)). Poi il loop iterererร su una strip alla volta, e sugli elementi restanti (do i = tn+1, n). In questo modo il codice vettoriale puรฒ effettuare le operazioni direttamente sulle strips:Cycle Shrinking
Versione specializzata di strip mining.
Trasformazione che converte un ciclo seriale in due cicli innestati, loop esterno (seriale) ed un loop interno (parallelizzato, come l'operazione vettoriale dell'esempio precedente
a[ti:ti+63] = a[ti:ti+63] +c). Questa ottimizzazione รจ utile per permettere l'applicazione di strip mining al ciclo interno dal momento che l'elemento (o gli elementi) dell'array con cui esso lavora sono costanti per tutto lo spazio delle iterazioni del ciclo.Utilizzato in casi in cui il ciclo in questione ha dipendenze fra dati (cosa ad esempio non valida per l'esempio della trasformazione precedente) a distanza costante. In caso in cui i dati in questione sono memorizzati tramite matrici si utilizza Loop Tiling, una generalizzazione di Cycle Shrinking / Strip Mining.
Loop Tiling
Generalizzazione multidimensionale di strip mining. L'obbiettivo primario รจ quello di migliorare il riutilizzo della cache (ad esempio un loop che manipola una matrice che รจ troppo grande per essere memorizzata in cache). Idea: blockwise matrix operations. Aumenta il numero di loop innestati dividendo la matrice in sottomatrici su cui effettuare la stessa operazione del loop originale.
Loop Distribution
Suddivisione di un singolo loop in piรน loops. Ogni loop ha lo stesso spazio delle iterazioni del loop originale ma contiene un sottoinsieme delle istruzioni. Possibili utilizzi:
Perfect loop nests
Sottoloop con meno dipendenze
Miglioramenti di cache e TLB locality
Miglioramento del riutilizzo di registri
Ad esempio posso avere un loop che lavora con due arrays, uno con dipendenze, l'altro senza. In questo modo posso parallelizzare il loop che accede al primo array.
Loop Fusion
Inverso di loop distribution. Possibili miglioramenti:
Overhead minore
Miglior parallelismo
Miglior utilizzo di registri, operazioni vettoriali, data cache, TLB
Per n grandi questa trasformazione migliora le prestazioni su macchine vettoriali. Nota che per fondere due loop questi devono avere gli stesso bounds (condizione che puรฒ essere ottenuta via peeling).
Non puรฒ essere eseguita se esistono due statements con dipendenze nel loop fuso.
Loop Restructuring
Trasformazioni che modificano la stuttura dei cicli.
Loop Unrolling
Replicazione del corpo del ciclo per un certo numero di volte u, chiamato unrolling factor, usando u come nuovo step. ร la tecnica usata dalle macchine VLIW per generare le istruzioni. Miglioramenti:
Overhead minore
Miglior parallelismo
Miglioramenti nella locality di registri, data cache e TLB.
Puรฒ essere necessario un loop epilogue nel caso in cui non รจ noto a compile time se il numero di iterazioni del loop sarร esattamente un multiplo di u.
Nota che, se u>2, il loop epilogue รจ un loop e non un condizionale.
Loop Coalescing
Contrario di tiling: abbiamo dei cicli innestati, li trasformiamo in un singolo ciclo. Dal momento che non modifica l'ordine delle iterazioni รจ sempre legale. Viene utilizzato un singolo iteratore da cui estraiamo gli iteratori dei precedenti due cicli all'interno del corpo del ciclo tramite manipolazione algebrica:
Loop Collapsing
Versione specializzata di loop coalescing in cui viene ridotta la dimensionalitร di array multidimensionali modificati da cicli innestati (entrambi stride-1).
Loop Peeling
Permette loop fusion rimuovendo iterazioni iniziali o finali da un ciclo ed eseguendole separatamente. Utile per rimuovere dipendenze in modo da migliorare la parallelizzazione.
Loop Normalization
Modifica i loop in modo che l'iteratore inizi da 1 (o 0), e sia incrementata di 1 ad ogi iterazione.
Loop Spreading
Modifica due loop seriali fra loro in modo che parte della computazione all'interno del secondo loop venga spostata nel primo in modo che i due possano essere eseguiti in parallelo.
Loop Replacement Transformation
Trasformazioni che modificano completamente la struttura dei cicli.
Reduction Recognition
Trasformazione che calcola uno scalare a partire da un array. La riduzione puรฒ essere parallelizzata se l'operazione eseguita รจ associativa. La parallelizzazione maggiore si ottiene utilizzando un albero per eseguire la riduzione: si sommano gli elementi pair-wise in modo ricorsivo a partire dalle foglie. Il numero di operazioni passa da O(n) a O(logn).
Loop Idiom Recognition
Termine generale per identificare il riconoscimento di istruzioni hardware specializzate, tipiche di macchine vettoriali o di architetture parallele (e.g. SIMD), che supportano la riduzione direttamente a livello hardware.
Array Statement Scalarization
Nei linguaggi in cui alcune operazioni possono essere espresse con notazione degli array. Ad esempio:
Verrร convertita in una versione scalare:
Il problema puรฒ essere risolto invertendo il ciclo, eliminando cosรฌ le antidipendenze.
Memory Access Transformations
Ottimizzazioni dell'uso della memoria per migliorare il riutilizzo e la parallelizzazione. L'utilizzo efficiente dei registri e della cache di primo livello รจ essenziale.
Array Padding
Data locations inutilizzate vengono inserite tra le celle di un array, o tra array diversi, oppure array vengono memorizzati in modo interleaved. Utile per mitigare conflitti di bank, cache e TLB. Grazie al padding si ottiene uno stride-1 tra le varie banks, in modo che il padding p rispetti il seguente vincolo: gcd(s+p,B)=1, con s lo stride originario e B il numero di banks.
Gli svantaggi sono utilizzo di memoria maggiore, e conflitti con altre ottimizzazioni come ad esempio loop collapsing.
Scalar Expansion
In caso di variabili usate come temporary all'interno del corpo di cicli, tali variabili creano antidipendenze fra iterazioni. Allocando una temporary per ogni iterazione (un array di dimensioni uguale al numero di iterazioni del ciclo ad esempio) risolve questo problema.
Array Contraction
Riduzione di una delle dimensioni dell'array se questa non viene utilizzata all'interno di un ciclo innestato.
Scalar Replacement
Simile ad Array Contraction, quanto un elemento di un array รจ utilizzato di frequente in un loop innestato, puรฒ essere memorizzato in un registro e salvato in memoria quando il loop interno smette di iterare.
Code Colocation
Si applica tipicamente a instruction cache. Aumenta la dimensione del codice, ma diminuisce il numero di salti.
Migliora l'accesso della memoria facendo in modo che codice correlato sia memorizzato nelle stesse aree di memoria.
Ottenuto tramite profiling della frequenza con cui determinati costrutti vengono eseguiti.
Displacement Minimization
Simile a code colocation, ma relativo ai Basic Block: consiste nel posizionamento in aree di memoria contigue di B.B. eseguiti spesso nello stesso execution flow.
Il posizionamento di B.B. รจ un problema di ottimizzazione complesso. Il target di un branch o di un jump รจ specificato relativamente al valore del program counter (PC), se il controllo รจ trasferito in un'area di memoria al di fuori del range dell'offset, รจ necessaria una sequenza di piรน istruzioni per effettuare il salto con un conseguente peggioramento prestazionale. Il codice quindi dovrebbe essere organizzato in modo da salvare in memoria sezioni di codice correlato tali per cui siano vicine.
Partial Evaluation
Trasformazioni che fanno riferimento alla precomputazione a compile time di parti del codice.
Constant Propagation
Constant Folding
Se un espressione contiene un operazione con valori costanti come operandi, il compilatore puรฒ sostuirla con il risultato.
Copy Propagation
Se alcune variabili sono riassegnate, il compilatore puรฒ propagare la variabile originale in modo da eliminare copie ridondanti.
Forward Substitution
Generalizzazione di Copy Propagation, utilizzo di una variabile รจ sostituito dalla sua definizione.
Algebraic Simplification
Il compilatore applica regole algebriche per ottimizzare alcune operazioni (e.g. โ 0,+0,โ 1,+1)
Strength Reduction
Operatori costosi sono sostuiti con operatori meno costosi (e.g. xโ 2 diventa diventa xโ x, etc.). Il codice della funzione รจ ottimizzato e la chiamata originaria รจ sostituita con la nuova versione, che occupa piรน spazio ma รจ piรน efficiente.
Superoptimizing
Trasformazione che cerca di sostituire una sequenza di istruzioni con l'alternativa ottimale tramite ricerca esaustiva (iniziando da una sequenza di una sola istruzione, e in caso la sostituzione non sia effettuabile, reitera il processo aumentando il numero di istruzioni prese in considerazione).
Redundancy Elimination
Trasformazioni con l'obbiettivo di eliminare due tipi di computazioni: quelle che non sono raggiungibili, e quelle che non servono alcuna utilitร .
Spesso รจ utile per "ripulire" tali computazioni dopo che altre ottimizzazioni hanno modificato la struttura del codice.
Unreachable-Code Elimination
Codice che non verrร raggiunto da nessuna path del CFG della procedura, ad esempio un condizionale che รจ sempre vero o sempre falso, oppure un loop che itera zero volte. Spesso codice non raggiungibile viene generato da constant propagation, ad esempio xโค0 puรฒ diventare constโค0 e di conseguenza uno dei branch del condizionale puรฒ diventare non raggiungibile.
Useless-Code Elimination
Se il compilatore verifica che un valore genenerato da una computazione non รจ utilizzato, il codice associato a tale computazione verrร rimosso.
Dead-Variable Elimination
Spesso dopo loop optimization alcune variabili non verranno mai usate. In tal caso le dead variables saranno rimosse dal codice.
Common-Subexpression Elimination
Spesso Alcune computazioni lavorano con le stesse sottoespressioni, le quali possono essere calcolate una sola volta, salvate in memoria, e riutilizzate quando necessario. ร comunque presente un tradeoff che vede da un lato un incremento prestazionale, dall'altro una maggiore pressione sui registri.
Short Circuiting
Utilizzato su espressioni booleane, se il valore puรฒ essere determinato dal primo operando dell'espressione, la parte rimanente dell'espressione non verrร computata.
Procedure Call Transformation
Trasformazioni che effettuano una rimozione di chiamate oppure di overhead da queste ultime.
Leaf Procedure Optimization
Leaf procedures sono utili per ridurre l'utilizzo dello stack frame: non รจ necessario salvare l'indirizzo di ritorno sullo stack, ma quest'ultimo puรฒ essere salvato in un registro. Inoltre se non utilizza variabili, si puรฒ anche evitare di scrivere lo stack frame.
Cross-Call Register Allocation
Se una chiamata a procedura non utilizza i registri del caller, il callee puรฒ evitare di salvarli (modifica a livello di ABI).
Parameter Promotion
Quando un parametro รจ passato per riferimento, il calcolo dell'indirizzo รจ effettuato dal chiamante, ma la
loadรจ effettuata dal chiamato.Se il compilatore identifica correttamente i chiamanti di una leaf procedure, espanderร il loro stack frame per includere i dati di quest'ultima, che eviterร di effettuare una nuova allocazione sullo stack.
Procedure Inlining
Sostituisce una chiamata a procedura con una copia del corpo della procedura chiamata. Non puรฒ essere effettuata con chiamate ricorsive a meno che non abbia un numero limitato di passi.
ร utile per migliorare la precisione dell'analisi del compilatore, rimuovendo la necessitร di effettuare inter procedure analysis da parte del compilatore, e anche per migliorare le prestazioni riducendo il costo di modificare il flow di esecuzione del programma con un salto.
Uno svantaggio importante di questa trasformazione รจ un aumento nella dimensione del codice.
Procedure Cloning
Specializzazione ottimizzata di una procedura, ad esempio con diversi tipologie di parametri. Verrร generata una versione specializzata per ogni versione richiesta nel codice.
Loop Pushing
Simile ad hoisting, ma include una call. Sposta un loop innestato dal chiamante ad una versione clonata della procedura chiamata. In questo modo rimuove l'overhead della chiamata, e puรฒ permette la parallelizzazione dei cicli.
Tail Recursion Elimination
Ottimizzazione potente in linguaggi funzionali. Si ha una procedura che chiama se stessa e ritorna il valore della chiamata ricorsiva.
Questa trasformazione trasforma la ricorsione in un ciclo.
Meno comune per linguaggi imperativi.
Function Memoization
Memorizza il risultato di una chiamata a procedura, i queli verrano utilizzati in caso tale procedura venga chiamata nuovamente. Utile per funzioni particolarmente onerose (e.g. funzioni trigonometriche in alcuni casi).
Last updated