Najlepszą książką, w której można odpowiedzieć na twoje pytanie, byłyby prawdopodobnie: Cooper i Torczon, „Engineering a Compiler”, 2003. Jeśli masz dostęp do biblioteki uniwersyteckiej, powinieneś być w stanie pożyczyć kopię.
W kompilatorze produkcyjnym, takim jak llvm lub gcc, projektanci dokładają wszelkich starań, aby wszystkie algorytmy pozostawały poniżej gdzie jest rozmiarem danych wejściowych. W przypadku niektórych analiz dla faz „optymalizacji” oznacza to, że należy użyć heurystyki zamiast tworzyć naprawdę optymalny kod.O(n2)n
Lexer jest skończoną maszyną stanu, więc w wielkości wejściowej (w znakach) i wytwarza strumień tokenów który jest przekazywany do parsera.O(n)O(n)
W wielu kompilatorach dla wielu języków analizatorem jest LALR (1), a zatem przetwarza strumień tokenów w czasie w liczbie tokenów wejściowych. Podczas parsowania zwykle musisz śledzić tabelę symboli, ale w wielu językach można to obsłużyć za pomocą stosu tabel skrótów („słowniki”). Każdy dostęp do słownika to , ale czasami może być konieczne przejście stosu w celu wyszukania symbolu. Głębokość stosu wynosi gdzie jest głębokością zagnieżdżenia zakresów. (Więc w językach podobnych do C, ile warstw nawiasów klamrowych jesteś w środku.)O(n)O(1)O(s)s
Następnie parsowane drzewo jest zwykle „spłaszczane” do grafu kontrolnego. Węzły wykresu przepływu sterowania mogą być instrukcjami 3-adresowymi (podobnymi do języka asemblera RISC), a rozmiar wykresu przepływu sterowania będzie zwykle liniowy w stosunku do wielkości drzewa analizy.
Następnie zwykle stosuje się szereg etapów eliminacji redundancji (wspólna eliminacja podwyrażenia, ruch kodu niezmiennika w pętli, ciągła propagacja, ...). (Jest to często nazywane „optymalizacją”, chociaż rzadko jest coś optymalnego w wyniku, prawdziwym celem jest poprawienie kodu w jak największym stopniu w ramach ograniczeń czasowych i przestrzennych, które nałożyliśmy na kompilator.) Każdy krok eliminacji nadmiarowości będzie zazwyczaj wymagają dowodów potwierdzających niektóre fakty dotyczące grafu kontrolnego. Te dowody są zwykle wykonywane przy użyciu analizy przepływu danych . Większość analiz przepływu danych zaprojektowano tak, aby zbiegały się w przebiegach nad wykresem przepływu, gdzie jest (z grubsza) głębokością zagnieżdżenia pętli, a przejście przez wykres przepływu zajmuje czasO(d)dO(n)gdzie jest liczbą instrukcji 3-adresowych.n
Aby uzyskać bardziej wyrafinowane optymalizacje, możesz przeprowadzić bardziej wyrafinowane analizy. W tym momencie zaczynasz wpadać na kompromisy. Chcesz, aby algorytmy analizy zajmowały znacznie mniej niżO(n2)czas wielkości wykresu przepływu całego programu, ale oznacza to, że musisz obejść się bez informacji (i usprawnień transformacji programu), których udowodnienie może być kosztowne. Klasycznym przykładem tego jest analiza aliasów, w której dla niektórych zapisów pamięci chciałbyś udowodnić, że te dwa zapisy nigdy nie mogą być kierowane na to samo miejsce w pamięci. (Możesz przeprowadzić analizę aliasu, aby sprawdzić, czy możesz przenieść jedną instrukcję nad drugą.) Ale aby uzyskać dokładne informacje na temat aliasów, może być konieczne przeanalizowanie każdej możliwej ścieżki sterowania przez program, która jest wykładnicza pod względem liczby rozgałęzień w programie (a zatem wykładniczo w liczbie węzłów na grafie przepływu sterowania).
Następnie przejdziesz do alokacji rejestru. Przydział rejestru można sformułować jako problem polegający na zabarwieniu wykresu, a kolorowanie wykresu przy minimalnej liczbie kolorów jest znane jako NP-Hard. Dlatego większość kompilatorów używa pewnego rodzaju chciwej heurystyki w połączeniu z rozlewaniem rejestrów w celu jak najlepszego ograniczenia liczby wycieków rejestrów w rozsądnych ramach czasowych.
Wreszcie możesz przejść do generowania kodu. Generowanie kodu jest zwykle wykonywane jako maksymalny blok podstawowy w czasie, gdy blok podstawowy jest zestawem liniowo połączonych węzłów grafowych sterowania przepływem z jednym wejściem i jednym wyjściem. Można to przeformułować jako wykres obejmujący problem, w którym wykres, który próbujesz pokryć, jest wykresem zależności zestawu instrukcji 3-adresowych w bloku podstawowym, a próbujesz pokryć się zestawem wykresów reprezentujących dostępną maszynę instrukcje. Ten problem ma wykładniczy rozmiar największego bloku podstawowego (który w zasadzie może być tego samego rzędu co rozmiar całego programu), więc znów dzieje się tak zwykle z heurystyką, gdzie tylko niewielka część możliwych pokryć jest badany.