Just-in-time-Zusammenstellung - Just-in-time compilation

In der Computertechnik ist die Just-in-Time- Kompilierung ( JIT ) (auch dynamische Übersetzung oder Laufzeitkompilierung ) eine Methode zur Ausführung von Computercode , bei der die Kompilierung während der Ausführung eines Programms (zur Laufzeit ) und nicht vor der Ausführung erfolgt. Dies kann aus der Übersetzung des Quellcodes bestehen , ist aber häufiger eine Bytecode- Übersetzung in Maschinencode , der dann direkt ausgeführt wird. Ein System, das einen JIT-Compiler implementiert, analysiert typischerweise kontinuierlich den ausgeführten Code und identifiziert Teile des Codes, bei denen die durch die Kompilierung oder Neukompilierung erzielte Beschleunigung den Aufwand für die Kompilierung dieses Codes aufwiegen würde.

Die JIT-Kompilierung ist eine Kombination der beiden traditionellen Ansätze zur Übersetzung in Maschinencode – AOT -Kompilierung (AOT) und Interpretation – und kombiniert einige Vor- und Nachteile beider. Grob gesagt kombiniert die JIT-Kompilierung die Geschwindigkeit des kompilierten Codes mit der Flexibilität der Interpretation, mit dem Overhead eines Interpreters und dem zusätzlichen Overhead des Kompilierens und Linkens (nicht nur des Interpretierens). JIT - Kompilierung ist eine Form der dynamischen Kompilierung und ermöglicht adaptive Optimierung wie dynamische Neukompilierung und Mikroarchitektur -spezifische speedups. Interpretation und JIT-Kompilierung eignen sich besonders für dynamische Programmiersprachen , da das Laufzeitsystem spät gebundene Datentypen verarbeiten und Sicherheitsgarantien erzwingen kann.

Geschichte

Der früheste veröffentlichte JIT - Compiler ist in der Regel auf der Arbeit an zugeschrieben LISP von John McCarthy im Jahr 1960. In seinen wegweisenden Arbeit rekursive Funktionen von symbolischen Ausdrücken und deren Berechnung durch Maschine, Teil I , erwähnt er Funktionen , die während der Laufzeit übersetzt werden, wodurch die Notwendigkeit sparsam zu speichern Sie die Compiler-Ausgabe auf Lochkarten (obwohl dies genauer als " Compile-and-Go-System " bekannt wäre). Ein weiteres frühes Beispiel stammt von Ken Thompson , der 1968 eine der ersten Anwendungen von regulären Ausdrücken , hier für den Mustervergleich im Texteditor QED, lieferte . Aus Gründen der Geschwindigkeit implementierte Thompson die Übereinstimmung mit regulären Ausdrücken durch JITing mit IBM 7094- Code auf dem Compatible Time-Sharing-System . Eine einflussreiche Technik zur Ableitung von kompiliertem Code aus der Interpretation wurde 1970 von James G. Mitchell entwickelt und für die experimentelle Sprache LC² implementiert .

Smalltalk (ca. 1983) leistete Pionierarbeit bei neuen Aspekten der JIT-Zusammenstellung. Beispielsweise wurde die Übersetzung in Maschinencode bei Bedarf durchgeführt und das Ergebnis für die spätere Verwendung zwischengespeichert. Wenn der Speicher knapp wurde, löschte das System einen Teil dieses Codes und generierte ihn neu, wenn er wieder benötigt wurde. Suns Self- Sprache verbesserte diese Techniken erheblich und war zeitweise das schnellste Smalltalk-System der Welt; bis zu halb so schnell wie optimiertes C, aber mit einer vollständig objektorientierten Sprache.

Self wurde von Sun aufgegeben, aber die Forschung ging in die Java-Sprache. Der Begriff "Just-in-Time-Kompilierung" wurde dem Herstellerbegriff " Just in time " entlehnt und von Java populär gemacht, wobei James Gosling den Begriff aus dem Jahr 1993 verwendet. Derzeit wird JITing von den meisten Implementierungen der Java Virtual Machine verwendet , als HotSpot baut auf dieser Forschungsgrundlage auf und nutzt sie umfassend.

Das HP-Projekt Dynamo war ein experimenteller JIT-Compiler, bei dem das 'Bytecode'-Format und das Maschinencode-Format gleich waren; das System wandelte den Maschinencode PA-6000 in Maschinencode PA-8000 um. Entgegen der Intuition führte dies zu Geschwindigkeitssteigerungen, in einigen Fällen um 30 %, da dies Optimierungen auf Maschinencode-Ebene ermöglichte, z Compiler sind nicht in der Lage, es zu versuchen.

Im November 2020 führte PHP 8.0 einen JIT-Compiler ein.

Entwurf

In einem Bytecode-kompilierten System wird Quellcode in eine Zwischendarstellung übersetzt, die als bytecode bekannt ist . Bytecode ist nicht der Maschinencode für einen bestimmten Computer und kann zwischen Computerarchitekturen portierbar sein . Der Bytecode kann dann von einer virtuellen Maschine interpretiert oder auf dieser ausgeführt werden . Der JIT-Compiler liest die Bytecodes in vielen Abschnitten (oder selten vollständig) und kompiliert sie dynamisch in Maschinencode, damit das Programm schneller laufen kann. Dies kann pro Datei, pro Funktion oder sogar an jedem beliebigen Codefragment erfolgen; der Code kann kompiliert werden, wenn er kurz vor der Ausführung steht (daher der Name "just-in-time") und dann zwischengespeichert und später wiederverwendet werden, ohne neu kompiliert werden zu müssen.

Im Gegensatz dazu interpretiert eine herkömmliche interpretierte virtuelle Maschine einfach den Bytecode, im Allgemeinen mit viel geringerer Leistung. Einige Interpreter interpretieren sogar Quellcode ohne den Schritt, zuerst in Bytecode zu kompilieren, mit noch schlechterer Leistung. Statisch kompilierter oder systemeigener Code wird vor der Bereitstellung kompiliert. Eine dynamische Kompilierungsumgebung ist eine Umgebung, in der der Compiler während der Ausführung verwendet werden kann. Ein gemeinsames Ziel der Verwendung von JIT-Techniken besteht darin, die Leistung der statischen Kompilierung zu erreichen oder zu übertreffen , während die Vorteile der Bytecode-Interpretation beibehalten werden: Ein Großteil der "schweren Belastungen" des Parsens des ursprünglichen Quellcodes und der Durchführung grundlegender Optimierungen wird oft zur Kompilierzeit erledigt. vor der Bereitstellung: Die Kompilierung von Bytecode in Maschinencode ist viel schneller als die Kompilierung aus dem Quellcode. Der bereitgestellte Bytecode ist im Gegensatz zu nativem Code portabel. Da die Laufzeit die Kompilierung wie interpretierten Bytecode kontrolliert, kann sie in einer sicheren Sandbox ausgeführt werden. Compiler von Bytecode zu Maschinencode sind einfacher zu schreiben, da der portable Bytecode-Compiler bereits einen Großteil der Arbeit erledigt hat.

JIT-Code bietet im Allgemeinen eine weitaus bessere Leistung als Interpreter. Außerdem kann es in einigen Fällen eine bessere Performance bieten als die statische Kompilierung, da viele Optimierungen nur zur Laufzeit durchführbar sind:

  1. Die Kompilierung kann auf die Ziel-CPU und das Betriebssystemmodell, auf dem die Anwendung ausgeführt wird, optimiert werden. JIT kann beispielsweise SSE2- Vektor-CPU-Befehle auswählen, wenn es erkennt, dass die CPU diese unterstützt. Um dieses Maß an Optimierungsspezifität mit einem statischen Compiler zu erreichen, muss man entweder eine Binärdatei für jede beabsichtigte Plattform/Architektur kompilieren oder ansonsten mehrere Versionen von Teilen des Codes in eine einzelne Binärdatei einschließen.
  2. Das System ist in der Lage, Statistiken darüber zu sammeln, wie das Programm tatsächlich in der Umgebung läuft, in der es sich befindet, und es kann für eine optimale Leistung neu anordnen und kompilieren. Einige statische Compiler können jedoch auch Profilinformationen als Eingabe verwenden.
  3. Das System kann globale Codeoptimierungen (zB Inlining von Bibliotheksfunktionen) durchführen, ohne die Vorteile des dynamischen Linkens zu verlieren und ohne den Overhead statischer Compiler und Linker. Insbesondere bei globalen Inline-Ersetzungen kann ein statischer Kompilierungsprozess Laufzeitprüfungen erfordern und sicherstellen, dass ein virtueller Aufruf erfolgt, wenn die tatsächliche Klasse des Objekts die Inline-Methode überschreibt, und es müssen möglicherweise Randbedingungsprüfungen für Array-Zugriffe verarbeitet werden innerhalb von Schleifen. Bei der Just-in-Time-Kompilierung kann diese Verarbeitung in vielen Fällen aus Schleifen heraus verschoben werden, was oft zu großen Geschwindigkeitssteigerungen führt.
  4. Obwohl dies mit statisch kompilierten Garbage-Collection-Sprachen möglich ist, kann ein Bytecode-System ausgeführten Code für eine bessere Cache-Auslastung leichter neu anordnen.

Da ein JIT ein natives binäres Image zur Laufzeit rendern und ausführen muss, erfordern echte Maschinencode-JITs Plattformen, die die Ausführung von Daten zur Laufzeit ermöglichen, was die Verwendung solcher JITs auf einer auf der Harvard-Architektur basierenden Maschine unmöglich macht; dasselbe gilt auch für bestimmte Betriebssysteme und virtuelle Maschinen. Eine spezielle Art von "JIT" zielt jedoch möglicherweise nicht auf die CPU-Architektur der physischen Maschine ab, sondern eher auf einen optimierten VM-Bytecode, bei dem Einschränkungen des rohen Maschinencodes vorherrschen, insbesondere wenn die VM dieses Bytecodes schließlich ein JIT für nativen Code nutzt.

Leistung

JIT verursacht eine leichte bis spürbare Verzögerung bei der ersten Ausführung einer Anwendung aufgrund der Zeit, die zum Laden und Kompilieren des Bytecodes benötigt wird. Manchmal wird diese Verzögerung als "Startzeitverzögerung" oder "Aufwärmzeit" bezeichnet. Im Allgemeinen gilt: Je mehr Optimierungs-JIT durchführt, desto besser wird der generierte Code, aber auch die anfängliche Verzögerung nimmt zu. Ein JIT-Compiler muss daher einen Kompromiss zwischen der Kompilierungszeit und der Qualität des Codes eingehen, den er generieren möchte. Die Startzeit kann neben der JIT-Kompilierung auch erhöhte IO-gebundene Operationen umfassen: Zum Beispiel ist die rt.jar- Klassendatendatei für die Java Virtual Machine (JVM) 40 MB groß und die JVM muss viele Daten in dieser kontextuell riesigen Datei suchen .

Eine mögliche Optimierung, die von Suns HotSpot Java Virtual Machine verwendet wird, besteht darin, Interpretation und JIT-Kompilierung zu kombinieren. Der Anwendungscode wird zunächst interpretiert, aber die JVM überwacht, welche Sequenzen von Bytecode häufig ausgeführt werden und übersetzt sie in Maschinencode zur direkten Ausführung auf der Hardware. Bei Bytecode, der nur wenige Male ausgeführt wird, spart dies die Kompilierungszeit und reduziert die anfängliche Latenz; für häufig ausgeführten Bytecode wird die JIT-Kompilierung verwendet, um nach einer anfänglichen Phase langsamer Interpretation mit hoher Geschwindigkeit zu laufen. Da ein Programm außerdem die meiste Zeit damit verbringt, einen Teil seines Codes auszuführen, ist die verkürzte Kompilierungszeit erheblich. Schließlich können während der anfänglichen Codeinterpretation Ausführungsstatistiken vor der Kompilierung gesammelt werden, was zu einer besseren Optimierung beiträgt.

Der richtige Kompromiss kann je nach Umständen variieren. Die Java Virtual Machine von Sun hat beispielsweise zwei Hauptmodi – Client und Server. Im Client-Modus wird eine minimale Kompilierung und Optimierung durchgeführt, um die Startzeit zu verkürzen. Im Servermodus wird eine umfangreiche Kompilierung und Optimierung durchgeführt, um die Leistung zu maximieren, sobald die Anwendung ausgeführt wird, indem die Startzeit geopfert wird. Andere Java-Just-in-Time-Compiler haben eine Laufzeitmessung der Häufigkeit der Ausführung einer Methode in Kombination mit der Bytecode-Größe einer Methode als Heuristik verwendet, um zu entscheiden, wann kompiliert werden soll. Noch eine andere verwendet die Anzahl der ausgeführten Male in Kombination mit der Erkennung von Schleifen. Im Allgemeinen ist es viel schwieriger, genau vorherzusagen, welche Methoden bei Anwendungen mit kurzer Laufzeit optimiert werden müssen als bei Anwendungen mit langer Laufzeit.

Native Image Generator (Ngen) von Microsoft ist ein weiterer Ansatz zur Reduzierung der anfänglichen Verzögerung. Ngen kompiliert (oder "pre-JITs") Bytecode in einem Common Intermediate Language- Image in maschinennativen Code vor. Dadurch ist keine Laufzeitkompilierung erforderlich. .NET Framework 2.0, das mit Visual Studio 2005 geliefert wird, führt Ngen auf allen Microsoft-Bibliotheks-DLLs direkt nach der Installation aus. Pre-Jitting bietet eine Möglichkeit, die Startzeit zu verbessern. Die Qualität des generierten Codes ist jedoch möglicherweise nicht so gut wie der, der JIT-kompiliert ist, aus den gleichen Gründen, warum statisch kompilierter Code ohne profilgesteuerte Optimierung im Extremfall nicht so gut sein kann wie JIT-kompilierter Code: das Fehlen von Profiling-Daten, um beispielsweise Inline-Caching voranzutreiben.

Es gibt auch Java-Implementierungen, die einen AOT-Compiler (Ahead-of-Time) entweder mit einem JIT-Compiler ( Excelsior JET ) oder einem Interpreter ( GNU Compiler for Java ) kombinieren .

Sicherheit

Die JIT-Kompilierung verwendet grundsätzlich ausführbare Daten und birgt daher Sicherheitsherausforderungen und mögliche Exploits.

Die Implementierung der JIT-Kompilierung besteht darin, Quellcode oder Bytecode in Maschinencode zu kompilieren und auszuführen. Dies geschieht im Allgemeinen direkt im Speicher: Der JIT-Compiler gibt den Maschinencode direkt in den Speicher aus und führt ihn sofort aus, anstatt ihn auf die Festplatte auszugeben und den Code dann als separates Programm aufzurufen, wie es normalerweise vor der Kompilierung der Zeit üblich ist. In modernen Architekturen stößt dies aufgrund des Schutzes des ausführbaren Speicherplatzes auf ein Problem : Beliebiger Speicher kann nicht ausgeführt werden, da sonst eine potenzielle Sicherheitslücke besteht. Daher muss der Speicher als ausführbar markiert werden; aus Sicherheitsgründen sollte dies erfolgen, nachdem der Code in den Speicher geschrieben und als schreibgeschützt markiert wurde, da beschreibbarer/ausführbarer Speicher eine Sicherheitslücke ist (siehe W^X ). Zum Beispiel hat der JIT-Compiler für Javascript von Firefox diesen Schutz in einer Release-Version mit Firefox 46 eingeführt.

JIT-Spraying ist eine Klasse von Computersicherheits-Exploits , die die JIT-Kompilierung für das Heap-Spraying verwenden : Der resultierende Speicher ist dann ausführbar, was einen Exploit ermöglicht, wenn die Ausführung in den Heap verschoben werden kann.

Verwendet

Die JIT-Kompilierung kann auf einige Programme angewendet werden oder kann für bestimmte Kapazitäten verwendet werden, insbesondere für dynamische Kapazitäten wie reguläre Ausdrücke . Ein Texteditor kann beispielsweise einen zur Laufzeit bereitgestellten regulären Ausdruck in Maschinencode kompilieren, um einen schnelleren Abgleich zu ermöglichen: Dies ist nicht im Voraus möglich, da das Muster nur zur Laufzeit bereitgestellt wird. Mehrere moderne Laufzeitumgebungen verlassen sich auf die JIT-Kompilierung für die Hochgeschwindigkeitscodeausführung, einschließlich der meisten Java- Implementierungen zusammen mit dem .NET Framework von Microsoft . In ähnlicher Weise bieten viele Bibliotheken für reguläre Ausdrücke die JIT-Kompilierung von regulären Ausdrücken, entweder in Bytecode oder in Maschinencode. Die JIT-Kompilierung wird auch in einigen Emulatoren verwendet, um Maschinencode von einer CPU-Architektur in eine andere zu übersetzen.

Eine übliche Implementierung der JIT-Kompilierung besteht darin, zuerst die AOT-Kompilierung in Bytecode ( virtuelle Maschinencode ), bekannt als Bytecode-Kompilierung , und dann die JIT-Kompilierung in Maschinencode (dynamische Kompilierung), anstatt den Bytecode zu interpretieren. Dies verbessert die Laufzeitleistung im Vergleich zur Interpretation auf Kosten der Verzögerung aufgrund der Kompilierung. JIT-Compiler übersetzen kontinuierlich, wie bei Interpretern, aber das Zwischenspeichern von kompiliertem Code minimiert die Verzögerung bei der zukünftigen Ausführung desselben Codes während einer bestimmten Ausführung. Da nur ein Teil des Programms kompiliert wird, gibt es deutlich weniger Verzögerungen, als wenn das gesamte Programm vor der Ausführung kompiliert würde.

Siehe auch

Anmerkungen

Verweise

Weiterlesen

Externe Links