Optymalizacja lokalna

Transkrypt

Optymalizacja lokalna
AGT – optymalizacja kodu pośredniego w bokach podstawowych
1
Wykład 8.
Optymalizacja lokalna kodu1
Optymalizacja kodu jest kolejnym etapem procesu kompilacji, realizowanym
najczęściej po wygenerowaniu kodu w formie pośredniej. Proces ten polega na
modyfikacji kodu pośredniego, która polega na przestawianiu i zmianie operacji w
celu utworzenia bardziej efektywnego kodu wynikowego.
Optymalizacja kodu moŜe się odbywać na wielu etapach kompilacji, oprócz
optymalizacji kodu pośredniego moŜna wykonywać optymalizację na poziomie kodu
wynikowego, która jest w duŜym stopniu zaleŜna od języka wynikowego.
Zakres optymalizacji wykonywanej przez kompilator zaleŜy do wielu czynników.
Trzeba podkreślić, Ŝe nie wszystkie kompilatory powinny wykonywać kompletną
optymalizację. W przypadku gdy kompilator słuŜy przede wszystkim do sprawdzania
poprawności programów nie powinien uŜywać wyrafinowanych metod, poniewaŜ one
istotnie zmieniają kolejność operacji, a to utrudnia wyszukiwanie błędów.
W
dalszym
ciągu
zostaną
przedstawione
cztery
podstawowe
metody
optymalizacji kodu pośredniego, które są stosowane we prawie wszystkich znanych
językach programowania.
Są to następujące metody:
1. Składania, to znaczy wykonania w czasie tłumaczenia tych operacji,
których argumenty są juŜ w tedy znane.
2. Usuwania operacji zbędnych (przede wszystkim wydzielania wspólnych
podwyraŜeń).
3. Przenoszenia na zewnątrz pętli operacji, których argumenty nie
zmieniają się w pętli.
4. Zmniejszenie siły mnoŜeń w pętlach (prowadzi do zamiany mnoŜeń na
dodawania).
AGT – optymalizacja kodu pośredniego w bokach podstawowych
2
Do ilustracji metod optymalizacji będziemy uŜywali czwórek lub trójek, w
zaleŜności od tego, która z postaci jest wygodniejsza w danym momencie.
8.1 Optymalizacja wewnątrz bloków podstawowych.
Pierwsze dwie metody optymalizacji wykonywane są w blokach podstawowych,
przy czym blokiem podstawowym jest ciąg kolejnych operacji z tylko jednym
wejściem i wyjściem, którymi są odpowiednio pierwsza i ostatnia operacje.
8.1.1 Składanie
Jak wspomniano, składanie polega na wykonaniu w czasie tłumaczenia tych
operacji, dla których znane są wartości argumentów, a więc dla tych operacji nie
będzie generowany kod wynikowy.
RozwaŜmy fragment kodu:
Rys. Ilustracja składania: (a) instrukcje programu, (b) trójki,
(c) trójki po optymalizacji
(a)
1
(b)
(c)
I := 1 + 1;
(1)
(+, 1, 1)
(1) (:=, 2, I)
I := 3;
(2)
(:=, (1), I)
(2) (:=, 3, I)
B := 6.2 + I;
(3)
(:=, 3, I)
(3) (:=, 9.2, B)
(4)
(CVIR, 0, I)
(5)
(+, 6.2, (4))
(6)
(:=, (5), B)
D. Gries, ‘Konstrukcja kompilatorów dla maszyn cyfrowych’, PWN
AGT – optymalizacja kodu pośredniego w bokach podstawowych
3
Trójka (1) moŜe być oczywiście wykonana i zastąpiona odpowiednią stałą. MoŜna
równieŜ wykonać trójkę (4), a to pociąga za sobą wykonanie trójki (5). Wynik
składania przedstawia rys. (c).
Składania wykonuje się głównie dla:
•
operacji arytmetycznych: +, -, *, /
•
operacji konwersji: CVIR, CVRI
•
operacje inicjalizacji zmiennych.
Przed podaniem algorytmu składania załoŜymy, Ŝe kod pośredni jest pamiętany
w strukturze postaci:
Tr:
array[1..MaxLen] of record
op: typ_operacji;
op1, op2: typ_argumentu
end;
Algorytm będzie korzystał z dodatkowej tablicy table, która w czasie składania
będzie zawierać pary (A, k) wszystkich zmiennych prostych A o znanej wartości
równej k.
Tablica ta ma postać:
Table: array[1 .. Max] of record
name: string;
k: type
end;
W poniŜszym algorytmie złoŜone trójki, będą zastępowane nowymi trójkami
(empty, k, 0), gdzie empty jest nowym operatorem, dla którego nie generuje się
kodu wynikowego, natomiast k jest obliczoną wartością zastąpionej trójki.
Algorytm składania przetwarza kolejne trójki bloku podstawowego w
następujący sposób:
(1) for j:=1 to 2 do if Tr[i].op j = A and (A, k) ∈ table then Tr[i].op j = k
(Jeśli argument trójki jest zmienną z tablicy table, to zastępuje się ten argument
odpowiednią wartością k.)
(2) for j := 1 to 2 do if Tr[i].op j =’(n)’ and (n) ≡(empty, k, 0) then Tr[i].op j = k
(Jeśli argument trójki jest odwołaniem do trójki postaci (empty, k, 0) to
odwołanie to zastępuje się wartością k)
AGT – optymalizacja kodu pośredniego w bokach podstawowych
4
(3) Jeśli wszystkie argumenty trójki są stałymi i jeśli operacja moŜe być złoŜona, to
wykonaj daną trójkę i zastąp ją trójką postaci (empty, k, 0), gdzie k jest
obliczoną wartością zastąpionej trójki.
(4) Jeśli dana trójka jest podstawieniem A := B, gdzie A nie jest zmienną
indeksowaną, to:
• Jeśli B jest stałą, to do tablicy table dołącza się parę (A, B), usuwając z tablicy
poprzednią parę,( jeśli taka była) której pierwszym elementem jest A.
•
Jeśli B nie jest stałą i w tablicy table występuje para (A, k), to usuwa się ją z
tablicy.
Na poniŜszym rysunku przedstawiono działanie algorytmu składania dla
podanego programu.
(1) (+, 1, 1)
(7)
(1) (empty, 2, 0)
(1)
(empty, 2, 0)
(2) (:=, (1), I)
(8)
(2) (:=, 2, I)
(2)
(:=, (1), I)
(3) (:=, 3, I)
(9)
(3) (:=, 3, I)
(3)
(:=, 3, I)
(4) (CVIR, 0, I)
(4) (CVIR, 0, I)
(4)
(empty, 0, I)
(5) (+, 6.2, (4))
(5) (+, 6.2, (4))
(5)
(empty, 9.2, 0)
(6) (:=, (5), B)
(6) (:=, (5), B)
(6)
(:=, 9.2, B)
table:
(I, 2)
(I,3).
(I, 3)
(B, 9.2)
W czasie składania musimy sprawdzać czy wykonanie operacji jest moŜliwe,
np. przypadku próby wykonania operacji, np. (/, 1, 0) – kompilator musi wykryć, Ŝe
dzielnik jest zerem.
MoŜliwe jest takŜe wykonanie składania w procedurach semantycznych,
generujących kod pośredni, nie ma wtedy oddzielnego przebiegu.
Przykładowo w czasie wykonania produkcji:
E1 E2 + T
naleŜy sprawdzić własności semantyczne symboli E2 i T, jeśli oba symbole są stałymi
lub znane są ich wartości,
to w procedurze dodaje się te wartości, a otrzymany
wynik jest związany z symbolem E1.
AGT – optymalizacja kodu pośredniego w bokach podstawowych
5
8.1.2 Usuwanie zbędnych operacji.
W bloku podstawowym i-ta operacja jest zbędna, jeśli istnieje wcześniejsza
identyczna operacja j, oraz Ŝadna z operacji pośrednich (pomiędzy j-tą a i-tą) nie
zmienia wartości zmiennych, od których zaleŜy i-ta operacja.
Na przykład, dla instrukcji: D := D + C * B; A := D + C * B ; C := D + C * B;
Blok podstawowy trójek ma postać:
(1) (*, C, B)
(4) (*, C, B)
(7) (*, C, B)
(2) (+, D, (1))
(5) (+, D, (4))
(8) (+, D, (7))
(3) (:=, (2), D)
(6) (:=, (5), A)
(9) (:=, (8), C)
Operacje (4) i (7) są zbędne, poniewaŜ po trójce (1) nie zmienia się Ŝadna
z wartości C i B. MoŜna takŜe zauwaŜyć, Ŝe trójka (8) teŜ jest zbędna, gdyŜ istnieje
wcześniejsza trójka (5), natomiast trójka (5) nie jest zbędna, chociaŜ istnieje
wcześniej podobna trójka (2), ale trójka (3) zmienia wartość zmiennej D.
Tworząc programy programiści zwykle unikają zbędnych operacji, ale operacje
zbędne powstają najczęściej przy odwołaniach do zmiennych indeksowych, na które
programista nie ma wpływu.
Na przykład dla instrukcji podstawienia: X[i, j] := X[i, j+1] zostanie
wygenerowany następujący blok trójek:
(1) (*, i, d2)
(3) (*, i, d2)
(5) (+, (4), 1)
(2) (+, (1), j)
(4) (+, (3), j)
(6) (:=, X(5), X(2))
Trójki (3) i (4) są oczywiście zbędne i moŜemy je usunąć, otrzymując:
(1) (*, i, d2)
(2) (+, (1),j)
(3) (+, (2), 1)
(4) (:=, X[(3)], X[(2)])
Algorytm usuwania operacji zbędnych sprawdza trójki w kolejności ich
występowania. Jeśli i-ta trójka jest zbędna (poniewaŜ istnieje odpowiednia trójka
AGT – optymalizacja kodu pośredniego w bokach podstawowych
6
j-ta) to zastępuje ją trójką postaci (empty, j, 0), dla której nie generowany jest kod
wynikowy.
Aby efektywnie sprawdzać czy dana trójka jest zbędna zdefiniujemy tzw.
stopnie zaleŜności – (Dep) dla trójek i zmiennych.
Definicja
1.
Dep(A) = 0 ( początkowo Dep dla zmiennych w bloku jest równa 0)
2.
Jeśli i-ta trójka jest postaci (:=, w, A) (A := w) to Dep(A) = i
3.
Dep(i) = 1 + maxj=1,2 {Dep(Tr[i].opj)}
Fakt.
Jeśli i-ta trójka jest identyczna z j-tą trójką, to i-ta trójka jest zbędna wtedy i tylko
wtedy gdy Dep(i) = Dep(j).
Algorytm usuwania zbędnych operacji analizuje kolejno kaŜdą trójkę w następujący
sposób:
1.
for j:= 1 to 2 do
if Tr[i].opj = (n) and (n) = (empty, j, 0) then Tr[i].opj=(j)
(Jeśli argument trójki jest odwołaniem do trójki postaci (empty, j, 0), to
zmienia się go na odwołanie do j-tej trójki.)
2.
Oblicz Dep(i) = 1+maxj=1,2{Dep(Tr[i].opj)}
3.
Jeśli istnieje identyczna j-ta trójka, taka, Ŝe j<i oraz Dep(i)=Dep(j) to i-ta
trójka jest zbędna; zastąp ją trójką postaci (empty, j, 0). Sprawdzenie
wykonuje się przez porównanie z trójkami o numerach i-1, i-2, ... , 1.
4.
Jeśli i-ta trójka nadaje wartość zmiennej prostej, elementowi lub całej
tablicy B, to wartość Dep(B) zmień na numer trójki i.
Stopień zaleŜności trójki, której argumentem jest zmienna indeksowa B[T],
zaleŜy od stopni zaleŜności B i T. Po drugie, jeśli w i-tej trójce występuje
podstawienie pod zmienną B[T], to wartość Dep(B) zmienia się na i. W ten sposób
następne odwołanie do tablicy B zaleŜy od tej trójki.
Na poniŜszym rysunku pokazano kolejne kroki działania algorytmu usuwania
operacji zbędnych w przykładowym bloku.
AGT – optymalizacja kodu pośredniego w bokach podstawowych
i-ta trójka
Dep(X)
Dep(i)
7
Przekształcona trójka
A B C D
(1) (*, C, B)
0 0 0 0
1
(1) (*, C, B)
(2) (+, D, (1))
0 0 0 0
2
(2) (+, D, (1))
(3) (:=, (2), D)
0 0 0 0
3
(3) (:=, (2), D)
(4) (*, C, B)
0 0 0 3
1
(4) (empty, 1, 0)
(5) (+, D, (4))
0 0 0 3
4
(5) (+, D, (1))
(6) (:=, (5), A)
0 0 0 3
5
(6) (:=, (5), A)
(7) (*, C, B)
6 0 0 3
1
(7) (empty, 1, 0)
(8) (+, D, (7))
6 0 0 3
4
(8) (empty, 5, 0)
(9) (:=, (8), C)
6 0 0 3
5
(9) (:=, (5), C)
6 0 9 3
Podane algorytmy nie są optymalne, uwzględniając przemienność niektórych
operatorów moŜemy wykonać duŜo lepszą optymalizację.
Na przykład, operacje: (*, A, B) i (*, B, A) naleŜy uwaŜać za identyczne. Aby to
zrobić systematycznie, musimy uporządkować wszystkie argumenty wielokrotnego
dodawania (lub mnoŜenia) w następującej kolejności:
1. Najpierw składniki, które nie są zmiennymi lub stałymi
2. Następnie zmienne indeksowe w porządku leksykograficznym
3. Dalej zmienne proste w porządku leksykograficznym
4. Na końcu stałe.
Na przykład po zmianie kolejności składników w instrukcjach:
A := 1 + B + C + 2;
B := C + B + 6;
A := B + C + 1 + 2;
B := B + C + 6;
otrzymamy
co pozwala złoŜyć operacje 1+2 i usunąć dodawanie B+C.
Nie jest to jednak kompletne rozwiązanie problemu, jeśli bowiem zmienimy
kolejność argumentów w instrukcji A := B+C+B+C otrzymamy A := B+B+C+C, a to
nie pomaga w wykryciu, Ŝe wyraŜenie B+C moŜe być obliczane tylko raz.
W praktyce problem wyszukiwania wszystkich wspólnych podwyraŜeń jest
dość złoŜony.
AGT – optymalizacja kodu pośredniego w bokach podstawowych
8
Równocześnie moŜna takŜe przenosić jednoargumentowy minus i operator
ABS. MoŜe to prowadzić nie tylko do zmniejszenia działań jednoargumentowych, ale
takŜe przyczynić się do zwiększenia liczby operacji zbędnych.
Na przykład, jeśli apostrofem oznaczymy jednoargumentowy minus, to trójki
dla instrukcji C := A – B ; D := B – A; mogą mieć postać:
Przed zmianą
Po zmianie
(1) (+, A, B’)
(1) (+, A, B’)
(2) (:=, (1), C)
(2) (:=, (1), C)
(3) (+, B, A’)
(3) (+, A, B’)
(4) (:=, (3), D)
(4) (:=, (3)’, D)
w której występują dwie identyczne operacje.
Nawet wtedy, gdy nie usuwa się zbędnych operacji, uŜywa się często pola
reprezentującego zmianę znaku argumentu w tym celu, aby skrócić przekład
programu dla maszyn, w których są zrealizowane rozkazy ‘pamiętania ze zmianą
znaku’ i ‘pobierania ze zmianą znaku’.
8.2 Optymalizacja pętli.
Efektywna optymalizacja pętli polega na przesunięciu poza pętlę operacji
niezmienniczych, czyli takich których argumenty nie zaleŜą od Ŝadnej zmiennej,
której wartość zmienia się wewnątrz pętli.
Drugi typ optymalizacji pętli, zwany zmniejszeniem siły operacji, polega na
zastępowaniu pewnych operacji przez inne szybsze operacje. Zamiany dokonuje się
głównie mnoŜeń I * K, gdzie I jest zmienną sterującą pętli, na dodawanie.
Dla ilustracji rozwaŜmy uogólnioną postać pętli:
for I := A step B until C do
… T1 := I * K …
od;
AGT – optymalizacja kodu pośredniego w bokach podstawowych
9
gdzie K jest niezmiennikiem pętli. Zmiennej I nadaje się wartość początkową,
równą A, a wewnątrz pętli jedyną instrukcja podstawienia pod I jest zwiększenie I,
postaci I := I + B.
ZauwaŜmy, Ŝe ilekroć I zmienia się o wartość B, wartość T1 zmienia się o B*K,
mamy bowiem:
T1 := (I+B)*K = I*K + B*K =T1 + B*K.
Dlatego, jeśli zmienna T1 nie jest zmieniana w innym miejscu pętli , moŜemy
zmniejszyć siłę operacji I*K zmieniając pętlę w następujący sposób:
1.
Przed pętlę dołączamy operacje: T1 := A * K; T2 := B * K, gdzie T2 nowa
zmienna robocza. W ten sposób nadajemy wartość początkową zmiennej T1
i obliczamy przyrost T2=B*K
2.
Z pętli usuwamy operację T1 := I * K
3.
Na końcu pętli dołączamy operację T1 := T1 + T2;
W wyniku tych operacji zastąpiliśmy mnoŜenie dodawaniem i otrzymaliśmy
pętle postaci:
T1 := A * K;
T2 := B * K;
for I := A step B until C do
…
T1 := T1 + T2
od;
ZałoŜyliśmy oczywiście, Ŝe zmienne A i B są niezmiennikami pętli, a zmienna I
wewnątrz pętli występuje tylko w wyniku dodania kroku.
Istotnym załoŜeniem jest takŜe fakt, Ŝe K jest typu całkowitego, w przypadku
gdy K jest typu rzeczywistego nie moŜna zmniejszać mnoŜenia, gdyŜ wielokrotne
dodawanie
nie
jest
zmiennoprzecinkowych.
dokładne
Mówiąc
z
ogólnie,
powodu
wartość
dokładniejsza niŜ wartość K + K + ... + K (I razy).
błędów
wyraŜenia
zaokrągleń
I*K
jest
liczb
duŜo
AGT – optymalizacja kodu pośredniego w bokach podstawowych
10
Dodajmy, Ŝe zmniejszanie siły mnoŜenia nie zawsze prowadzi do optymalizacji,
w niektórych przypadkach moŜe wydłuŜyć działanie programu (moŜe się zwiększyć
liczba operacji dodawania).
Aby dokonać optymalizacji pętli
musimy podczas jej analizy zebrać pewne
informacje, które pozwolą wyznaczyć niezmienniki pętli oraz zweryfikować warunki
konieczne do optymalizacji.
Proces optymalizacji realizowany jest w trzech przebiegach:
1. Sprawdzenie pętli
2. Przetwarzanie operacji niezmienniczych
3. Zmniejszanie siły operatorów.
Pierwszy z przebiegów jest realizowany w czasie analizy pętli, tworzy potrzebne
później informacje, sprawdza własności optymalizacyjne pętli i analizuje czy dana
pętla moŜe być optymalizowana. Drugi przebieg przesuwa na zewnątrz pętli operacje
niezmiennicze w nich. Ostatni przebieg zmniejsza siłę operatorów.
W dalszym ciągu załoŜymy, Ŝe rozpatrywać będziemy uogólnioną postać pętli:
for I := A step B until C do
(*)
<instrukcja sterowania>
od;
gdzie: A, B, C - są dowolnymi wyraŜeniami typu całkowitego, przy czym B jest
zawsze dodatnie.
Zakładamy ponadto, Ŝe postać pośrednia pętli w czwórkach ma następującą
strukturę:
Przed optymalizacją pętli:
Init:
I := A;
Po optymalizacji pętli:
Init: I := A; ‘operacje inicjujące’ ;
Test: if I > C then goto Over;
Test: if I > C then goto Over;
Loop: <ciało pętli>;
Loop: <zmienione ciało pętli>;
Incr: I := I + B ;
Incr: I := I + B ; ‘ operacje zwiększania’ ;
goto Test;
goto Test;
Over:
Over:
AGT – optymalizacja kodu pośredniego w bokach podstawowych
11
Część nowych operacji będzie wykonywana bezpośrednio przed właściwą
pętlą, pozostałe są wstawiane tak, aby były wykonywane wraz z kaŜdym
zwiększaniem zmiennej sterującej. Wiersz poprzedzony etykietą Init nazywamy
blokiem INIT, a wiersz poprzedzony etykietą Incr – blokiem INCR. Są to bloki
podstawowe.
Przebiegi wykonujące optymalizację dla kaŜdej pętli tworzą trzy oddzielne
tablice:
•
Tablica zawierająca informacje o pętli:
- zmienną sterującą,
- numer pętli zewnętrznej
- wskaźnik do wykazu zmiennych występujących w wyraŜeniu B
- wskaźnik do wykazu zmiennych występujących w pętli
- wskaźniki do bloków INIT i INCR
•
Tablica zawierająca operacje bloku INIT
•
Tablica zawierająca operacje bloku INCR
Dodajmy, Ŝe blok INIT po optymalizacji danej pętli moŜe być dalej optymalizowany
jako część pętli zewnętrznej.
8.2.1 Ograniczenia pętli
Aby wykonywać oba rodzaje optymalizacji pętli (*), muszą być spełnione
następujące warunki:
1. Zmienna sterująca I musi być zmienną typu całkowitego.
2. WyraŜenie B musi być typu całkowitego i nie moŜe zawierać zmiennej
sterującej I.
3. Instrukcja sterowania pętli nie moŜe zawierać podstawienia pod zmienną I ani
pod zmienne występujące w wyraŜeniu B.
AGT – optymalizacja kodu pośredniego w bokach podstawowych
12
8.2.2 Analiza operacji niezmienniczych pętli
Jak wspomniano operacje występujące w pętli, której argumenty nie zaleŜą od
zmiennych modyfikowanych w wewnątrz pętli moŜna często przesunąć z pętli do
bloku INIT. Najczęściej analizowane są tylko operacje zawierające operatory: +, -, *,
Round i CVRI. W tym kroku zostaną pominięte operacje podstawienia, poniewaŜ
wymagają one dokładniejszej analizy.
Zakładamy, Ŝe zmiennym roboczym Ti nadaje się wartość tylko raz i moŜna je
odróŜnić od zmiennych programu źródłowego.
Po napotkaniu operacji, którą być moŜe moŜna przesunąć wykonuje się
następujące kroki:
1. ZałóŜmy, Ŝe operacja ma postać (*, A, B, T1). Jeśli jej argumenty A lub B
zmieniają się wewnątrz pętli to pomijamy następne kroki.
2.
Jeśli operacja (*, A, B, T2) jest juŜ w bloku INIT to przejdź do kroku 4, w
przeciwnym razie przejdź do kroku 3.
3.
Dołącz operację do bloku INIT i przejdź do kroku 5.
4.
Zmień występujące dalej w pętli odwołania do T1 na odwołania do T2.
5.
Usuń operację z pętli oraz usuń T1 z wykazu zmiennych, których wartość
zmienia się w pętli (ale nie z pętli zewnętrzych).
W czasie analizy pętli mogliśmy przesunąć część operacji do bloku INIT. Blok
ten znajduje się na zewnątrz pętli, ale w środku pętli zewnętrznej. Po zakończeniu
analizy pętli naleŜy więc przejrzeć jej blok INIT, jako część pętli zewnętrznej, co
moŜe spowodować dalsze przesunięcie niektórych operacji.
8.2.3 Przebieg zmniejszający siłę operatorów.
W ostatnim przebiegu optymalizacyjnym przegląda się kolejne operacje
wszystkich pętli. Po napotkaniu operacji postaci (*, I, K, T1) lub (*, K, I, T1)
zastępuje się ją operacją pustą (empty, 0, 0, 0) i wykonuje następujące kroki:
AGT – optymalizacja kodu pośredniego w bokach podstawowych
1.
13
Jeśli w bloku INIT znajduje się operacja (*, I, K, T3) to juŜ wcześniej
zmniejszono siłę identycznej operacji. KaŜde następne odwołanie do T1 w
wewnątrz pętli zamień na odwołanie do T3 i pomiń następne kroki.
2.
Do bloku INIT dołącz operacje (*, I, K, T1).
3.
Jeśli w bloku INIT nie ma operacji (*, B, K, T2) lub (*, K, B, T2) to utwórz
nową zmienną roboczą T2 i dołącz do bloku INIT operacje (*, B, K, T2)
(taka operacja mogła być dołączona w przebiegu analizującym operacje
niezmiennicze).
4.
Do bloku INCR dołącz operacje (+, T1, T2, T1)
PoniŜej zilustrowano powyŜsze przebiegi optymalizacji dla programu:
....
for I := 1 step 1 until N do
A[I, J] := A[I, J] + A[J, I]
od;
przy czym: (a) zawiera program przed optymalizacją, (b) – program po przesunięciu
operacji niezmienniczych, (c) – program po zmniejszeniu siły operatorów.
(a)
INIT: I := 1 ;
TEST: if I > N
LOOP: T1:= I * d2;
T2:= T1 + J;
T3:= J * d2;
T4:= T3 + I;
T5:= A[T2]+A[T4];
A[T2]:= T5;
(b)
INIT: I := 1 ;
T3:= J * d2;
TEST: if I > N
LOOP: T1:= I * d2;
T2:= T1 + J;
T4:= T3 + I;
T5:= A[T2]+A[T4];
A[T2]:= T5;
INCR: I := I + 1
INCR: I := I + 1
GOTO TEST
OVER:
OVER:
GOTO TEST
(c)
INIT: I := 1 ;
T3:= J * d2;
T1:= I * d2;
TI:= 1 * d2;
TEST: if I > N
LOOP:
T2:= T1 + J;
T4:= T3 + I;
T5:= A[T2]+A[T4];
A[T2]:= T5;
INCR: I := I + 1
T1:= T1 + TI;
GOTO TEST
OVER:
AGT – optymalizacja kodu pośredniego w bokach podstawowych
14
ZauwaŜmy, Ŝe waŜnym jest, aby najpierw wykonać analizę operacji niezmienniczych.
RozwaŜmy na przykład wyraŜenie (X+Y)*I, gdzie X i Y są niezmiennikami pętli.
Postać pośrednią tego wyraŜenia jest następująca:
(1) (+, X, Y, T1)
(2) (*, T1, I, T2)
W tym przypadku nie moŜna zmniejszyć siły mnoŜenia, poniewaŜ T1 nie jest niezmiennikiem
pętli. Jeśli jednak w wyniku przesuwania pierwsza czwórka zostanie przesunięta do bloku
INIT, to zmienna T1 stanie się niezmiennikiem pętli.
8.2.4 Rozszerzenie zmniejszania siły operatorów
Opisane metody optymalizacji zawodzą dla wyraŜeń typu (I+K)*X, gdzie I jest
zmienną sterującą, a K i X są niezmiennikami pętli. W tym przypadku moŜna
zastosować rozszerzoną metodę zmniejszania siły operatorów. Stosuje się wówczas
następujące kroki.
Po pierwsze, zmniejszamy siłę dodawania T1 := I+K w następujący sposób:
1. W bloku INIT umieść operację T1 := I+K
2. W bloku INCR umieść operację T1 := T1 + B (jeśli wartość I zmienia się o B,
to tak samo zmienia się T1)
3. Usuń operację T1 := I+K.
Po drugie, zmniejszając siłę operatorów zmieniamy takŜe inne, rekursywnie
zdefiniowane zmienne. Odpowiednimi kandydatami są zmienne robocze analogiczne
do T1, które są rekursywne na skutek wcześniejszego zmniejszania siły operatorów.
RozwaŜmy przykład optymalizacji czwórek odpowiadających instrukcji:
D := (I * X + Y) * Z,
występującej w wewnątrz pętli for I := A step B until C.
Optymalizujemy wszystkie trzy operatory, generując w kaŜdym kroku nową,
rekursywnie zdefiniowaną zmienną.
Podajemy tylko bloki INIT, INCR i LOOP.
AGT – optymalizacja kodu pośredniego w bokach podstawowych
15
(a) eliminacja I*X
(b)eliminacja T1+Y
(c) eliminacja T2*Z
(d) postać końcowa
INIT: I := A;
INIT: I := A;
T1 := I*X;
T4 := B*X;
INIT: I := A;
T1 := I*X;
T4 := B*X;
T2 := T1+Y;
INIT: I := A;
T1 := I*X;
T4 := B*X;
T2 := T1+Y;
T3 := T2 * Z;
T5 := T4 * Z;
LOOP: T1 := I*X;
T2 := T1 + Y;
T3 := T2 * Z;
D := T3;
LOOP:
LOOP:
LOOP:
INCR: I := I + B
INCR: I := I + B
T1 := T1 + T4
T2 := T1+Y;
T3 := T2 * Z;
D := T3;
T3 := T2 * Z;
D := T3;
INCR: I := I + B
T1 := T1 + T4
T2 := T2 + T4
D := T3;
INCR: I := I + B
T1 := T1 + T4
T2 := T2 + T4
T3 := T3 + T5
ZauwaŜmy na koniec, Ŝe wraz z postępem optymalizacji otrzymujemy w bloku
INIT wiele niepotrzebnych czwórek, które powinny być usunięte. W (d) nie ma
odwołań do zmiennych T1 i T2, a więc moŜna usunąć podstawienia: T1 := T2+T4 i
T2 :=T2+T4
8.3
Optymalizacja w mniejszej liczbie przebiegów
Jak wspomniano wcześniej optymalizację moŜna wykonać za pomocą trzech
dodatkowych przebiegów. Okazuje się jednak, Ŝe przebieg analizy pętli moŜna zwykle
połączyć z procedurami semantycznymi generującymi postać pośrednią programu.
Natomiast jeśli zmienimy strukturę generowanego kodu dla pętli:
for I := A step B until C do
< instrukcje wewnętrzne pętli>
od;
AGT – optymalizacja kodu pośredniego w bokach podstawowych
tak aby analiza bloku INIT
16
mogła odbywać się dopiero po zakończeniu analizy
instrukcji wewnętrznych pętli, tzn.
goto INIT ;
TEST: if
I > C then
goto OVER;
LOOP: < nowe instrukcje wewnętrzne pętli >
INCR: I := I + B; ‘ nowe operacje zwiększania ‘;
goto TEST;
INIT: I := A ; ‘ nowe operacje inicjujące ‘
goto TEST;
OVER:
Wówczas moŜliwe jest połączenie procesu optymalizacji z generowaniem kodu
wynikowego programu.
Program zawiera dwa dodatkowe skoki, ale dzięki temu moŜemy równocześnie
optymalizować instrukcję wewnętrzne pętli i generować jej kod wynikowy.
Generowanie kodu wynikowego dla bloków INCR i INIT realizujemy później.
Postać pośrednią programu analizujemy pobierając kolejne rozkazy z trzech
źródeł:
•
tablicy zawierającej kod pośredni programu głównego,
•
tablic kodu dla bloków INCR i INIT.
przy czym proces analizy rozpoczyna się o tablicy kodu programu głównego.
Operacje przetwarzamy w kolejności ich występowania i zgodnie z podanym dalej
typem. Klasy operacji podajemy w porządku ich sprawdzania.
1. Operacja z argumentami o wartościach znanych w czasie tłumaczenia wykonujemy składanie operacji.
2. Operacja zbędna usuwamy ją.
3. Operacja niezmiennicza, którą moŜna zoptymalizować wykonujemy
optymalizację, przesuwając ją do bloku INIT aktualnej pętli.
4. Operacja, której siłę moŜna zmniejszyć i moŜna ją zoptymalizować wykonujemy odpowiednią optymalizacje
AGT – optymalizacja kodu pośredniego w bokach podstawowych
17
5. Początek pętli wykonujemy wszystkie czynności związane z otwarciem pętli:
a. W bloku INIT umieszczamy podstawienia: I := A; T0 := B;
b. W bloku INCR umieszczamy podstawienie: I := I + T0;
c. Tworzymy i generujemy kod wynikowy etykiety INIT, a takŜe rozkaz
skok u do niej
d. Tworzymy etykiety TEST i OVER i generujemy kod wynikowy dla
TEST: if I > C then goto OVER;
e. Tworzymy etykietę LOOP, wartość wskaźnika następnej operacji
określamy tak, by wyznaczał pierwszą operację instrukcji wewnętrznych
pętli i wznawiamy optymalizację pętli.
6. Koniec instrukcji wewnętrznych pętli przerywamy optymalizację pętli i
zmieniamy źródło analizowanych operacji na blok INCR kończącej się pętli.
7. Koniec bloku INCR wykonujemy kolejno:
a. Generujemy rozkaz skoku do etykiety TEST
b. Usuwamy blok INCR aktualnej pętli (wygenerowaliśmy juŜ jego kod
wynikowy)
c. Generujemy kod etykiety INIT.
d. Z
pętli
zewnętrznej
robimy
pętlę
aktualną
i
jeśli
moŜna
ją
optymalizować to wznawiamy optymalizację ( w ten sposób będziemy
optymalizować blok INIT)
e. Zmieniamy źródło analizowanych operacji na blok INIT zakończonej
właśnie pętli.
8. Koniec bloki INIT wykonujemy kolejno:
a. generujemy rozkaz skoku do etykiety TEST
b. generujemy kod wynikowy etykiety OVER
c. usuwamy zakończony właśnie blok INIT
AGT – optymalizacja kodu pośredniego w bokach podstawowych
d. zmieniamy
źródło
analizowanych
18
operacji
na
postać
pośrednią
programu głównego, rozpoczynając od operacji następnej po ostatnio
analizowanej z tego źródła.
9. Dowolna etykieta lub operacja, do której prowadzi skok usuwamy z tablicy
symboli wszystkie informacje uŜywane przy składaniu i usuwaniu operacji
zbędnych. Jest to początek nowego bloku podstawowego.
10. Dowolna inna operacja Generujemy dla jej kod wynikowy.
PoniŜej przedstawione zostały struktury kodu pośredniego przed (a) i po (b)
optymalizacji pętli:
for I := 1 step 1 until N do A[[I, J] := A[I, J] + A[J, I],
która jest pętlą wewnętrzną pętli, w której zmienną sterującą jest J.
W czasie analizy powyŜszej pętli zostaną przesunięte pewne instrukcje do
bloku INIT. Blok ten znajduje się na zewnątrz danej pętli, ale w środku pętli
zewnętrznej.
(b)
(a)
goto INIT;
goto INIT;
TEST: if I > N
LOOP: T1:= I * d2;
T2:= T1 + J;
T3:= J * d2;
T4:= T3 + I;
T5:= A[T2]+A[T4];
A[T2]:= T5;
INCR: I := I + 1
GOTO TEST
TEST: if I > N
LOOP:
T2:= T1 + J;
T4:= T3 + I;
T5:= A[T2]+A[T4];
A[T2]:= T5;
INCR: I := I + 1
T1:= T1 + TI;
GOTO TEST
INIT:
INIT: I := 1 ;
GOTO TEST
OVER:
OVER:
I := 1 ;
T3:= J * d2;
T1:= I * d2;
TI:= 1 * d2;
GOTO TEST

Podobne dokumenty