Plongée dans le nouveau compilateur Java JIT - Graal

Plongez au cœur du nouveau compilateur Java JIT - Graal

1. Vue d'ensemble

Dans ce didacticiel, nous allons examiner plus en détail le nouveau compilateur Java Just-In-Time (JIT), appelé Graal.

Nous allons voir ce qu'est le projetGraal et décrire l'une de ses parties, un compilateur JIT dynamique hautes performances.

2. Qu'est-ce qu'un compilateur JIT?

Expliquons d'abord ce que fait le compilateur JIT.

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. Ce bytecode est plus simple et plus compact que notre code source, mais les processeurs conventionnels de nos ordinateurs ne peuvent pas l'exécuter.

To be able to run a Java program, the JVM interprets the bytecode. Puisque les interpréteurs sont généralement beaucoup plus lents que le code natif s'exécutant sur un vrai processeur, lesJVM can run another compiler which will now compile our bytecode into the machine code that can be run by the processor. Ce compilateur dit juste à temps est beaucoup plus sophistiqué que le compilateurjavac, et il exécute des optimisations complexes pour générer du code machine de haute qualité.

3. Regard plus détaillé sur le compilateur JIT

L'implémentation du JDK par Oracle est basée sur le projet OpenJDK open-source. Cela inclut lesHotSpot virtual machine, disponibles depuis la version 1.3 de Java. Ilcontains two conventional JIT-compilers: the client compiler, also called C1 and the server compiler, called opto or C2.

C1 est conçu pour fonctionner plus rapidement et produire un code moins optimisé, alors que C2 prend un peu plus de temps à exécuter mais produit un code mieux optimisé. Le compilateur client convient mieux aux applications de bureau car nous ne voulons pas de longues pauses pour la compilation JIT. Le compilateur de serveur convient mieux aux applications serveur de longue durée qui peuvent passer plus de temps à la compilation.

3.1. Compilation à plusieurs niveaux

Aujourd'hui, l'installation de Java utilise les deux compilateurs JIT lors de l'exécution normale du programme.

Comme nous l'avons mentionné dans la section précédente, notre programme Java, compilé parjavac, démarre son exécution en mode interprété. La JVM suit chacune des méthodes fréquemment appelées et les compile. Pour ce faire, il utilise C1 pour la compilation. Mais le HotSpot garde toujours un œil sur les futurs appels de ces méthodes. Si le nombre d'appels augmente, la JVM recompilera ces méthodes une nouvelle fois, mais cette fois à l'aide de C2.

Il s'agit de la stratégie par défaut utilisée par le HotSpot, appeléetiered compilation.

3.2. Le compilateur de serveur

Concentrons-nous maintenant un peu sur C2, car c'est le plus complexe des deux. C2 a été extrêmement optimisé et produit du code qui peut rivaliser avec C ou être encore plus rapide. Le compilateur de serveur lui-même est écrit dans un dialecte spécifique de C.

Cependant, cela pose certains problèmes. En raison de possibles erreurs de segmentation en C ++, la machine virtuelle peut tomber en panne. De plus, aucune amélioration majeure n’a été apportée au compilateur au cours des dernières années. Le code de C2 est devenu difficile à maintenir, nous ne pouvions donc pas nous attendre à de nouvelles améliorations majeures avec la conception actuelle. Dans cet esprit, le nouveau compilateur JIT est créé dans le projet nommé GraalVM.

4. Projet GraalVM

Le projetGraalVM est un projet de recherche créé par Oracle dans le but de remplacer entièrement le HotSpot. Nous pouvons considérer Graal comme plusieurs projets connectés: un nouveau compilateur JIT pour HotSpot et une nouvelle machine virtuelle polyglot. Il offre un écosystème complet prenant en charge un grand nombre de langages (Java et autres langages basés sur JVM; JavaScript, Ruby, Python, R, C / C ++ et d’autres langages basés sur LLVM).

Nous allons bien sûr nous concentrer sur Java.

4.1. Graal - un compilateur JIT écrit en Java

Graal is a high-performance JIT compiler. Il accepte le bytecode JVM et produit le code machine.

L'écriture d'un compilateur en Java présente plusieurs avantages clés. Tout d’abord, la sécurité, c’est-à-dire qu’il n’ya pas de crash, mais des exceptions et pas de fuite mémoire réelle. De plus, nous bénéficierons d'un bon support IDE et nous serons en mesure d'utiliser des débogueurs ou des profileurs ou d'autres outils pratiques. En outre, le compilateur peut être indépendant du HotSpot et il serait capable de produire une version plus rapidement compilée par JIT.

Le compilateur Graal a été créé avec ces avantages à l’esprit. It uses the new JVM Compiler Interface – JVMCI to communicate with the VM. Pour activer l'utilisation du nouveau compilateur JIT, nous devons définir les options suivantes lors de l'exécution de Java à partir de la ligne de commande:

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

Cela signifie quewe 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. Interface du compilateur JVM

Le JVMCI faisant partie de l’OpenJDK depuis le JDK 9, nous pouvons donc utiliser n’importe quel standard OpenJDK ou Oracle JDK pour exécuter Graal.

Ce que JVMCI nous permet en fait de faire est d'exclure la compilation à plusieurs niveaux standard et de brancher notre tout nouveau compilateur (c.-à-d. Graal) sans avoir besoin de changer quoi que ce soit dans la JVM.

L'interface est assez simple. Lorsque Graal compile une méthode, il transmet le bytecode de cette méthode comme entrée au JVMCI. En sortie, nous obtiendrons le code machine compilé. L'entrée et la sortie ne sont que des tableaux d'octets:

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

Dans les scénarios réels, nous avons généralement besoin de plus d'informations telles que le nombre de variables locales, la taille de la pile et les informations collectées à partir du profilage dans l'interpréteur afin de savoir comment le code s'exécute dans la pratique.

Essentiellement, lors de l'appel ducompileMethod () de l'interfaceJVMCICompiler, nous devons passer un objetCompilationRequest. Il retournera ensuite la méthode Java que nous voulons compiler, et dans cette méthode, nous trouverons toutes les informations dont nous avons besoin.

4.3. Graal en action

Graal lui-même est exécuté par la VM, il sera donc d'abord interprété et compilé JIT lorsqu'il devient chaud. Voyons un exemple, qui peut également être trouvé sur lesGraalVM’s official site:

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);
        }
    }
}

Maintenant, nous allons le compiler et l'exécuter:

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

Cela produira un résultat similaire à celui-ci:

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)

Nous pouvons voir queit takes more time in the beginning. Ce temps de préchauffage dépend de divers facteurs, tels que la quantité de code multithread de l'application ou le nombre de threads utilisés par la machine virtuelle. S'il y a moins de cœurs, le temps de préchauffage pourrait être plus long.

Si nous voulons voir les statistiques des compilations Graal, nous devons ajouter l'indicateur suivant lors de l'exécution de notre programme:

-Dgraal.PrintCompilation=true

Cela montrera les données liées à la méthode compilée, le temps pris, les codes bytec traités (qui inclut également les méthodes en ligne), la taille du code machine produit et la quantité de mémoire allouée lors de la compilation. La sortie de l'exécution prend beaucoup d'espace, nous ne la montrerons donc pas ici.

4.4. Comparaison avec le compilateur de premier niveau

Comparons maintenant les résultats ci-dessus avec l’exécution du même programme compilé avec le compilateur de niveau supérieur. Pour ce faire, nous devons dire à la machine virtuelle de ne pas utiliser le compilateur JVMCI:

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)

Nous pouvons voir qu'il y a une différence plus petite entre les moments individuels. Il en résulte également un temps initial plus court.

4.5. La structure de données derrière Graal

Comme nous l'avons dit précédemment, Graal transforme un tableau d'octets en un autre tableau d'octets. Dans cette section, nous allons nous concentrer sur ce qui se cache derrière ce processus. Les exemples suivants reposent surChris Seaton’s talk at JokerConf 2017.

Le travail de base du compilateur, en général, est d’agir sur notre programme. Cela signifie qu'il doit le symboliser avec une structure de données appropriée. Graal uses a graph for such a purpose, the so-called program-dependence-graph.

Dans un scénario simple, où nous voulons ajouter deux variables locales, à savoir,x + y,we would have one node for loading each variable and another node for adding them. À côté,we’d also have two edges representing the data flow:

image

The data flow edges are displayed in blue. Ils soulignent que lorsque les variables locales sont chargées, le résultat entre dans l’opération d’addition.

Introduisons maintenantanother type of edges, the ones that describe the control flow. Pour ce faire, nous allons étendre notre exemple en appelant des méthodes pour récupérer nos variables au lieu de les lire directement. Lorsque nous faisons cela, nous devons suivre les méthodes d’appel d’ordre. Nous allons représenter cet ordre avec les flèches rouges:

image

Ici, nous pouvons voir que les nœuds n'ont pas changé en fait, mais nous avons ajouté les bords du flux de contrôle.

4.6. Graphes réels

Nous pouvons examiner les graphes Graal réels avec lesIdealGraphVisualiser. Pour l'exécuter, nous utilisons la commandemx igv . Nous devons également configurer la JVM en définissant l'indicateur-Dgraal.Dump.

Voyons un exemple simple:

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

Cela a un flux de données très simple:

image

Dans le graphique ci-dessus, nous pouvons voir une représentation claire de notre méthode. Les paramètres P (0) et P (1) entrent dans l'opération d'addition qui entre l'opération de division avec la constante C (2). Enfin, le résultat est renvoyé.

Nous allons maintenant modifier l'exemple précédent pour qu'il s'applique à un tableau de nombres:

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

Nous pouvons voir que l'ajout d'une boucle nous a conduit au graphique beaucoup plus complexe:

image

Ce que nous pouvons remarquerhere are:

  • les nœuds de début et de fin

  • les noeuds représentant la lecture du tableau et la lecture de la longueur du tableau

  • bords de données et de flux de contrôle, comme avant.

This data structure is sometimes called a sea-of-nodes, or a soup-of-nodes. Nous devons mentionner que le compilateur C2 utilise une structure de données similaire, donc ce n’est pas quelque chose de nouveau, innové exclusivement pour Graal.

Il convient de noter que Graal optimise et compile notre programme en modifiant la structure de données susmentionnée. Nous pouvons voir pourquoi c'était un bon choix d'écrire le compilateur Graal JIT en Java: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. Mode compilateur d'avance

Il est également important de mentionner quewe can also use the Graal compiler in the Ahead-of-Time compiler mode in Java 10. Comme nous l'avons déjà dit, le compilateur Graal a été écrit à partir de zéro. Il est conforme à une nouvelle interface épurée, le JVMCI, ce qui nous permet de l’intégrer au HotSpot. Cela ne veut pas dire que le compilateur y est lié.

Une façon d'utiliser le compilateur consiste à utiliser une approche basée sur le profil pour ne compiler que les méthodes chaudes, maiswe can also make use of Graal to do a total compilation of all methods in an offline mode without executing the code. Il s’agit d’une soi-disant «compilation à l’avance»,JEP 295,, mais nous n’allons pas approfondir ici la technologie de compilation AOT.

La principale raison pour laquelle nous utiliserions Graal de cette manière est d’accélérer le temps de démarrage jusqu’à ce que la méthode classique de compilation par niveaux du HotSpot puisse prendre le relais.

5. Conclusion

Dans cet article, nous avons exploré les fonctionnalités du nouveau compilateur Java JIT dans le cadre du projet Graal.

Nous avons d'abord décrit les compilateurs JIT traditionnels, puis abordé les nouvelles fonctionnalités du Graal, en particulier la nouvelle interface du compilateur JVM. Ensuite, nous avons illustré le fonctionnement des deux compilateurs et comparé leurs performances.

Après cela, nous avons parlé de la structure de données que Graal utilise pour manipuler notre programme et, enfin, du mode compilateur AOT comme une autre façon d'utiliser Graal.

Comme toujours, le code source peut être trouvéover on GitHub. N'oubliez pas que la machine virtuelle Java doit être configurée avec les indicateurs spécifiques, décrits ici.