[Übersetzung] Funktionsweise von Graal - Java JVM JVM Compiler

Hallo Habr! Ich präsentiere Ihnen die Übersetzung des Artikels " Verstehen, wie Graal funktioniert - ein in Java geschriebener Java JIT-Compiler ".


Einführung


Einer der Gründe, warum ich Forscher in Programmiersprachen wurde, ist, dass in einer großen Gemeinschaft von Menschen, die mit Computertechnologie zu tun haben, fast jeder Programmiersprachen verwendet und viele daran interessiert sind, wie sie funktionieren. Als ich als Kind zum ersten Mal auf das Programmieren stieß und eine Programmiersprache kennenlernte, wollte ich zunächst wissen, wie es funktioniert, und als erstes wollte ich meine eigene Sprache erstellen.


In dieser Präsentation zeige ich Ihnen einige der Arbeitsmechanismen der Sprache, die Sie alle verwenden - Java. Die Besonderheit ist, dass ich ein Projekt namens Graal verwenden werde , das das Konzept von Java in Java implementiert.


Graal ist nur eine der Komponenten in der Arbeit von Java - es ist ein Just-in-Time- Compiler. Dies ist der Teil der JVM, der Java-Bytecode während der Programmausführung in Maschinencode konvertiert, und einer der Faktoren, die eine hohe Plattformleistung gewährleisten. Es scheint mir auch, dass die meisten Leute dies als einen der komplexesten und vagesten Teile der JVM betrachten, was außerhalb ihres Verständnisses liegt. Diese Meinung zu ändern ist der Zweck dieser Rede.


Wenn Sie wissen, was eine JVM ist; im Allgemeinen verstehen, was die Begriffe Bytecode und Maschinencode bedeuten; und in der Lage, in Java geschriebenen Code zu lesen, dann hoffe ich, dass dies ausreicht, um das präsentierte Material zu verstehen.


Ich werde zunächst diskutieren, warum wir möglicherweise einen neuen JIT-Compiler für die in Java geschriebene JVM wollen, und danach werde ich zeigen, dass dies nichts Besonderes ist, wie Sie vielleicht denken, indem ich die Aufgabe in die Zusammenstellung, Verwendung und Demonstration des Compilers aufteile dass sein Code der gleiche ist wie in jeder anderen Anwendung.


Ich werde die Theorie ein wenig ansprechen und dann zeigen, wie sie während des gesamten Kompilierungsprozesses vom Bytecode bis zum Maschinencode angewendet wird. Ich werde auch einige Details zeigen, und am Ende werden wir über die Vorteile dieser Funktion sprechen, zusätzlich zur Implementierung von Java in Java für sich.


Ich werde die Screenshots des Codes in Eclipse verwenden, anstatt sie während der Präsentation zu starten, um die unvermeidlichen Probleme der Live-Codierung zu vermeiden.


Was ist ein JIT-Compiler?


Ich bin sicher, dass viele von Ihnen wissen, was ein JIT-Compiler ist, aber ich werde trotzdem auf die Grundlagen eingehen, damit hier niemand Angst hat, diese Hauptfrage zu stellen.


Wenn Sie den Befehl javac oder compile-on-save in der IDE ausführen, kompiliert Ihr Java-Programm vom Java-Code zum JVM-Bytecode, der die binäre Darstellung des Programms darstellt. Es ist kompakter und einfacher als der Java-Quellcode. Der normale Prozessor Ihres Laptops oder Servers kann jedoch nicht nur den JVM-Bytecode ausführen.


Für den Betrieb Ihres Programms interpretiert die JVM diesen Bytecode. Dolmetscher sind normalerweise viel langsamer als Maschinencode, der auf dem Prozessor ausgeführt wird. Aus diesem Grund kann die JVM während der Ausführung des Programms einen anderen Compiler starten, der Ihren Bytecode in Maschinencode konvertiert, den der Prozessor bereits ausführen kann.


Dieser Compiler, der normalerweise viel ausgefeilter als javac , führt komplexe Optimierungen durch, um als Ergebnis hochwertigen Maschinencode zu erzeugen.


Warum einen JIT-Compiler in Java schreiben?


Bisher enthält eine JVM-Implementierung namens OpenJDK zwei Haupt-JIT-Compiler. Der als C1 bekannte Client-Compiler ist für einen schnelleren Betrieb ausgelegt, erzeugt jedoch gleichzeitig weniger optimierten Code. Der als opto oder C2 bekannte Server-Compiler benötigt etwas mehr Zeit zum Arbeiten, erzeugt jedoch optimierten Code.


Die Idee war, dass der Client-Compiler besser für Desktop-Anwendungen geeignet war, bei denen lange Pausen im JIT-Compiler unerwünscht waren, und der Server-Compiler für lang laufende Server-Anwendungen, die mehr Zeit für das Kompilieren aufwenden konnten.


Heute können sie so kombiniert werden, dass der Code zuerst von C1 kompiliert wird. Wenn er dann weiterhin intensiv ausgeführt wird und es sinnvoll ist, zusätzliche Zeit zu verbringen, - C2. Dies wird als gestufte Kompilierung bezeichnet .


Lassen Sie uns auf C2 eingehen, den Server-Compiler, der weitere Optimierungen durchführt.


Wir können OpenJDK aus dem Spiegel auf GitHub klonen oder einfach den Projektbaum auf der Site öffnen.


 $ git clone https://github.com/dmlloyd/openjdk.git 

Der C2-Code befindet sich in openjdk / hotspot / src / share / vm / opto .


Quellcode c2


Zunächst ist anzumerken, dass C2 in C ++ geschrieben ist . Das ist natürlich nichts Schlimmes, aber es gibt bestimmte Nachteile. C ++ ist eine unsichere Sprache. Dies bedeutet, dass Fehler in C ++ die VM zum Absturz bringen können. Der Grund dafür ist wahrscheinlich das Alter des Codes, aber C2 C ++ - Code ist sehr schwierig zu warten und zu entwickeln.


Cliff Click, eine der Schlüsselfiguren hinter dem C2-Compiler, sagte, dass er VM nie wieder in C ++ schreiben würde, und wir hörten das Twitter- JVM-Team sagen, dass C2 stagniert und aus einem bestimmten Grund ersetzt werden muss Schwierigkeiten bei der Weiterentwicklung.


Cliff klicken auf Darstellungation


Was Cliff Click nicht noch einmal machen macht


https://www.youtube.com/watch?v=Hqw57GJSrac


Zurück zur Frage: Was kann Java in Java zur Lösung dieser Probleme beitragen? Das gleiche, was das Schreiben eines Programms in Java anstelle von C ++ ermöglicht. Dies ist wahrscheinlich Sicherheit (Ausnahmen anstelle von Abstürzen, kein wirklicher Speicherverlust oder baumelnde Zeiger), gute Helfer (Debugger, Profiler und Tools wie VisualVM ), gute IDE-Unterstützung usw.


Sie könnten denken: Wie kann ich so etwas wie einen Java JIT-Compiler schreiben? und dass dies nur in einer einfachen Systemprogrammiersprache wie C ++ möglich ist. Ich hoffe, Sie in dieser Präsentation davon zu überzeugen, dass dies überhaupt nicht der Fall ist! Im Wesentlichen sollte der JIT-Compiler nur den JVM-Bytecode akzeptieren und den Maschinencode ausgeben - Sie geben ihm byte[] am Eingang und Sie möchten auch byte[] . Dies erfordert viel komplexe Arbeit, wirkt sich jedoch nicht auf die Systemebene aus und erfordert daher keine Systemsprache wie C oder C ++.


Graal Setup


Das erste, was wir brauchen, ist Java 9. Die verwendete Graal-Schnittstelle namens JVMCI wurde Java als Teil der JEP 243 Java-Level-JVM-Compiler-Schnittstelle hinzugefügt. Die erste Version, die sie enthält, ist Java 9. Ich verwende 9 + 181 . Bei besonderen Anforderungen gibt es Ports (Backports) für Java 8.


 $ export JAVA_HOME=`pwd`/jdk9 $ export PATH=$JAVA_HOME/bin:$PATH $ java -version java version "9" Java(TM) SE Runtime Environment (build 9+181) Java HotSpot(TM) 64-Bit Server VM (build 9+181, mixed mode) 

Das nächste, was wir brauchen, ist ein Build-System namens mx . Es ist ein bisschen wie Maven oder Gradle , aber höchstwahrscheinlich würden Sie es nicht für Ihre Anwendung auswählen. Es implementiert bestimmte Funktionen, um einige komplexe Anwendungsfälle zu unterstützen. Wir werden sie jedoch nur für einfache Assemblys verwenden.


Sie können mx mit GitHub klonen. Ich verwende Commit #7353064 . Fügen Sie nun einfach die ausführbare Datei zum Pfad hinzu.


 $ git clone https://github.com/graalvm/mx.git $ cd mx; git checkout 7353064 $ export PATH=`pwd`/mx:$PATH 

Jetzt müssen wir Graal selbst klonen. Ich verwende eine Distribution namens GraalVM Version 0.28.2 .


 $ git clone https://github.com/graalvm/graal.git --branch vm-enterprise-0.28.2 

Dieses Repository enthält andere Projekte, an denen wir nicht interessiert sind. Gehen Sie also einfach zum Compiler- Unterprojekt, dem Graal JIT-Compiler, und kompilieren Sie es mit mx .


 $ cd graal/compiler $ mx build 

Um mit Graal-Code zu arbeiten, verwende ich die Eclipse-IDE . Ich benutze Eclipse 4.7.1. mx kann Eclipse-Projektdateien für uns generieren.


 $ mx eclipseinit 

Um das Graal- Verzeichnis als Arbeitsbereich zu öffnen , müssen Sie Datei, Importieren ..., Allgemein, Bestehende Projekte ausführen und das Graal- Verzeichnis erneut auswählen. Wenn Sie Eclipse nicht unter Java 9 ausgeführt haben, müssen Sie möglicherweise auch die JDK-Quellen anhängen.


Graal in der Finsternis


Gut. Jetzt, da alles fertig ist, wollen wir sehen, wie es funktioniert. Wir werden diesen sehr einfachen Code verwenden.


 class Demo { public static void main(String[] args) { while (true) { workload(14, 2); } } private static int workload(int a, int b) { return a + b; } } 

Zuerst kompilieren wir diesen javac Code und führen dann die JVM aus. Zunächst zeige ich Ihnen, wie der Standard-C2-JIT-Compiler funktioniert. Dazu geben wir mehrere Flags an: -XX:+PrintCompilation , die erforderlich sind, damit die JVM beim Kompilieren einer Methode ein Protokoll schreibt, und -XX:CompileOnly=Demo::workload , damit nur diese Methode kompiliert wird. Wenn dies nicht der Fall ist, werden zu viele Informationen angezeigt, und die JVM ist intelligenter als erforderlich und optimiert den gewünschten Code.


 $ javac Demo.java $ java \ -XX:+PrintCompilation \ -XX:CompileOnly=Demo::workload \ Demo ... 113 1 3 Demo::workload (4 bytes) ... 

Ich werde dies nicht im Detail erklären, aber ich werde nur sagen, dass dies eine Protokollausgabe ist, die zeigt, dass die workload Methode kompiliert wurde.


Als JIT-Compiler unserer Java 9-JVM verwenden wir jetzt das frisch kompilierte Graal. Fügen Sie dazu einige weitere Flags hinzu.


--module-path=... und --upgrade-module-path=... fügen Graal zum --upgrade-module-path=... hinzu. Ich möchte Sie daran erinnern, dass der Modulpfad in Java 9 als Teil des Jigsaw- Modulsystems angezeigt wurde . Für unsere Zwecke können wir ihn analog zum Klassenpfad betrachten .


Wir benötigen das -XX:+UnlockExperimentalVMOptions , da JVMCI (die von Graal verwendete Schnittstelle) in dieser Version eine experimentelle Funktion ist.


Das Flag -XX:+EnableJVMCI benötigt, um zu sagen, dass wir JVMCI verwenden möchten, und -XX:+UseJVMCICompiler - um einen neuen JIT-Compiler zu aktivieren und zu installieren.


Um das Beispiel nicht zu komplizieren und anstatt C1 in Verbindung mit der JVMCI zu verwenden, nur den JVMCI-Compiler zu verwenden, geben Sie das Flag -XX:-TieredCompilation , wodurch die schrittweise Kompilierung deaktiviert wird.


Wie zuvor geben wir die Flags -XX:+PrintCompilation und -XX:CompileOnly=Demo::workload .


Wie im vorherigen Beispiel sehen wir, dass eine Methode kompiliert wurde. Aber dieses Mal haben wir für die Kompilierung nur zusammengesetztes Graal verwendet. Fürs Erste, nimm einfach mein Wort dafür.


 $ java \ --module-path=graal/sdk/mxbuild/modules/org.graalvm.graal_sdk.jar:graal/truffle/mxbuild/modules/com.oracle.truffle.truffle_api.jar \ --upgrade-module-path=graal/compiler/mxbuild/modules/jdk.internal.vm.compiler.jar \ -XX:+UnlockExperimentalVMOptions \ -XX:+EnableJVMCI \ -XX:+UseJVMCICompiler \ -XX:-TieredCompilation \ -XX:+PrintCompilation \ -XX:CompileOnly=Demo::workload \ Demo ... 583 25 Demo::workload (4 bytes) ... 

JVM-Compiler-Schnittstelle


Denken Sie nicht, dass wir etwas ziemlich Ungewöhnliches gemacht haben? Wir haben eine JVM installiert und den JIT-Compiler durch den gerade kompilierten neuen ersetzt, ohne etwas an der JVM selbst zu ändern. Diese Funktion wird von einer neuen JVM-Schnittstelle namens JVMCI bereitgestellt, der JVM-Compiler-Schnittstelle , die, wie oben erwähnt, JEP 243 war und in Java 9 geliefert wurde.


Die Idee ähnelt einigen anderen vorhandenen JVM-Technologien.


Vielleicht sind Sie jemals auf zusätzliche Quellcodeverarbeitung in javac mit der Java-API zur Annotationsverarbeitung gestoßen . Dieser Mechanismus ermöglicht es, Anmerkungen und das Quellcodemodell, in dem sie verwendet werden, zu identifizieren und basierend darauf neue Dateien zu erstellen.


Möglicherweise haben Sie auch zusätzliche Bytecode-Verarbeitung in der JVM mithilfe von Java-Agenten verwendet . Mit diesem Mechanismus können Sie den Java-Bytecode ändern, indem Sie ihn beim Booten abfangen.


Die Idee von JVMCI ist ähnlich. Sie können damit Ihren eigenen in Java geschriebenen Java JIT-Compiler verbinden.


Jetzt möchte ich ein paar Worte darüber sagen, wie ich den Code während dieser Präsentation zeigen werde. Um die Idee zu verstehen, werde ich zunächst einige vereinfachte Bezeichner und Logik in Form von Text auf Folien zeigen. Danach werde ich zu Eclipse-Screenshots wechseln und den realen Code zeigen, der etwas komplizierter sein kann, aber die Hauptidee bleibt dieselbe. Der Hauptteil dieser Präsentation soll zeigen, dass es wirklich möglich ist, mit dem realen Projektcode zu arbeiten, und deshalb möchte ich ihn nicht ausblenden, obwohl er etwas kompliziert sein kann.


Von nun an zerstreue ich die Meinung, dass der JIT-Compiler möglicherweise sehr kompliziert ist.


Was akzeptiert der JIT-Compiler für die Eingabe? Es akzeptiert den Bytecode der zu kompilierenden Methode. Und der Bytecode ist, wie der Name schon sagt, nur ein Array von Bytes.


Was produziert der JIT-Compiler als Ergebnis? Es gibt den Maschinencode der Methode aus. Maschinencode ist auch nur ein Array von Bytes.


Infolgedessen sieht die Schnittstelle, die beim Schreiben eines neuen JIT-Compilers zum Einbetten in die JVM implementiert werden muss, ungefähr so ​​aus.


 interface JVMCICompiler { byte[] compileMethod(byte[] bytecode); } 

Wenn Sie sich also nicht vorstellen können, wie Java etwas so Niedriges wie die JIT-Kompilierung in Maschinencode ausführen kann, können Sie jetzt sehen, dass dies keine so niedrige Arbeit ist. Richtig? Dies ist nur eine Funktion von byte[] zu byte[] .


In Wirklichkeit ist alles etwas komplizierter. Nur Bytecode reicht nicht aus - wir benötigen außerdem weitere Informationen wie die Anzahl der lokalen Variablen, die erforderliche Stapelgröße und die vom Interpreter-Profiler gesammelten Informationen, um zu verstehen, wie der Code tatsächlich ausgeführt wird. Stellen Sie sich daher die Eingabe in Form von CompilationRequest , die uns über die zu kompilierende JavaMethod und alle erforderlichen Informationen bereitstellt.


 interface JVMCICompiler { void compileMethod(CompilationRequest request); } interface CompilationRequest { JavaMethod getMethod(); } interface JavaMethod { byte[] getCode(); int getMaxLocals(); int getMaxStackSize(); ProfilingInfo getProfilingInfo(); ... } 

Außerdem erfordert die Schnittstelle keine Rückgabe von kompiliertem Code. Stattdessen wird eine andere API verwendet, um den Maschinencode in der JVM zu installieren.


 HotSpot.installCode(targetCode); 

Um einen neuen JIT-Compiler für die JVM zu schreiben, müssen Sie nur diese Schnittstelle implementieren. Wir erhalten Informationen über die Methode, die kompiliert werden muss, und wir müssen sie in Maschinencode kompilieren und installCode .


 class GraalCompiler implements JVMCICompiler { void compileMethod(CompilationRequest request) { HotSpot.installCode(...); } } 

Wechseln wir mit Graal zur Eclipse-IDE und sehen uns einige echte Schnittstellen und Klassen an. Wie bereits erwähnt, werden sie etwas komplizierter sein, aber nicht viel.


JVMCICompiler


HotSpotGraalCompiler


compileMethod


Jetzt möchte ich zeigen, dass wir Änderungen an Graal vornehmen und diese sofort in Java 9 verwenden können. Ich werde eine neue Protokollmeldung hinzufügen, die beim Kompilieren der Methode mit Graal angezeigt wird. Fügen Sie es der implementierten Schnittstellenmethode hinzu, die von der JVMCI aufgerufen wird.


 class HotSpotGraalCompiler implements JVMCICompiler { CompilationRequestResult compileMethod(CompilationRequest request) { System.err.println("Going to compile " + request.getMethod().getName()); ... } } 

Zusammenstellung


Deaktivieren Sie vorerst die Kompilierungsprotokollierung in HotSpot. Jetzt können wir unsere Nachricht von der geänderten Version des Compilers sehen.


 $ java \ --module-path=graal/sdk/mxbuild/modules/org.graalvm.graal_sdk.jar:graal/truffle/mxbuild/modules/com.oracle.truffle.truffle_api.jar \ --upgrade-module-path=graal/compiler/mxbuild/modules/jdk.internal.vm.compiler.jar \ -XX:+UnlockExperimentalVMOptions \ -XX:+EnableJVMCI \ -XX:+UseJVMCICompiler \ -XX:-TieredCompilation \ -XX:CompileOnly=Demo::workload \ Demo Going to compile workload 

Wenn Sie versuchen, dies selbst zu wiederholen, werden Sie feststellen, dass Sie nicht einmal unser Build-System - mx build - ausführen müssen. Genug, normal für Eclipse, beim Speichern kompilieren . Und natürlich müssen wir die JVM selbst nicht neu erstellen. Wir binden den geänderten Compiler einfach in die vorhandene JVM ein.


Graf Graal


Nun, wir wissen, dass Graal ein byte[] in ein anderes byte[] konvertiert. Lassen Sie uns nun über die Theorie und Datenstrukturen sprechen, die er verwendet, weil Sie sind selbst für den Compiler etwas ungewöhnlich.


Im Wesentlichen verwaltet der Compiler Ihr Programm. Dazu muss das Programm in Form einer Datenstruktur dargestellt werden. Eine Option ist Bytecode und ähnliche Anweisungslisten, die jedoch nicht sehr aussagekräftig sind.


Stattdessen verwendet Graal ein Diagramm, um Ihr Programm darzustellen. Wenn wir einen einfachen Additionsoperator verwenden, der zwei lokale Variablen zusammenfasst, enthält das Diagramm einen Knoten zum Laden jeder Variablen, einen Knoten für die Summe und zwei Kanten, die zeigen, dass das Ergebnis des Ladens der lokalen Variablen in den Additionsoperator eingegeben wird.


Dies wird manchmal als Programmabhängigkeitsgraph bezeichnet .


Mit einem Ausdruck der Form x + y erhalten wir Knoten für die lokalen Variablen x und y und einen Knoten ihrer Summe.


Datenflussdiagramm


Die blauen Ränder in diesem Diagramm zeigen die Richtung des Datenflusses vom Lesen lokaler Variablen bis zur Summierung.


Wir können auch Kanten verwenden, um die Ausführungsreihenfolge des Programms widerzuspiegeln. Wenn wir anstelle des Lesens lokaler Variablen Methoden aufrufen, müssen wir uns die Reihenfolge des Aufrufs merken und können sie nicht neu anordnen (ohne den darin enthaltenen Code zu kennen). Dazu gibt es zusätzliche Kanten, die diese Reihenfolge definieren. Sie sind rot dargestellt.


Kontrollflussdiagramm


Das Graal-Diagramm besteht also aus zwei Diagrammen, die in einem zusammengefasst sind. Die Knoten sind gleich, aber einige Kanten geben die Richtung des Datenstroms an, während andere die Übertragung der Kontrolle zwischen ihnen anzeigen.


Um das Graal-Diagramm anzuzeigen , können Sie ein Tool namens IdealGraphVisualiser oder IGV verwenden . Der Start erfolgt mit dem mx igv .


IdealGraphVisualiser


Führen Sie danach die JVM mit dem Flag -Dgraal.Dump .


Ein einfacher Datenstrom kann durch Schreiben eines einfachen Ausdrucks angezeigt werden.


 int average(int a, int b) { return (a + b) / 2; } 

Arithmetisches mittel igv


Sie können sehen, wie die Parameter 0 ( P(0 ) und 1 ( P(1) ) zum Eingang der Additionsoperation gehen, die zusammen mit der Konstanten 2 ( C(2) ) zum Eingang der Divisionsoperation gehen, wonach dieser Wert zurückgegeben wird.


Um einen komplexeren Daten- und Kontrollfluss zu betrachten, führen wir einen Zyklus ein.


 int average(int[] values) { int sum = 0; for (int n = 0; n < values.length; n++) { sum += values[n]; } return sum / values.length; } 

Arithmetisches Mittel igv mit Zyklus


Arithmetisches Medium Detailliertes IGV mit Schleife


In diesem Fall haben wir Knoten am Anfang und Ende der Schleife, die die Elemente des Arrays lesen und die Länge des Arrays lesen. Nach wie vor geben die blauen Linien die Richtung des Datenstroms und die roten Linien den Kontrollfluss an.


Jetzt können Sie sehen, warum diese Datenstruktur manchmal als Knotenmeer oder Knotensuppe bezeichnet wird .


Ich möchte sagen, dass C2 eine sehr ähnliche Datenstruktur verwendet, und tatsächlich war es C2, das die Idee eines Compilers aus einem Meer von Knoten populär machte, sodass dies keine Innovation von Graal ist.


Ich werde den Prozess der Erstellung dieses Diagramms erst im nächsten Teil der Präsentation zeigen, aber wenn Graal das Programm in diesem Format erhält, wird die Optimierung und Kompilierung durch Ändern dieser Datenstruktur durchgeführt. Dies ist einer der Gründe, warum das Schreiben eines JIT-Compilers in Java sinnvoll ist. Java ist eine objektorientierte Sprache, und ein Diagramm ist eine Sammlung von Objekten, die durch Kanten in Form von Links verbunden sind.


Vom Bytecode zum Maschinencode


Lassen Sie uns sehen, wie diese Ideen in der Praxis aussehen, und einige Schritte des Kompilierungsprozesses befolgen.


Bytecode erhalten


Die Kompilierung beginnt mit dem Bytecode. Kehren wir zu unserem kleinen Summationsbeispiel zurück.


 int workload(int a, int b) { return a + b; } 

Wir geben den am Eingang empfangenen Bytecode kurz vor Beginn der Kompilierung aus.


 class HotSpotGraalCompiler implements JVMCICompiler { CompilationRequestResult compileMethod(CompilationRequest request) { System.err.println(request.getMethod().getName() + " bytecode: " + Arrays.toString(request.getMethod().getCode())); ... } } 

 workload bytecode: [26, 27, 96, -84] 

Wie Sie sehen können, ist die Eingabe in den Compiler Bytecode.


Bytecode-Parser und Graph Builder


Der Builder , der dieses Byte-Array als JVM-Bytecode wahrnimmt, konvertiert es in ein Graal-Diagramm. Dies ist eine Art abstrakte Interpretation - der Builder interpretiert den Java-Bytecode, manipuliert jedoch anstelle der Übergabe von Werten die freien Enden der Kanten und verbindet sie schrittweise miteinander.


Nutzen wir die Tatsache, dass Graal in Java geschrieben ist, und sehen wir, wie es mit den Eclipse-Navigationswerkzeugen funktioniert. Wir wissen, dass es in unserem Beispiel einen Additionsknoten gibt. Lassen Sie uns also herausfinden, wo er erstellt wurde.


Addnode


Suchaufrufe AddNode.create


Parser


Es ist ersichtlich, dass sie vom Bytecode-Parser erstellt werden, und dies führte uns zum IADD Verarbeitungscode ( 96 , den wir im gedruckten Eingabearray gesehen haben).


 private void genArithmeticOp(JavaKind kind, int opcode) { ValueNode y = frameState.pop(kind); ValueNode x = frameState.pop(kind); ValueNode v; switch (opcode) { ... case LADD: v = genIntegerAdd(x, y); break; ... } frameState.push(kind, append(v)); } 

Ich habe oben gesagt, dass dies eine abstrakte Interpretation ist, weil All dies ist einem Bytecode-Interpreter sehr ähnlich. Wenn es sich um einen echten JVM-Interpreter handeln würde, würde er zwei Werte vom Stapel nehmen, eine Addition durchführen und das Ergebnis zurücksetzen. In diesem Fall entfernen wir zwei Knoten aus dem Stapel, die beim Start des Programms Berechnungen sind, fügen, was das Ergebnis der Summierung ist, einen neuen Knoten zum Hinzufügen hinzu und platzieren ihn auf dem Stapel.


Somit ist der Graph Graal aufgebaut.


Maschinencode abrufen


Um ein Graal-Diagramm in Maschinencode zu konvertieren, müssen Sie Bytes für alle seine Knoten generieren. Dies erfolgt für jeden Knoten separat, indem seine generate aufgerufen wird.


 void generate(Generator gen) { gen.emitAdd(a, b); } 

Ich wiederhole, hier arbeiten wir auf einem sehr hohen Abstraktionsniveau. Wir haben eine Klasse, mit der wir Maschinencode-Anweisungen erteilen, ohne auf Einzelheiten der Funktionsweise einzugehen.


emitAdd , , , , . .


 int workload(int a) { return a + 1; } 

, .


 void incl(Register dst) { int encode = prefixAndEncode(dst.encoding); emitByte(0xFF); emitByte(0xC0 | encode); } void emitByte(int b) { data.put((byte) (b & 0xFF)); } 

Montageanleitung im Assembler


Byte-Ausgabe


, , ByteBuffer — .



— .


 class HotSpotGraalCompiler implements JVMCICompiler { CompilationResult compileHelper(...) { ... System.err.println(method.getName() + " machine code: " + Arrays.toString(result.getTargetCode())); ... } } 

Druckmaschinencode


. HotSpot. . OpenJDK, , -, JVM, .


 $ cd openjdk/hotspot/src/share/tools/hsdis $ curl -O http://ftp.heanet.ie/mirrors/gnu/binutils/binutils-2.24.tar.gz $ tar -xzf binutils-2.24.tar.gz $ make BINUTILS=binutils-2.24 ARCH=amd64 CFLAGS=-Wno-error $ cp build/macosx-amd64/hsdis-amd64.dylib ../../../../../.. 

: -XX:+UnlockDiagnosticVMOptions -XX:+PrintAssembly .


 $ java \ --module-path=graal/sdk/mxbuild/modules/org.graalvm.graal_sdk.jar:graal/truffle/mxbuild/modules/com.oracle.truffle.truffle_api.jar \ --upgrade-module-path=graal/compiler/mxbuild/modules/jdk.internal.vm.compiler.jar \ -XX:+UnlockExperimentalVMOptions \ -XX:+EnableJVMCI \ -XX:+UseJVMCICompiler \ -XX:-TieredCompilation \ -XX:+PrintCompilation \ -XX:+UnlockDiagnosticVMOptions \ -XX:+PrintAssembly \ -XX:CompileOnly=Demo::workload \ Demo 

.


 workload machine code: [15, 31, 68, 0, 0, 3, -14, -117, -58, -123, 5, ...] ... 0x000000010f71cda0: nopl 0x0(%rax,%rax,1) 0x000000010f71cda5: add %edx,%esi ;\*iadd {reexecute=0 rethrow=0 return_oop=0} ; - Demo::workload@2 (line 10) 0x000000010f71cda7: mov %esi,%eax ;\*ireturn {reexecute=0 rethrow=0 return_oop=0} ; - Demo::workload@3 (line 10) 0x000000010f71cda9: test %eax,-0xcba8da9(%rip) # 0x0000000102b74006 ; {poll_return} 0x000000010f71cdaf: vzeroupper 0x000000010f71cdb2: retq 

. , . generate , .


 class AddNode { void generate(...) { ... gen.emitSub(op1, op2, false) ... // changed from emitAdd } } 

Ausgabe der AddNode-Anweisung


, , , .


 workload mechine code: [15, 31, 68, 0, 0, 43, -14, -117, -58, -123, 5, ...] 0x0000000107f451a0: nopl 0x0(%rax,%rax,1) 0x0000000107f451a5: sub %edx,%esi ;\*iadd {reexecute=0 rethrow=0 return_oop=0} ; - Demo::workload@2 (line 10) 0x0000000107f451a7: mov %esi,%eax ;\*ireturn {reexecute=0 rethrow=0 return_oop=0} ; - Demo::workload@3 (line 10) 0x0000000107f451a9: test %eax,-0x1db81a9(%rip) # 0x000000010618d006 ; {poll_return} 0x0000000107f451af: vzeroupper 0x0000000107f451b2: retq 

, ? Graal ; ; ; . , Graal.


 [26, 27, 96, -84] → [15, 31, 68, 0, 0, 43, -14, -117, -58, -123, 5, ...] 


, , . Graal , .


— . .


 interface Phase { void run(Graph graph); } 

(canonicalisation)


. , , ( constant folding ) .


canonical .


 interface Node { Node canonical(); } 

, , , . — . -(-x) x .


 class NegateNode implements Node { Node canonical() { if (value instanceof NegateNode) { return ((NegateNode) value).getValue(); } else { return this; } } } 

findSynonym in NegateNode


Graal . , .


Java, canonical .


Global value numbering


Global value numbering (GVN) — . a + b , — .


 int workload(int a, int b) { return (a + b) * (a + b); } 

Graal . — . GVN . hash map , , .


Globale Wertnummerierung


Diagramm zur Nummerierung globaler Werte


, — , , - . , , , , — .


 int workload() { return (getA() + getB()) * (getA() + getB()); } 

Ein Diagramm, dessen globaler Wert nicht nummeriert werden kann


(lock coarsening)


. . , , , ( inlining ).


 void workload() { synchronized (monitor) { counter++; } synchronized (monitor) { counter++; } } 

, , , , .


 void workload() { monitor.enter(); counter++; monitor.exit(); monitor.enter(); counter++; monitor.exit(); } 

. .


 void workload() { monitor.enter(); counter++; counter++; monitor.exit(); } 

Graal LockEliminationPhase . run , . , , .


 void run(StructuredGraph graph) { for (monitorExitNode monitorExitNode : graph.getNodes(MonitorExitNode.class)) { FixedNode next = monitorExitNode.next(); if (next instanceof monitorEnterNode) { AccessmonitorNode monitorEnterNode = (AccessmonitorNode) next; if (monitorEnterNode.object() ## monitorExitNode.object()) { monitorExitNode.remove(); monitorEnterNode.remove(); } } } } 

Vereinfachung der Sperre


, , , , 2 .


 void workload() { monitor.enter(); counter += 2; monitor.exit(); } 

IGV . , , \ , , , 2 .


Vereinfachen Sie Sperren auf


Vereinfachen Sie Sperren nach



Graal , , , . , , , .


Graal , , , , , , .



Graal , , . ? , ?


, , . . , , , , . , , , , , .


( register allocation ). Graal , JIT-, — ( linear scan algorithm ).



, , , - , .


, , , , (.. ), . , , .


( graph scheduling ). . , . , , , .


, .


Graal?


, , , Graal — , Oracle . , Graal?


(final-tier compiler)


C JVMCI Graal HotSpot — , . ( HotSpot) Graal , .


Twitter Graal , Java 9 . : -XX:+UseJVMCICompiler .


JVMCI , Graal JVM. (deploy) - JVM, Graal. Java-, Graal, JVM.


OpenJDK Metropolis JVM Java. Graal .


Metropolis-Projekt


http://cr.openjdk.java.net/\~jrose/metropolis/Metropolis-Proposal.html



Graal . Graal JVM, Graal . , Graal . , - , , JNI.


Charles Nutter JRuby Graal Ruby. , - .


AOT (ahead-of-time)


Graal — Java. JVMCI , Graal, , , Graal . , Graal , JIT-.


JIT- AOT- , Graal . AOT Graal.


Java 9 JIT-, . JVM, .


AOT Java 9 Graal, Linux. , , .


. SubstrateVM — AOT-, Java- JVM . , - (statically linked) . JVM , . SubstrateVM Graal. ( just-in-time ) SubstrateVM, , Graal . Graal AOT- .


 $ javac Hello.java $ graalvm-0.28.2/bin/native-image Hello classlist: 966.44 ms (cap): 804.46 ms setup: 1,514.31 ms (typeflow): 2,580.70 ms (objects): 719.04 ms (features): 16.27 ms analysis: 3,422.58 ms universe: 262.09 ms (parse): 528.44 ms (inline): 1,259.94 ms (compile): 6,716.20 ms compile: 8,817.97 ms image: 1,070.29 ms debuginfo: 672.64 ms write: 1,797.45 ms [total]: 17,907.56 ms $ ls -lh hello -rwxr-xr-x 1 chrisseaton staff 6.6M 4 Oct 18:35 hello $ file ./hello ./hellojava: Mach-O 64-bit executable x86_64 $ time ./hello Hello! real 0m0.010s user 0m0.003s sys 0m0.003s 

Truffle


Graal Truffle . Truffle — JVM.


, JVM, , JIT- (, , , JIT- JVM , ). Truffle — , , Truffle, , ( partial evaluation ).


, ( inlining ) ( constant folding ) . Graal , Truffle .


Graal — Truffle. Ruby, TruffleRuby Truffle , , Graal. TruffleRuby — Ruby, 10 , , , .


https://github.com/graalvm/truffleruby


Schlussfolgerungen


, , , JIT- Java . JIT- , , , - . , , . JIT- , byte[] JVM byte[] .


, Java. , C++.


Java- Graal - . , , .


. , Eclipse . (definitions), .. .


JIT JIT- JVM, JITWatch , , Graal , . , , - , Graal JVM. IDE, hello-world .


SubstrateVM Truffle, Graal, , Java . , Graal Java. , , - LLVM , , , , .


, , Graal JVM. Weil JVMCI Java 9, Graal , , Java-.


Graal — . , Graal. , !




More information about TruffleRuby


Low Overhead Polling For Ruby


Top 10 Things To Do With GraalVM


Ruby Objects as C Structs and Vice Versa


Understanding How Graal Works — a Java JIT Compiler Written in Java


Flip-Flops — the 1-in-10-million operator


Deoptimizing Ruby


Very High Performance C Extensions For JRuby+Truffle


Optimising Small Data Structures in JRuby+Truffle


Pushing Pixels with JRuby+Truffle


Tracing With Zero Overhead in JRuby+Truffle


How Method Dispatch Works in JRuby+Truffle


A Truffle/Graal High Performance Backend for JRuby

Source: https://habr.com/ru/post/de419637/


All Articles