githubEdit

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:

  1. Decidere quale parte di programma da ottimizzare, e quale trasformazione applicare.

  2. Verificare che la trasformazione puรฒ essere applicata.

  3. 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

  1. Statement: solitamente espressioni aritmetiche

  2. Basic Block

  3. Innermost loop: parte del codice che รจ eseguita piรน di frequente, di conseguenza trasformazioni a questo livello sono molto efficaci dal punto di vista computazionale.

  4. Perfect loop nest: ogni ciclo contiene solo un ciclo innestato e non altre istruzioni, eccetto il loop piรน interno.

  5. General loop nest

  6. 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:

  1. Flow dependency: S4S_{4} ha una flow dependence con S3S_{3} (S3โ†’S4)\left.S_{3} \rightarrow S_{4}\right) quando S3S_{3} deve essere eseguita prima perchรจ scrive un valore che รจ letto da S4S_{4}.

  2. Antidependence: S6S_{6} una una antidependence con S5S_{5} (denoted by S5โ†›S6)\left.S_{5} \nrightarrow S_{6}\right) quando S6S_{6} scrive una variabile letta da S5S_{5}.

  3. Output dependence: S8S_{8} ha una output dependence con S7S_{7} (denoted by S7S_{7} ใ…‡ S8)\left.S_{8}\right) 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 dd elementi I=(i1,โ€ฆ,id)I = (i_1, \dots, i_d) dove ogni indice ipi_p 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 JJ dipende da un istruzione in una generica iterazione II se e solo se almeno un accesso ad una variabile รจ in scrittura e Iโ‰บJโˆงโˆ€p:fp(I)=gp(J)I \prec J \wedge \forall p: f_{p}(I)=g_{p}(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โ‡’YX \Rightarrow Y il distance vector รจ definito come:

    Yโˆ’X=(y1โˆ’x1,โ€ฆ,ydโˆ’xd)Y-X=\left(y_{1}-x_{1}, \ldots, y_{d}-x_{d}\right)

    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โ†’JI \rightarrow J come il vettore W=(w1,โ€ฆ,wd)W = (w_1, \dots, w_d), dove:

    wp={<ip<jp=ip=jp>ip>jpw_{p}= \begin{cases}< & i_{p}<j_{p} \\ = & i_{p}=j_{p} \\ > & i_{p}>j_{p}\end{cases}

Ottimizzazioni (47 in totale)

General Purpose

  1. 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.

    Expr
    Init
    Use
    Update

    c * 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

  2. 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.

  3. 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.

  4. 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.

  1. 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 se a รจ abbastanza piccola da essere contenuta in un registro.

    Scambiare i bound dei loop รจ semplice se lo spazio delle iterazioni รจ rettangolare.

  2. 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 ff) 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.

  3. 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.

  4. Strip Mining

    Modifica della granularitร  di un loop. Puรฒ essere utilizzata in ambito di vettorizzazione del codice.

    Ad esempio do i = 1, n divide n in 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:

  5. 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.

  6. 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.

  7. 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.

  8. Loop Fusion

    Inverso di loop distribution. Possibili miglioramenti:

    • Overhead minore

    • Miglior parallelismo

    • Miglior utilizzo di registri, operazioni vettoriali, data cache, TLB

    Per nn 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.

  1. Loop Unrolling

    Replicazione del corpo del ciclo per un certo numero di volte uu, chiamato unrolling factor, usando uu 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 uu.

    Nota che, se u>2u > 2, il loop epilogue รจ un loop e non un condizionale.

  2. 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:

  3. Loop Collapsing

    Versione specializzata di loop coalescing in cui viene ridotta la dimensionalitร  di array multidimensionali modificati da cicli innestati (entrambi stride-1).

  4. 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.

  5. Loop Normalization

    Modifica i loop in modo che l'iteratore inizi da 1 (o 0), e sia incrementata di 1 ad ogi iterazione.

  6. 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.

  1. 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)O(n) a O(logโกn)O(\log{n}).

  2. 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.

  3. 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.

  1. 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 pp rispetti il seguente vincolo: gcdโก(s+p,B)=1\gcd{(s + p, B)} = 1, con ss lo stride originario e BB il numero di banks.

    Gli svantaggi sono utilizzo di memoria maggiore, e conflitti con altre ottimizzazioni come ad esempio loop collapsing.

  2. 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.

  3. Array Contraction

    Riduzione di una delle dimensioni dell'array se questa non viene utilizzata all'interno di un ciclo innestato.

  4. 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.

  5. 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.

  6. 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.

  1. Constant Propagation

  2. Constant Folding

    Se un espressione contiene un operazione con valori costanti come operandi, il compilatore puรฒ sostuirla con il risultato.

  3. Copy Propagation

    Se alcune variabili sono riassegnate, il compilatore puรฒ propagare la variabile originale in modo da eliminare copie ridondanti.

  4. Forward Substitution

    Generalizzazione di Copy Propagation, utilizzo di una variabile รจ sostituito dalla sua definizione.

  5. Algebraic Simplification

    Il compilatore applica regole algebriche per ottimizzare alcune operazioni (e.g. โ‹…0,+0,โ‹…1,+1\cdot 0, + 0, \cdot 1, + 1)

  6. Strength Reduction

    Operatori costosi sono sostuiti con operatori meno costosi (e.g. xโ‹…2x \cdot 2 diventa diventa xโ‹…xx \cdot x, etc.). Il codice della funzione รจ ottimizzato e la chiamata originaria รจ sostituita con la nuova versione, che occupa piรน spazio ma รจ piรน efficiente.

  7. 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.

  1. 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โ‰ค0x \le 0 puรฒ diventare constโ‰ค0\text{const} \le 0 e di conseguenza uno dei branch del condizionale puรฒ diventare non raggiungibile.

  2. Useless-Code Elimination

    Se il compilatore verifica che un valore genenerato da una computazione non รจ utilizzato, il codice associato a tale computazione verrร  rimosso.

  3. Dead-Variable Elimination

    Spesso dopo loop optimization alcune variabili non verranno mai usate. In tal caso le dead variables saranno rimosse dal codice.

  4. 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.

  5. 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.

  1. 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.

  2. 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).

  3. 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.

  4. 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.

  5. 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.

  6. 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.

  7. 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.

  8. 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