Zrozumienie różnic: tradycyjny interpreter, kompilator JIT, interpreter JIT i kompilator AOT

130

Próbuję zrozumieć różnice między tradycyjnym tłumaczem, kompilatorem JIT, interpreterem JIT i kompilatorem AOT.

Tłumacz to po prostu maszyna (wirtualna lub fizyczna), która wykonuje instrukcje w pewnym języku komputerowym. W tym sensie JVM jest tłumaczem, a fizyczne procesory to tłumacze.

Kompilacja z wyprzedzeniem oznacza po prostu skompilowanie kodu do jakiegoś języka przed jego wykonaniem (interpretacją).

Nie jestem jednak pewien dokładnych definicji kompilatora JIT i interpretera JIT.

Zgodnie z definicją, którą przeczytałem, kompilacja JIT po prostu kompiluje kod tuż przed jego interpretacją.

Zasadniczo kompilacja JIT to kompilacja AOT, wykonywana tuż przed wykonaniem (interpretacją)?

A interpreter JIT, czy program zawiera zarówno kompilator JIT, jak i interpreter i kompiluje kod (JITs) tuż przed jego interpretacją?

Proszę wyjaśnić różnice.

Aviv Cohn
źródło
4
Dlaczego uważasz, że istnieje różnica między „kompilatorem JIT” a „tłumaczem JIT”? Są to zasadniczo dwa różne słowa na to samo. Ogólna koncepcja JIT jest taka sama, ale istnieje wiele różnych technik implementacji, których nie można po prostu podzielić na „kompilator” vs. „interpreter”.
Greg Hewgill
2
Przeczytaj strony internetowe na temat kompilacji Just In Time , kompilatora AOT , kompilatora , interpretera , kodu bajtowego, a także książki Lisp in Small Pieces
Queinnec

Odpowiedzi:

198

Przegląd

Interpreter dla języka X to program (lub urządzenia, lub po prostu jakiś rodzaj mechanizmu w ogóle), który wykonuje żadnego programu p napisany w języku X takie, że spełnia ona efekty i ocenia wyniki, jak zapisano w specyfikacji X . Procesory są zwykle interpretatorami odpowiednich zestawów instrukcji, chociaż nowoczesne wysokowydajne procesory stacji roboczych są w rzeczywistości bardziej skomplikowane; mogą faktycznie mieć własny, zastrzeżony prywatny zestaw instrukcji i albo tłumaczyć (kompilować), albo interpretować widoczny publicznie zestaw instrukcji.

Kompilator od X do Y to program (lub maszyną, lub po prostu jakiś rodzaj mechanizmu w ogóle), który przekłada dowolny program p od jakiegoś języka X do semantycznie równoważne programu p ' w jakimś języku Y w taki sposób, że semantyka programu są zachowane, to znaczy, że interpretacji p ' tłumacza dla Y, dadzą takie same wyniki i mają te same efekty jak interpretacji p tłumacza dla X . (Pamiętaj, że X i Y mogą być tym samym językiem.)

Określenia Ahead-of-time (AOT) oraz Just-in-Time (JIT) odnoszą się do kiedy kompilacja odbywa: "czas", o którym mowa w tych kategoriach jest "czas pracy", czyli kompilator JIT kompiluje program , jak to jest po uruchomieniu kompilator AOT kompiluje program przed jego uruchomieniem . Zauważ, że wymaga to, aby kompilator JIT z języka X na język Y musiał jakoś współpracować z tłumaczem języka Y, w przeciwnym razie nie byłoby sposobu na uruchomienie programu. (Na przykład kompilator JIT, który kompiluje JavaScript do kodu maszynowego x86, nie ma sensu bez procesora x86; kompiluje program podczas działania, ale bez procesora x86 program nie działałby.)

Zauważ, że to rozróżnienie nie ma sensu dla tłumaczy: interpreter uruchamia program. Pomysł interpretera AOT, który uruchamia program przed jego uruchomieniem, lub interpretera JIT, który uruchamia program podczas jego działania, jest bezsensowny.

Więc mamy:

  • Kompilator AOT: kompiluje się przed uruchomieniem
  • Kompilator JIT: kompiluje podczas działania
  • tłumacz: działa

Kompilatory JIT

W rodzinie kompilatorów JIT nadal istnieje wiele różnic dotyczących tego, kiedy dokładnie kompilują, jak często i przy jakiej ziarnistości.

Na przykład kompilator JIT w CLR Microsoftu kompiluje kod tylko raz (po załadowaniu) i kompiluje cały zestaw na raz. Inne kompilatory mogą gromadzić informacje podczas działania programu i kilkakrotnie rekompilować kod, gdy stają się dostępne nowe informacje, które pozwalają im lepiej je zoptymalizować. Niektóre kompilatory JIT są nawet zdolne do dezoptymalizowania kodu. Teraz możesz zadać sobie pytanie, dlaczego ktoś chciałby to zrobić? Dezoptymalizacja pozwala na przeprowadzanie bardzo agresywnych optymalizacji, które mogą być w rzeczywistości niebezpieczne: jeśli okaże się, że jesteś zbyt agresywny, możesz po prostu wycofać się, podczas gdy kompilator JIT, który nie może dezoptymalizować, nie mógł uruchomić przede wszystkim agresywne optymalizacje.

Kompilatory JIT mogą albo kompilować pewną statyczną jednostkę kodu za jednym razem (jeden moduł, jedna klasa, jedna funkcja, jedna metoda,…; zazwyczaj są one nazywane na przykład metodą JIT metodą pojedynczego testu) lub mogą śledzić dynamikę wykonanie kodu w celu znalezienia śladów dynamicznych (zwykle pętli), które następnie skompilują (są to tak zwane śledzenie JIT).

Łączenie tłumaczy ustnych i kompilatorów

Tłumacze ustni i kompilatory można łączyć w jednym języku wykonawczym. Istnieją dwa typowe scenariusze, w których jest to wykonywane.

Łączenie kompilator AOT od X do Y z tłumacza Y . Tutaj zazwyczaj X jest językiem wyższego poziomu zoptymalizowanym pod kątem czytelności dla ludzi, podczas gdy Yto kompaktowy język (często jakiś kod bajtowy) zoptymalizowany pod kątem interpretacji przez maszyny. Na przykład silnik wykonawczy CPython Python ma kompilator AOT, który kompiluje kod źródłowy Pythona do kodu bajtowego CPython oraz interpreter interpretujący kod bajtowy CPython. Podobnie, silnik wykonawczy YARV Ruby ma kompilator AOT, który kompiluje kod źródłowy Ruby do kodu bajtowego YARV oraz interpreter, który interpretuje kod bajtowy YARV. Dlaczego chcesz to zrobić? Zarówno Ruby, jak i Python są bardzo wysokopoziomowymi i nieco złożonymi językami, dlatego najpierw kompilujemy je w języku, który jest łatwiejszy do analizy i interpretacji, a następnie interpretuje ten język.

Innym sposobem na połączenie interpretera i kompilatora jest silnik wykonujący w trybie mieszanym . Tutaj „mix” dwa „tryby” realizacji tego samego języka razem, czyli tłumacza dla X i kompilator JIT od X do Y . (Różnica polega na tym, że w powyższym przypadku mieliśmy wiele „etapów” z kompilatorem kompilującym program, a następnie wprowadzającym wynik do interpretera, tutaj mamy dwa działające obok siebie w tym samym języku. ) Kod skompilowany przez kompilator ma tendencję do działania szybciej niż kod wykonywany przez interpretera, ale w rzeczywistości kompilacja kodu wymaga czasu (a zwłaszcza, jeśli chcesz mocno zoptymalizować kod do uruchomieniabardzo szybko, zajmuje dużo czasu). Aby więc zmostkować czas, w którym kompilator JIT jest zajęty kompilowaniem kodu, interpreter może już uruchomić kod, a po zakończeniu kompilacji JIT możemy przełączyć wykonanie na skompilowany kod. Oznacza to, że uzyskujemy zarówno najlepszą możliwą wydajność skompilowanego kodu, ale nie musimy czekać na zakończenie kompilacji, a nasza aplikacja zaczyna działać od razu (choć nie tak szybko, jak to możliwe).

Jest to właściwie najprostsze możliwe zastosowanie silnika wykonawczego w trybie mieszanym. Bardziej interesujące możliwości to, na przykład, nie rozpoczynanie kompilacji od razu, ale pozwól tłumaczowi trochę uruchomić i zbieraj statystyki, informacje o profilowaniu, informacje o typie, informacje o prawdopodobieństwie podjęcia określonych gałęzi warunkowych, które metody są nazywane najczęściej itp., a następnie podaj te informacje dynamiczne do kompilatora, aby mógł wygenerować bardziej zoptymalizowany kod. Jest to również sposób na wdrożenie deoptymalizacji, o której mówiłem powyżej: jeśli okaże się, że byłeś zbyt agresywny w optymalizacji, możesz wyrzucić (część) kodu i powrócić do tłumaczenia. JVM HotSpot robi to na przykład. Zawiera zarówno interpreter kodu bajtowego JVM, jak i kompilator kodu bajtowego JVM. (W rzeczywistości,dwa kompilatory!)

Jest również możliwe, a w rzeczywistości wspólnej połączenie tych dwóch podejść: dwóch faz z których pierwsza jest kompilator AOT że zestawia X do Y , a druga faza jest silnik w trybie mieszanym, że zarówno interpretuje Y i zestawia Y do Z . Silnik wykonawczy Rubinius Ruby działa w ten sposób, na przykład: ma kompilator AOT, który kompiluje kod źródłowy Rubyus do kodu bajtowego Rubinius oraz silnik mieszany, który najpierw interpretuje kod bajtowy Rubiniusa, a po zebraniu niektórych informacji kompiluje najczęściej wywoływane metody na język macierzysty kod maszynowy.

Zauważ, że rolę, jaką odgrywa interpreter w przypadku silnika wykonawczego w trybie mieszanym, a mianowicie zapewnianie szybkiego uruchamiania, a także potencjalnie gromadzenia informacji i zapewniania funkcji awaryjnej, alternatywnie może pełnić także drugi kompilator JIT. Tak na przykład działa V8. V8 nigdy nie interpretuje, zawsze się kompiluje. Pierwszy kompilator to bardzo szybki, bardzo cienki kompilator, który uruchamia się bardzo szybko. Kod, który tworzy, nie jest jednak bardzo szybki. Ten kompilator wstrzykuje również kod profilujący do generowanego kodu. Drugi kompilator działa wolniej i zużywa więcej pamięci, ale generuje znacznie szybszy kod i może korzystać z informacji profilowania zebranych przez uruchomienie kodu skompilowanego przez pierwszy kompilator.

Jörg W Mittag
źródło
1
Czy kompilatory kodu bajtowego Python i Ruby naprawdę liczą się jako AOT? Biorąc pod uwagę, że oba języki pozwalają na dynamiczne ładowanie modułów, które są kompilowane podczas ładowania, działają one w czasie wykonywania programu.
Sebastian Redl,
1
@SebastianRedl, za pomocą CPython możesz uruchomić python -m compileall .lub załadować moduły raz. Nawet w tym drugim przypadku, ponieważ pliki pozostają i są ponownie używane po pierwszym uruchomieniu, wygląda na to, że AOT.
Paul Draper,
Czy masz jakieś referencje do dalszego czytania? Chciałbym dowiedzieć się więcej o V8.
Vince Panuccio
@VincePanuccio W poście wymieniono kompilatory Full-Codegen i Crankshaft, które zostały wymienione . Możesz znaleźć o nich online.
eush77
CLR Jit kompiluje metodę metodą, a nie cały zestaw
Grigory