Überblick
Ein Interpreter für die Sprache X ist ein Programm (oder eine Maschine oder nur eine Art Mechanismus im Allgemeinen), das jedes in der Sprache X geschriebene Programm p so ausführt, dass es die Effekte ausführt und die Ergebnisse gemäß der Spezifikation von X auswertet . CPUs sind normalerweise Interpreter für ihre jeweiligen Befehlssätze, obwohl moderne Hochleistungs-Workstation-CPUs tatsächlich komplexer sind. Sie können tatsächlich einen zugrunde liegenden proprietären privaten Anweisungssatz haben und den extern sichtbaren öffentlichen Anweisungssatz entweder übersetzen (kompilieren) oder interpretieren.
Ein Compiler von X nach Y ist ein Programm (oder eine Maschine oder nur eine Art Mechanismus im Allgemeinen), das ein beliebiges Programm p aus einer Sprache X in ein semantisch äquivalentes Programm p ' in einer Sprache Y übersetzt, so dass die Semantik des Programms erhalten bleiben, das heißt , dass die Interpretation p ' für mit einem Interpreter Y die gleichen Ergebnisse liefern , und die gleichen Wirkungen wie die Interpretation haben p mit einem Interpreter für X . (Beachten Sie, dass X und Y dieselbe Sprache sein können.)
Die Begriffe AOT (Ahead-of-Time) und Just-in-Time (JIT) beziehen sich auf den Zeitpunkt der Kompilierung: Die in diesen Begriffen genannte "Zeit" ist "Laufzeit", dh ein JIT-Compiler kompiliert das Programm so, wie es ist ausgeführt , erstellt ein AOT - Compiler das Programm , bevor es ausgeführt wird . Beachten Sie, dass ein JIT-Compiler von Sprache X zu Sprache Y irgendwie mit einem Interpreter für Sprache Y zusammenarbeiten mussSonst gäbe es keine Möglichkeit, das Programm auszuführen. (So ist beispielsweise ein JIT-Compiler, der JavaScript in x86-Maschinencode kompiliert, ohne eine x86-CPU nicht sinnvoll. Er kompiliert das Programm, während es ausgeführt wird, aber ohne die x86-CPU würde das Programm nicht ausgeführt.)
Beachten Sie, dass diese Unterscheidung für Interpreter keinen Sinn ergibt: Ein Interpreter führt das Programm aus. Die Idee eines AOT-Interpreters, der ein Programm ausführt, bevor es ausgeführt wird, oder eines JIT-Interpreters, der ein Programm ausführt, während es ausgeführt wird, ist unsinnig.
Also haben wir:
- AOT-Compiler: Kompiliert vor dem Ausführen
- JIT-Compiler: Kompiliert während der Ausführung
- Dolmetscher: läuft
JIT-Compiler
Innerhalb der Familie der JIT-Compiler gibt es immer noch viele Unterschiede, wann genau sie kompiliert werden, wie oft und mit welcher Granularität.
Der JIT-Compiler in der CLR von Microsoft kompiliert beispielsweise Code nur einmal (wenn er geladen ist) und kompiliert jeweils eine gesamte Assembly. Andere Compiler sammeln möglicherweise Informationen, während das Programm ausgeführt wird, und kompilieren den Code mehrmals neu, sobald neue Informationen verfügbar werden, mit denen sie ihn besser optimieren können. Einige JIT-Compiler können sogar Code deoptimieren . Nun könnten Sie sich fragen, warum man das jemals tun möchte? Mit der Deoptimierung können Sie sehr aggressive Optimierungen durchführen, die möglicherweise unsicher sind. Wenn sich herausstellt, dass Sie zu aggressiv sind, können Sie einfach wieder zurücktreten, während Sie mit einem JIT-Compiler, der die Deoptimierung nicht durchführen kann, die nicht ausführen konnten aggressive Optimierungen an erster Stelle.
JIT-Compiler können entweder eine statische Codeeinheit auf einmal kompilieren (ein Modul, eine Klasse, eine Funktion, eine Methode, ...; diese werden zum Beispiel in der Regel als JIT- Methode zu einem Zeitpunkt bezeichnet ) oder sie können die Dynamik verfolgen Ausführung von Code dynamisch finden Spuren (typischerweise Schleifen) , dass sie dann kompilieren (diese werden als Tracing GEG).
Kombinieren von Dolmetschern und Compilern
Interpreter und Compiler können zu einer einzigen Sprachausführungsmaschine zusammengefasst werden. Es gibt zwei typische Szenarien, in denen dies durchgeführt wird.
Ein AOT - Compiler aus der Kombination von X zu Y mit einem Interpreter für Y . In diesem Fall ist X normalerweise eine höhere Sprache, die für die Lesbarkeit durch den Menschen optimiert ist, während Y eine höhere Sprache istist eine kompakte Sprache (oft eine Art Bytecode), die für die Interpretierbarkeit durch Maschinen optimiert ist. Das CPython-Python-Ausführungsmodul verfügt beispielsweise über einen AOT-Compiler, der Python-Quellcode in CPython-Bytecode kompiliert, und einen Interpreter, der CPython-Bytecode interpretiert. Ebenso verfügt die YARV-Ruby-Ausführungs-Engine über einen AOT-Compiler, der den Ruby-Quellcode in den YARV-Bytecode kompiliert, und einen Interpreter, der den YARV-Bytecode interpretiert. Warum willst du das tun? Ruby und Python sind beide auf sehr hohe Niveau und etwas komplexen Sprachen, so dass wir sie zuerst in eine Sprache kompilieren , die einfacher zu analysieren und leichter zu interpretieren ist, und dann interpretieren , dass die Sprache.
Die andere Möglichkeit, einen Interpreter und einen Compiler zu kombinieren, ist eine Ausführungs-Engine im gemischten Modus . Hier haben wir „mischen“ zwei „Modi“ der Umsetzung derselben Sprache zusammen, dh einen Interpreter für X und einen JIT - Compiler von X zu Y . (Der Unterschied hier ist, dass wir im obigen Fall mehrere "Phasen" hatten, in denen der Compiler das Programm kompilierte und dann das Ergebnis in den Interpreter einspeiste. Hier arbeiten die beiden nebeneinander in derselben Sprache. ) Code, der von einem Compiler kompiliert wurde, wird in der Regel schneller ausgeführt als Code, der von einem Interpreter ausgeführt wird. Das eigentliche Kompilieren des Codes nimmt jedoch einige Zeit in Anspruch (insbesondere, wenn Sie den auszuführenden Code stark optimieren möchten)sehr schnell, es braucht viel Zeit). Um diese Zeit zu überbrücken, in der der JIT-Compiler gerade mit dem Kompilieren des Codes beschäftigt ist, kann der Interpreter bereits mit dem Ausführen des Codes beginnen. Sobald das Kompilieren des JIT abgeschlossen ist, können wir die Ausführung auf den kompilierten Code umstellen. Dies bedeutet, dass wir beide die bestmögliche Leistung des kompilierten Codes erzielen, aber nicht warten müssen, bis die Kompilierung abgeschlossen ist, und unsere Anwendung sofort ausgeführt wird (obwohl nicht so schnell wie möglich).
Dies ist eigentlich nur die einfachste mögliche Anwendung einer Ausführungs-Engine im gemischten Modus. Interessanter ist beispielsweise, nicht gleich mit dem Kompilieren zu beginnen, sondern den Interpreter ein wenig laufen zu lassen und Statistiken, Profilinformationen, Typinformationen, Informationen über die Wahrscheinlichkeit, mit der bestimmte bedingte Verzweigungen ausgeführt werden, zu sammeln, welche Methoden aufgerufen werden am häufigsten usw. und geben Sie diese dynamischen Informationen an den Compiler weiter, damit dieser optimierten Code generieren kann. Dies ist auch eine Möglichkeit, die oben erwähnte Deoptimierung zu implementieren: Wenn sich herausstellt, dass Sie bei der Optimierung zu aggressiv waren, können Sie einen Teil des Codes wegwerfen und zur Interpretation zurückkehren. Die HotSpot JVM macht das zum Beispiel. Es enthält sowohl einen Interpreter für JVM-Bytecode als auch einen Compiler für JVM-Bytecode. (Eigentlich,zwei Compiler!)
Es ist auch möglich und tatsächlich üblich, diese beiden Ansätze zu kombinieren: Zwei Phasen, wobei die erste ein AOT-Compiler ist, der X zu Y kompiliert , und die zweite Phase eine Mixed-Mode-Engine, die Y interpretiert und Y zu Z kompiliert . Die Rubinius Ruby-Ausführungs-Engine funktioniert beispielsweise so: Sie verfügt über einen AOT-Compiler, der Ruby-Quellcode in Rubinius-Bytecode kompiliert, und eine Mixed-Mode-Engine, die Rubinius-Bytecode zuerst interpretiert und nach dem Sammeln einiger Informationen die am häufigsten genannten Methoden in native kompiliert Maschinensprache.
Es ist zu beachten, dass die Rolle, die der Interpreter im Fall einer Ausführungsmaschine im gemischten Modus spielt, nämlich das Bereitstellen eines schnellen Starts und auch das potenzielle Sammeln von Informationen und das Bereitstellen von Fallback-Fähigkeiten, alternativ auch von einem zweiten JIT-Compiler übernommen werden kann. So funktioniert zum Beispiel V8. V8 interpretiert nie, es kompiliert immer. Der erste Compiler ist ein sehr schneller, sehr flacher Compiler, der sehr schnell startet. Der erzeugte Code ist jedoch nicht sehr schnell. Dieser Compiler fügt außerdem Profiling-Code in den von ihm generierten Code ein. Der andere Compiler ist langsamer und benötigt mehr Speicher, erzeugt jedoch viel schnelleren Code. Er kann die Profilinformationen verwenden, die durch Ausführen des vom ersten Compiler kompilierten Codes gesammelt wurden.