Tiefer Einstieg in den neuen Java JIT Compiler - Graal

Tauchen Sie ein in den neuen Java JIT Compiler - Graal

1. Überblick

In diesem Tutorial werden wir uns den neuen Java Just-In-Time-Compiler (JIT) mit dem Namen Graal genauer ansehen.

Wir werden sehen, was das ProjektGraal ist, und einen seiner Teile beschreiben, einen dynamischen Hochleistungs-JIT-Compiler.

2. Was ist ein JIT-Compiler?

Lassen Sie uns zunächst erklären, was der JIT-Compiler tut.

When we compile our Java program (e.g., using the javac command), we’ll end up with our source code compiled into the binary representation of our code – a JVM bytecode. Dieser Bytecode ist einfacher und kompakter als unser Quellcode, aber herkömmliche Prozessoren in unseren Computern können ihn nicht ausführen.

To be able to run a Java program, the JVM interprets the bytecode. Da Interpreter normalerweise viel langsamer sind als nativer Code, der auf einem realen Prozessor ausgeführt wird, sind dieJVM can run another compiler which will now compile our bytecode into the machine code that can be run by the processor. Dieser sogenannte Just-in-Time-Compiler ist viel ausgefeilter als derjavac-Compiler und führt komplexe Optimierungen aus, um qualitativ hochwertigen Maschinencode zu generieren.

3. Detaillierterer Blick in den JIT-Compiler

Die JDK-Implementierung von Oracle basiert auf dem Open-Source-OpenJDK-Projekt. Dies schließt dieHotSpot virtual machine ein, die seit Java Version 1.3 verfügbar sind. Escontains two conventional JIT-compilers: the client compiler, also called C1 and the server compiler, called opto or C2.

C1 wurde entwickelt, um schneller zu laufen und weniger optimierten Code zu produzieren, während C2 etwas mehr Zeit für die Ausführung benötigt, aber einen besser optimierten Code produziert. Der Client-Compiler eignet sich besser für Desktop-Anwendungen, da keine langen Pausen für die JIT-Kompilierung erforderlich sind. Der Server-Compiler eignet sich besser für Serveranwendungen mit langer Laufzeit, die mehr Zeit für die Kompilierung benötigen.

3.1. Abgestufte Zusammenstellung

Heutzutage werden bei der Java-Installation während der normalen Programmausführung beide JIT-Compiler verwendet.

Wie im vorherigen Abschnitt erwähnt, startet unser Java-Programm, das mitjavac kompiliert wurde, seine Ausführung in einem interpretierten Modus. Die JVM verfolgt jede häufig aufgerufene Methode und kompiliert sie. Zu diesem Zweck wird C1 für die Kompilierung verwendet. Der HotSpot behält jedoch die zukünftigen Aufrufe dieser Methoden im Auge. Wenn die Anzahl der Aufrufe zunimmt, kompiliert die JVM diese Methoden erneut, diesmal jedoch mit C2.

Dies ist die vom HotSpot verwendete Standardstrategie mit dem Namentiered compilation.

3.2. Der Server-Compiler

Konzentrieren wir uns jetzt ein wenig auf C2, da es das komplexeste von beiden ist. C2 wurde extrem optimiert und produziert Code, der mit C konkurrieren oder sogar noch schneller sein kann. Der Server-Compiler selbst ist in einem bestimmten Dialekt von C geschrieben.

Es kommt jedoch mit einigen Problemen. Aufgrund möglicher Segmentierungsfehler in C ++ kann die VM abstürzen. Außerdem wurden in den letzten Jahren keine wesentlichen Verbesserungen im Compiler implementiert. Der Code in C2 ist schwierig zu pflegen, sodass wir mit dem aktuellen Design keine neuen wesentlichen Verbesserungen erwarten können. In diesem Sinne wird der neue JIT-Compiler im Projekt GraalVM erstellt.

4. Projekt GraalVM

ProjectGraalVM ist ein Forschungsprojekt von Oracle mit dem Ziel, den HotSpot vollständig zu ersetzen. Wir können Graal als mehrere zusammenhängende Projekte betrachten: einen neuen JIT-Compiler für den HotSpot und eine neue polyglotte virtuelle Maschine. Es bietet ein umfassendes Ökosystem, das eine Vielzahl von Sprachen unterstützt (Java und andere JVM-basierte Sprachen; JavaScript, Ruby, Python, R, C / C ++ und andere LLVM-basierte Sprachen).

Wir werden uns natürlich auf Java konzentrieren.

4.1. Graal - ein in Java geschriebener JIT-Compiler

Graal is a high-performance JIT compiler. It akzeptiert den JVM-Bytecode und erzeugt den Maschinencode.

Das Schreiben eines Compilers in Java bietet mehrere wichtige Vorteile. Zuallererst Sicherheit, dh keine Abstürze, sondern Ausnahmen und keine echten Speicherlecks. Darüber hinaus verfügen wir über eine gute IDE-Unterstützung und können Debugger, Profiler oder andere praktische Tools verwenden. Außerdem kann der Compiler unabhängig vom HotSpot sein und eine schnellere JIT-kompilierte Version von sich selbst erstellen.

Der Graal-Compiler wurde unter Berücksichtigung dieser Vorteile entwickelt. It uses the new JVM Compiler Interface – JVMCI to communicate with the VM. Um die Verwendung des neuen JIT-Compilers zu ermöglichen, müssen die folgenden Optionen festgelegt werden, wenn Java über die Befehlszeile ausgeführt wird:

-XX:+UnlockExperimentalVMOptions -XX:+EnableJVMCI -XX:+UseJVMCICompiler

Dies bedeutet, dasswe can run a simple program in three different ways: with the regular tiered compilers, with the JVMCI version of Graal on Java 10 or with the GraalVM itself.

4.2. JVM-Compiler-Schnittstelle

Die JVMCI ist seit JDK 9 Teil des OpenJDK, sodass Graal mit jedem Standard-OpenJDK oder Oracle-JDK ausgeführt werden kann.

Was uns JVMCI tatsächlich erlaubt, ist, die standardmäßige gestufte Kompilierung auszuschließen und unseren brandneuen Compiler (d. H. Graal), ohne dass irgendetwas in der JVM geändert werden muss.

Die Oberfläche ist recht einfach. Wenn Graal eine Methode kompiliert, wird der Bytecode dieser Methode als Eingabe an die JVMCI übergeben. Als Ausgabe erhalten wir den kompilierten Maschinencode. Sowohl die Eingabe als auch die Ausgabe sind nur Byte-Arrays:

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

In realen Szenarien benötigen wir normalerweise weitere Informationen wie die Anzahl der lokalen Variablen, die Stapelgröße und die Informationen, die bei der Profilerstellung im Interpreter gesammelt wurden, damit wir wissen, wie der Code in der Praxis ausgeführt wird.

Wenn SiecompileMethod () derJVMCICompiler-Schnittstelle aufrufen, müssen Sie im Wesentlichen einCompilationRequest-Objekt übergeben. Anschließend wird die Java-Methode zurückgegeben, die kompiliert werden soll. In dieser Methode finden wir alle Informationen, die wir benötigen.

4.3. Graal in Aktion

Graal selbst wird von der VM ausgeführt, sodass es zuerst interpretiert und JIT-kompiliert wird, wenn es heiß wird. Schauen wir uns ein Beispiel an, das auch aufGraalVM’s official site zu finden ist:

public class CountUppercase {
    static final int ITERATIONS = Math.max(Integer.getInteger("iterations", 1), 1);

    public static void main(String[] args) {
        String sentence = String.join(" ", args);
        for (int iter = 0; iter < ITERATIONS; iter++) {
            if (ITERATIONS != 1) {
                System.out.println("-- iteration " + (iter + 1) + " --");
            }
            long total = 0, start = System.currentTimeMillis(), last = start;
            for (int i = 1; i < 10_000_000; i++) {
                total += sentence
                  .chars()
                  .filter(Character::isUpperCase)
                  .count();
                if (i % 1_000_000 == 0) {
                    long now = System.currentTimeMillis();
                    System.out.printf("%d (%d ms)%n", i / 1_000_000, now - last);
                    last = now;
                }
            }
            System.out.printf("total: %d (%d ms)%n", total, System.currentTimeMillis() - start);
        }
    }
}

Jetzt kompilieren wir es und führen es aus:

javac CountUppercase.java
java -XX:+UnlockExperimentalVMOptions -XX:+EnableJVMCI -XX:+UseJVMCICompiler

Dies führt zu einer Ausgabe ähnlich der folgenden:

1 (1581 ms)
2 (480 ms)
3 (364 ms)
4 (231 ms)
5 (196 ms)
6 (121 ms)
7 (116 ms)
8 (116 ms)
9 (116 ms)
total: 59999994 (3436 ms)

Wir können sehen, dassit takes more time in the beginning. Diese Aufwärmzeit hängt von verschiedenen Faktoren ab, z. B. der Menge des Multithread-Codes in der Anwendung oder der Anzahl der von der VM verwendeten Threads. Wenn weniger Kerne vorhanden sind, kann die Aufwärmzeit länger sein.

Wenn wir die Statistiken der Graal-Kompilierungen sehen wollen, müssen wir das folgende Flag hinzufügen, wenn wir unser Programm ausführen:

-Dgraal.PrintCompilation=true

Hier werden die Daten für die kompilierte Methode, die benötigte Zeit, die verarbeiteten Bytecodes (einschließlich Inline-Methoden), die Größe des erzeugten Maschinencodes und die während der Kompilierung zugewiesene Speicherkapazität angezeigt. Die Ausgabe der Ausführung nimmt ziemlich viel Platz ein, daher werden wir sie hier nicht zeigen.

4.4. Vergleich mit dem Top Tier Compiler

Vergleichen wir nun die obigen Ergebnisse mit der Ausführung desselben Programms, das stattdessen mit dem Top-Tier-Compiler kompiliert wurde. Dazu müssen wir die VM anweisen, den JVMCI-Compiler nicht zu verwenden:

java -XX:+UnlockExperimentalVMOptions -XX:+EnableJVMCI -XX:-UseJVMCICompiler
1 (510 ms)
2 (375 ms)
3 (365 ms)
4 (368 ms)
5 (348 ms)
6 (370 ms)
7 (353 ms)
8 (348 ms)
9 (369 ms)
total: 59999994 (4004 ms)

Wir können sehen, dass es einen kleineren Unterschied zwischen den einzelnen Zeiten gibt. Dies führt auch zu einer kürzeren Anfangszeit.

4.5. Die Datenstruktur hinter Graal

Wie bereits erwähnt, wandelt Graal ein Byte-Array in ein anderes Byte-Array um. In diesem Abschnitt konzentrieren wir uns auf die Hintergründe dieses Prozesses. Die folgenden Beispiele basieren aufChris Seaton’s talk at JokerConf 2017.

Die grundlegende Aufgabe des Compilers besteht im Allgemeinen darin, auf unser Programm zu reagieren. Dies bedeutet, dass es mit einer entsprechenden Datenstruktur symbolisiert werden muss. Graal uses a graph for such a purpose, the so-called program-dependence-graph.

In einem einfachen Szenario, in dem zwei lokale Variablen hinzugefügt werden sollen, d. H.x + y,we would have one node for loading each variable and another node for adding them. Danebenwe’d also have two edges representing the data flow:

image

The data flow edges are displayed in blue. Sie weisen darauf hin, dass beim Laden der lokalen Variablen das Ergebnis in die Additionsoperation eingeht.

Lassen Sie uns nunanother type of edges, the ones that describe the control flow einführen. Zu diesem Zweck erweitern wir unser Beispiel, indem wir Methoden aufrufen, um unsere Variablen abzurufen, anstatt sie direkt zu lesen. Wenn wir das tun, müssen wir die Methoden verfolgen, die die Reihenfolge aufrufen. Wir werden diese Reihenfolge mit den roten Pfeilen darstellen:

image

Hier können wir sehen, dass sich die Knoten nicht tatsächlich geändert haben, aber wir haben die Kontrollflusskanten hinzugefügt.

4.6. Aktuelle Grafiken

Wir können die realen Graal-Graphen mitIdealGraphVisualiser untersuchen. Um es auszuführen, verwenden wir den Befehlmx igv . Wir müssen auch die JVM konfigurieren, indem wir das Flag-Dgraal.Dump setzen.

Schauen wir uns ein einfaches Beispiel an:

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

Dies hat einen sehr einfachen Datenfluss:

image

In der obigen Grafik sehen wir eine übersichtliche Darstellung unserer Methode. Die Parameter P (0) und P (1) fließen in die Additionsoperation ein, die in die Divisionsoperation mit der Konstanten C (2) eintritt. Schließlich wird das Ergebnis zurückgegeben.

Wir werden nun das vorherige Beispiel so ändern, dass es auf ein Array von Zahlen anwendbar ist:

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

Wir können sehen, dass das Hinzufügen einer Schleife zu dem viel komplexeren Diagramm führte:

image

Was wirhere are: bemerken können

  • die Anfangs- und Endschleifenknoten

  • die Knoten, die den Array-Lesevorgang und den Array-Längen-Lesevorgang darstellen

  • Daten- und Kontrollflusskanten wie zuvor.

This data structure is sometimes called a sea-of-nodes, or a soup-of-nodes. Wir müssen erwähnen, dass der C2-Compiler eine ähnliche Datenstruktur verwendet, es ist also nichts Neues, das exklusiv für Graal innoviert wurde.

Es ist bemerkenswert, dass Graal unser Programm durch Änderung der oben genannten Datenstruktur optimiert und kompiliert. Wir können sehen, warum es eine gute Wahl war, den Graal JIT-Compiler in Java zu schreiben:a graph is nothing more than a set of objects with references connecting them as the edges. That structure is perfectly compatible with the object-oriented language, which in this case is Java.

4.7. Vorab-Compiler-Modus

Es ist auch wichtig zu erwähnen, dasswe can also use the Graal compiler in the Ahead-of-Time compiler mode in Java 10. Wie wir bereits sagten, wurde der Graal-Compiler von Grund auf neu geschrieben. Es entspricht einer neuen sauberen Schnittstelle, der JVMCI, die es uns ermöglicht, sie in den HotSpot zu integrieren. Dies bedeutet jedoch nicht, dass der Compiler daran gebunden ist.

Eine Möglichkeit, den Compiler zu verwenden, besteht darin, einen profilgesteuerten Ansatz zu verwenden, um nur die Hot-Methoden, aberwe can also make use of Graal to do a total compilation of all methods in an offline mode without executing the code zu kompilieren. Dies ist eine sogenannte "Ahead-of-Time-Kompilierung",JEP 295,, aber wir werden hier nicht weiter auf die AOT-Kompilierungstechnologie eingehen.

Der Hauptgrund, warum wir Graal auf diese Weise verwenden würden, besteht darin, die Startzeit zu verkürzen, bis der reguläre Tiered Compilation-Ansatz im HotSpot die Kontrolle übernehmen kann.

5. Fazit

In diesem Artikel haben wir die Funktionen des neuen Java JIT-Compilers als Teil des Projekts Graal untersucht.

Wir haben zuerst traditionelle JIT-Compiler beschrieben und dann die neuen Funktionen von Graal, insbesondere die neue JVM-Compiler-Oberfläche, besprochen. Anschließend haben wir gezeigt, wie beide Compiler arbeiten, und ihre Leistungen verglichen.

Danach haben wir über die Datenstruktur gesprochen, mit der Graal unser Programm manipuliert, und schließlich über den AOT-Compilermodus als eine andere Möglichkeit, Graal zu verwenden.

Wie immer kann der Quellcodeover on GitHub gefunden werden. Denken Sie daran, dass die JVM mit den hier beschriebenen spezifischen Flags konfiguriert werden muss.