Ottimizzazione dei loop - WinDizio

Transcript

Ottimizzazione dei loop - WinDizio
http://www.windizio.altervista.org/appunti/
File distribuito con licenza Creative Commons BY­NC­SA 2.5 IT
Copyright © 2007 ­ Michele Tartara
Parte I
Ottimizzazione dei loop
L'ottimizzazione dei loop è particolarmente importante perché sono le parti di codice in cui viene spesa la maggior parte del
tempo di esecuzione.
In generale, si darà la precedenza all'ottimizzazione dei loop innestati perchè sono quelli che verranno eseguiti più volte.
1 Denizioni di loop
Denizione 1 Un loop è un circuito sul grafo di controllo del usso
Denizione 2 Un insieme S di nodi del grafo di controllo del usso è un loop se esiste un h ∈ S chiamato testata (header )
tale che:
• da qualunque nodo di S c'è un cammino di archi orientati che conduce a h
• c'è un cammino di archi orientati da h a ogni nodo di S
• non ci sono archi da un nodo p esterno a S ad un qualunque nodo interno a S eccetto (eventualmente) p → h.
Denizione 3 (loop riducibile o naturale ) Se un nodo precede un'altro e ne è l'unico predecessore, questi nodi possono
essere fusi assieme (ridotti).
Un grafo tale che, applicando ripetutamente la riduzione, contiene come sottografo il grafo di Figura 1 si dice essere un grafo
di usso irriducibile.
Figura 1: Il prototipo di grafo che non da origine ad un loop (sottografo irriducibile)
Tale grafo è ottenibile solo tramite l'utilizzo manuale di istruzioni goto. Utilizzando i cicli classici (while, do-while, repeat-until,
for, ecc, comprese eventuali istruzioni break) si ha la certezza di non ottenerlo mai.
Un loop è una serie di istruzioni ripetute no al vericarsi di una condizione di terminazione tali che, applicando ripetutamente
la riduzione, il grafo risultante non è irriducibile.
1.1 Altre denizioni
Un nodo di ingresso di un loop è un nodo avente almeno un predecessore all'esterno del loop. Un nodo di uscita di un loop è un
nodo avente almeno un successore al di fuori del loop.
Un loop può avere più punti di uscita ma ha un solo ingresso.
1.2 Dominanza
La nozione di dominanza è utile per trovare i loop all'interno di un programma.
d domina n se e solo se per ogni cammino dal nodo di ingresso a n, questo cammino contiene d.
È conveniente assumere che questa relazione sia riessiva: ∀n, n domina n.
Proprietà della dominanza d domina n ⇔ ∀p ∈ pred(n) d domina p
1
http://www.windizio.altervista.org/appunti/
File distribuito con licenza Creative Commons BY­NC­SA 2.5 IT
Copyright © 2007 ­ Michele Tartara
1.2.1 Algoritmo per trovare i dominatori
Si consideri un nodo n con i predecessori p1 , . . . , pk e s0 il nodo iniziale del grafo.
Sia D[n] l'insieme di nodi che dominano n. Allora
D[s0 ] = {s0 } Ã
!
T
D[n] = {n} ∪
D[p] per n 6= s0 .
p∈pred[n]
Le equazioni possono essere risolte per iterazione inizializzando gli insiemi D[n] all'insieme di nodi del grafo (che verrà via
via ridotto per mezzo dell'intersezione).
L'ecienza dell'algoritmo può essere incrementata per mezzo di un ordinamento quasi-topologico dei nodi.
Tecnicamente, un nodo irraggiungibile è dominato da ogni nodo del grafo. Per evitare i problemi che questo potrebbe causare,
tali nodi vengono cancellati dal grafo prima di calcolare i dominatori ed applicare le ottimizzazioni ai loop.
1.2.2 Dominatori immediati
Teorema: In un grafo connesso, se d domina n, ed e domina n, allora o d domina e o e domina d.
Quindi, possiamo aermare che ogni nodo n non ha più di un dominatore immediato p = idom(n) tale che:
• p 6= n
(idom(n) non è il nodo n stesso)
• p dom n
(idom(n) domina n)
e
• @q : q 6= p ∧ q 6= n ∧ p dom q
(idom(n) non domina alcun altro dominatore di n)
Perciò, possiamo denire un albero dei dominatori disegnando un arco per ogni n un arco tra idom(n) ed n.
Un dominatore immediato di un nodo non è necessariamente il suo predecessore nel grafo di usso (potrebbe essere il primo
elemento comune di due diversi percorsi che portano a n).
Un arco nel grafo di usso da un nodo n ad un nodo h che domina n si chiama arco retroverso (back edge ).
Ogni volta che abbiamo un arco retroverso in verso opposto rispetto ad una dominanza, si ha un loop.
Applicando questa denizione, si ha la conferma che il grafo di Figura 1 non rappresenta un loop, perchè non ha archi
retroversi.
1.2.3 Loop
Il loop naturale (natural loop ) di un arco retroverso n → h dove h domina n è l'insieme di nodi x tali che h domina x e c'è un
cammino da x a n che non contiene h. La testata del loop sarà h.
Uno stesso nodo h può essere la testata di più cicli, nel caso in cui più di un arco retroverso termini in h.
Loop innestati Se A e B sono loop con testate, rispettivamente, a e b tali che a 6= b, e b è contenuto in A, allora i nodi di B
sono un sottoinsieme di quelli di A. Diciamo quindi che il loop B è innestato (nested ) in A, o che B è un loop interno (nested ).
Pre-testata di un loop (preheader ) Molte ottimizzazioni che agiscono sui loop hanno bisogno di spostare delle istruzioni
dall'interno del loop all'esterno, prima dell'esecuzione dello stesso.
Per avere un punto preciso in cui inserire tali istruzioni si inserisce una pre-testata p, inizialmente vuota, fuori dal loop con
un arco p → h. Tutti gli archi dai nodi x interni al loop continueranno a puntare a h, ma gli archi dai nodi y esterni al loop
verranno cambiati per puntare a p.
2 Ottimizzazioni
2.1 Calcoli loop-invariant
Il risultato di alcune espressioni è sempre lo stesso per qualunque iterazione di un ciclo (loop-invariant ). Non sempre è possibile
denire se una variabile calcolata è loop-invariant, perciò si attua un'approssimazione conservativa.
La denizione d : t ← a1 ⊕ a2 è un invariante di ciclo (loop-invariant ) all'interno di un loop se, per ogni operando ai :
2
http://www.windizio.altervista.org/appunti/
File distribuito con licenza Creative Commons BY­NC­SA 2.5 IT
Copyright © 2007 ­ Michele Tartara
• ai è costante
• oppure tutte le denizioni di ai che raggiungono d sono al di fuori del loop
• oppure solo una denizione di ai raggiunge d e quella denizione è loop-invariant.
2.1.1 Sollevamento del codice (code hoisting )
Se un'espressione è loop-invariant, è inutile ricalcolarla ogni volta: ha molto più senso sollevarla (hoist ) nella pre-testata per
calcolarla una volta sola ed utilizzarla poi quando necessario.
Bisogna però fare attenzione a rispettare alcune condizioni, pena l'ottenimento di un programma scorretto.
Si può applicare l'hoisting su d : t ← a ⊕ b se:
1. d domina tutte le uscite dal ciclo in cui t è live-out.
2. e c'è solo una denizione di t nel ciclo
3. e t non è live-out dalla pretestata
Quando si applica il code hoisting per più di una variabile, è importante inserirle nel pre-header nell'ordine in cui sono state
scoperte.
2.2 Variabili induttive di un loop (induction variables )
La terminologia variabili induttive è piuttosto vecchia: oggi si preferisce denirle trasformazioni ani (ane loop transformations ) o polyhedral transformation.
Vengono utilizzate per ridurre il numero di istruzioni all'interno di un ciclo e per trasformare alcune moltiplicazioni in somme
(computazionalmente più leggere). Ciò può essere fatto nel caso in cui, in un ciclo controllato dalla variabile i, venga calcolato
un valore k = i · c + d, dove c e d sono loop-invariant. Un tale valore k , infatti, può essere ottenuto sommando c · a (calcolato nel
pre-header) ogni volta che si somma a ad i.
L'algoritmo di sostituzione delle variabili induttive si divide in tre parti:
1. rilevamento delle variabili induttive
2. riduzione di forza (strength reduction ) - Sostituzione delle moltiplicazioni con la somma del valore precalcolato
3. eliminazione delle variabili induttive - Rimuove tutte le occorrenze di i.
Una variabile induttiva di base (basic induction variable ) è la variabile che viene utilizzata per gestire le ripetizioni di un ciclo.
Una variabile induttiva derivata è una variabile linearmente correlata ad una variabile induttiva di base, che è rappresentabile
tramite una tripla (i, a, b): i è il nome della variabile induttiva di base a cui si riferisce, a è il valore di inizializzazione (init
value ), b è il valore del passo di incremento (step value ).
Una variabile induttiva è lineare se cambia dello stesso valore ad ogni iterazione.
Rilevamento di una variabile induttiva Def: i è una variabile induttiva di base se e solo se esiste ed è unica una denizione
di i in L con una forma del tipo i ← i + c oppure i ← i − c, dove c è loop-invariant.
Def: k è una variabile induttiva derivata di L se e solo se:
1. esiste unica la denizione di k in L nella forma k ← j · c oppure k ← j + d dove j è una variabile induttiva e c e d sono
loop-invariant
2. e se j è una variabile induttiva derivata nella famiglia di i, allora
(a) la sola denizione di j che raggiunge k è quella nel ciclo
(b) e non ci sono denizioni di i su alcun cammino tra la denizione di j e la denizione di k .
3
http://www.windizio.altervista.org/appunti/
File distribuito con licenza Creative Commons BY­NC­SA 2.5 IT
Copyright © 2007 ­ Michele Tartara
Riduzione di forza (strength reduction ) Siccome su molte macchine l'addizione è meno costosa della moltiplicazione,
vorremmo riuscire a trasformare le varibili induttive derivate in addizioni.
1. Per ogni variabile induttiva derivata j ≡ (i, a, b): crea una nuova variabile j 0 appartenente alla famiglia di i
2. Dopo ogni assegnamento j ← i + c inserisci una nuova denizione j 0 ← j + c · b, dove c · b è un loop invariant (che può
quindi essere calcolato nel preheader)
3. Sostituisci l'assegnamento (unico) a j con j ← j 0
4. Al termine del preheader inizializza j 0 come j 0 ← a + i · b
Eliminazione Tutte le variabili inutili che rimangono al termine delle operazioni precedenti possono essere eliminate dal ciclo.
Una variabile è inutile (useless ) in un ciclo L se è morta a tutte le uscite da L e il suo solo uso è la ridenizione di se stessa.
Una variabile è quasi inutile se è usata solo nel confronto con valori loop-invariant e per ridenire se stessa. In tal caso, può
essere resa inutile modicando il confronto in modo tale da utilizzare la variabile induttiva correlata.
3 Controllo dei limiti degli array
Sia A un array [1..N]. Prima di poter accedere all'elemento A[i] dobbiamo vericare che 0 ≤ i ≤ N . In molti casi, i è una
variabile induttiva. Possiamo quindi pensare di integrare il controllo di validità con il controllo del ciclo. In tal modo risulterà
possibile eliminare buona parte dei controlli di validità espliciti, ottenendo un notevole guadagno di prestazioni.
Dato un ciclo L, una variabile induttiva j , un invariante di ciclo u e un'etichetta L2 esterna al ciclo, possiamo eliminare i
controlli dei limiti dell'array se:
1. L'istruzione s1 di controllo del loop assume la forma:
(a) if j < u goto L1 else goto L2
(b) if j ≥ u goto L2 else goto L1
(c) if u > j goto L1 else goto L2
(d) if u ≤ j goto L2 else goto L1
2. C'è un'istruzione s2 della forma:
if k <u n goto L3 else goto L4
j−aj
k
dove k è una variabile induttiva coordinata con j , cioè per cui vale k−a
(vale quasi ovunque) e s1 domina s2 . La
bk = bj
dominanza è necessaria perchè ci assicura che quando dovrei eseguire il test sull'indice dell'array, ho già eseguito almeno
quello sulla variabile di controllo del ciclo.
3. Non ci sono cicli innestati in L che contengano una denizione di k
4. k cresce con j
Trasformazione Se le condizioni sono soddisfatte, applichiamo la trasformazione.
Vogliamo garantire che sia sempre valido: k ≥ 0 ∧ k < n
Per garantire che k ≥ 0, inserisco alla ne della pre-testata il controllo: k ≥ 0 ∧ ∆k1 ≥ 0 ∧ . . . ∧ ∆km ≥ 0, dove i vari ∆ki
sono i valori (loop-invariant) che vengono sommati a k all'interno del ciclo.
b
Per garantire che k < n in s2 , è suciente assicurarsi che, in s1 , valga j < bkj (n − (∆0 k1 + . . . + ∆0 kp ) − ak ) + aj , dove i
0
∆ ki sono i valori loop-invariant che sono sommati a k lungo qualsiasi percorso tra s1 e s2 che non ripassa da s1 . Tutto ciò che
compare in questo controllo è loop-invariant, quindi il controllo stesso può essere spostato nella pre-testata ed essere calcolato
una volta sola.
4
http://www.windizio.altervista.org/appunti/
File distribuito con licenza Creative Commons BY­NC­SA 2.5 IT
Copyright © 2007 ­ Michele Tartara
4 Srotolamento dei cicli (loop unrolling )
Quando un ciclo è eseguito un numero elevato di volte ed il body è molto breve, è utile srotolarlo, cioè copiare più volte il codice
del corpo del ciclo, per ridurre l'inuenza (altrimenti molto elevata) delle istruzioni di controllo.
Lo srotolamento non viene eseguito tante volte quante solo le iterazioni del ciclo (aumenterebbero troppo le dimensioni del
codice) ma solo due o tre volte, dimezzando o riducendo ad un terzo il numero di test e di salti. Avere un body più lungo
permette anche di sfruttare meglio la pipeline, aumentando così il parallelismo a livello hardware.
Copiare l'intero ciclo limitandosi a cambiare la destinazione delle istruzioni di salto non è una buona soluzione (mantiene la
suddivisione in blocchi basici e non porta alcun miglioramento). Si può fare di meglio sfruttando le variabili induttive:
1. Cerca una variabile induttiva i tale che ogni incremento p : i ← i + ∆1 di i domina ogni arco retroverso del ciclo.
2. Se ci sono più incrementi di i li aggrego in modo di poterli calcolare in un unico passaggio
Tuttavia, con queste trasformazioni il programma potrà eseguire solo un numero pari di iterazioni. Per mantenere le
funzionalità del programma originale bisogna aggiungere in coda un epilogo con un'ulteriore copia del codice del corpo
del ciclo da eseguire quando necesario.
Analogamente, nel caso di uno srotolamento di fattore 10 (10 copie del ciclo), bisogna aggiungere un epilogo, che sarà un'unica
copia del body eseguita da 0 a 9 volte, a seconda dei casi. È inutile inserire anche srotolamenti di grado 9, 8 ,7, ecc, perchè il
guadagno prestazionale non giusticherebbe un tale aumento di codice.
Inlining Un approccio parzialmente simile allo srotolamento è l'inlining, che consiste nel copiare il codice di una funzione
all'interno del chiamante invece di gestire la chiamata della stessa. Ciò migliora le prestazioni.
Quando si applica l'inlining, esso è una delle prime operazioni da fare, perchè permette di attuare molte ottimizzazzioni
successive.
5