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