Metody realizacji języków programowania/MRJP Wykład 11: Różnice pomiędzy wersjami
Linia 52: | Linia 52: | ||
definicje niepewne i ścieżki, które być może nigdy nie zostaną wykorzystane. | definicje niepewne i ścieżki, które być może nigdy nie zostaną wykorzystane. | ||
=== Programy strukturalne i obliczanie definicji | === Programy strukturalne i obliczanie definicji osiągających === | ||
Załóżmy standardową gramatykę pewnego języka strukturalnego: | Załóżmy standardową gramatykę pewnego języka strukturalnego: | ||
Linia 63: | Linia 63: | ||
Zakładamy, że wszystkie definicje są pewne. Równania przepływu danych | Zakładamy, że wszystkie definicje są pewne. Równania przepływu danych | ||
dla definicji | dla definicji osiągających można łatwo zapisać: | ||
S ::= S1 ; S2 <math>gen[S]=gen[S2] \cup (gen[S1] - kill[S2])</math> | S ::= S1 ; S2 <math>gen[S]=gen[S2] \cup (gen[S1] - kill[S2])</math> | ||
Linia 93: | Linia 93: | ||
Takie różnice są akceptowalne (bezpieczne), pod warunkiem, że wykonane optymalizacje w oparciu o takie | Takie różnice są akceptowalne (bezpieczne), pod warunkiem, że wykonane optymalizacje w oparciu o takie | ||
zbiory wygenerowanych definicji | zbiory wygenerowanych definicji osiągających nie zmieniają obliczanej funkcji programu. | ||
== Podejście iteracyjne == | == Podejście iteracyjne == |
Wersja z 11:42, 26 lip 2006
Globalna analiza przepływu danych
Opracowana na podstawie "Kompilatory" Aho, Sethi, Ullmana.
Podstawy
Równania przepływu danych
(Zakładam, ze pojęcie grafu przepływu jest już znane.)
Informacje dotyczące przepływu danych zwykle wylicza się za pomocą układów równań, które przedstawiają zależności między występowaniem tych informacji pomiędzy różnymi punktami programu. Zwykle równanie ma postać:
Należy je rozumieć w następująco: informacja dostępna na końcu instrukcji jest sumą zbiorów informacji
- przez nią generowanych (oznaczonych jako )
- dostępnych na wejściu do niej () ale bez informacji, które są niszczone przez tą instrukcję ().
W zależności od rozwiązywanego zadania, będziemy rozważać równania, w których definiuje się za pomocą (w analizie od końcowego bloku). W globalnej analizie przepływu danych zamiast pojedyńczej instrukcji stosuje się bloki bazowe z grafu przepływu.
Definicje osiągające
Definicją zmiennej nazywamy instrukcję, która wiąże lub może wiązać tą zmienną z wartością. Przypisania lub czytanie z pliku to instrukcje zawsze definiującą zmienną. Nazywamy je definicjami pewnymi. Instrukcje niepewne to takie, którę mogą definiować zmienną np.:
- wywołanie procedury ze zmienną jako argumentem przekazywanym nie przez wartość,
- przypisania przez wskaźnik do zmiennej (w praktyce oznacza to defnicję każdej zmiennej).
Rozważmy ścieżkę w grafie przepływu łączącą dwa punkty. Wystąpienie pewnej definicji zmiennej a na tej ścieżce zabija wcześniejszą definicję tej zmiennej. Piszemy, że definicja D osiąga punkt P w grafie przepływu, jeśli istnieje ścieżka z miejsca występującego bezpośrednio po D do P, taka że D nie jest zabijane na tej ścieżce. Zauważmy, że w danym punkcie mogą być osiągalne pewne i niepewne definicje.
W poniższym przykładzie definicje X=1 i X=3 są pewne, a *Z=1 jest niepewna. Definicja X=1 nie jest zabijana przez definicje z bloku C, zatem w bloku D jest osiągalna. Jednocześnie w bloku D jest osiągalna definicja niepewna *Z=1, która potencjalnie też jest definiującą dla zmiennej X. Instrukcja X=3 w bloku E zabija wcześniejsze definicje X.
Zakładamy, że docelowy program będzie wykorzystywał wszystkie krawędzie (sprawdzenie tego jest problemem nierozstrzygalnym). Wszelkie wątpliwości rozważamy ``konserwatywnie, tak by uwzględnić każdy potencjalny scenariusz wykonania. Dlatego musimy rozważać definicje pewne zasłonięte przez definicje niepewne i ścieżki, które być może nigdy nie zostaną wykorzystane.
Programy strukturalne i obliczanie definicji osiągających
Załóżmy standardową gramatykę pewnego języka strukturalnego:
S ::= S1 ; S2 // Złożenie | if E then S1 else S2 // Instrukcja warunkowa | do S1 while E // Pętla | id := E // Przypisanie (definicja pewna) E ::= id + id | id
Zakładamy, że wszystkie definicje są pewne. Równania przepływu danych dla definicji osiągających można łatwo zapisać:
S ::= S1 ; S2 | if E then S1 else S2 | do S1 while E | id := E
Tu to zbiór wszystkich definicji zmiennej id w programie.
Cechy oblicznych zbiorów:
- "prawdziwy" kill (tzn. taki który zostałby wygenerowany podczas pewnego uruchomienia programu) jest zawsze nadzbiorem obliczanego (wg powyższych reguł),
- "prawdziwy" gen jest zawsze podzbiorem obliczanego.
Takie różnice są akceptowalne (bezpieczne), pod warunkiem, że wykonane optymalizacje w oparciu o takie zbiory wygenerowanych definicji osiągających nie zmieniają obliczanej funkcji programu.
Podejście iteracyjne
W ogólnej sytuacji stosuje się podejście iteracyjne. Ponieważ wiele problemów związanych z analizą przepływu danych ma charakter "generowania-zabijania", pokażemy na przykładzie problemu definicji osiągających jak rozwiązywać równania przepływu danych dla dowolnych grafów przepływu.
Schemat:
- zbuduj graf przepływu,
- oblicz zbiory gen i kill dla bloków bazowych,
- oblicz jednocześnie, stosując podejście iteracyjne, zbiory in i out.
Bedziemy wyróżniać dwa rodzaje obliczeń:
- do przodu, gdy zbiory out oblicza się na podstawie zbiorów in,
- do tyłu, gdy zbiory in oblicza się na podstawie zbiorów out.
Operatorem łączącym nazywamy operator użyty do zdefiniowania zbiorów przychodzących do danego bloku. Np. dla problemu definicji osiągających jest to operator sumy.
Problem osiągających definicji
Równania dla tego problemu są następujące:
Parser nie mógł rozpoznać (błąd składni): {\displaystyle in[B] = \bigcup_{P\ \mbox{\small poprzednik}\ B}\ out[B]}
Algorytm iteracyjny obliczający in i out jest dość naturalny:
1. Oblicz gen i kill dla bloków bazowych 2. for każdy blok bazowy B do 3. end for 4. while zbiory in lub out zmieniają się do 5. for każdy blok bazowy B do Parser nie mógł rozpoznać (błąd składni): {\displaystyle in[B]:= \bigcup_{P\ \mbox{\small poprzednik}\ B}\ out[B]} 6. end for 5. done
Zmienne żywe
Przypomnijmy definicję zmiennej żywej: zmienna jest żywa w danym miejscu jeśli może być użyta na pewnej ścieżce rozpoczynającej się od tego miejsca. W przeciwnym przypadku mówimy, że zmienna jest martwa. Ta informacja jest ważna w generowaniu kodu wynikowego, np. w sytuacji gdy potrzebujemy wolnego rejestru można użyć tych które aktualnie przechowują zmienne martwe.
Równania dla tego problemu są następujące:
Parser nie mógł rozpoznać (błąd składni): {\displaystyle in[B] = \bigcup_{S\ \mbox{\small następnik}\ B}\ in[S]}
gdzie
- in[B] - zbiór zmiennych żywych bezpośrednio przed blokiem B
- out[B] - zbiór zmiennych żywych bezpośrednio za blokiem B
- def[B] - do tego zbioru należy każda zmienna, której definicja i póżniejsze użycie jest w B (odpowiednik zbioru gen)
- use[B] - do tego zbioru należy każda zmienna, która może być użyta w B przed dowolną definicją tej zmiennej (odpowiednik zbioru kill)
Algorytm iteracyjny jest analogiczny do poprzedniego.
Generowanie kodu wynikowego - przydział rejestrów
O zmiennych żywych i martwych w przydziale jest wspomniane wyżej.
Może osobny wykład?