[Français] Comment Graal - Java JVM JVM Compiler Works

Bonjour, Habr! Je vous présente la traduction de l'article " Comprendre le fonctionnement de Graal - un compilateur Java JIT écrit en Java ".


Présentation


L'une des raisons pour lesquelles je suis devenu chercheur en langages de programmation est que, dans une grande communauté de personnes impliquées dans la technologie informatique, presque tout le monde utilise des langages de programmation, et beaucoup sont intéressés par leur fonctionnement. Quand j'ai découvert la programmation quand j'étais enfant et que je me suis familiarisé avec un langage de programmation, la premiÚre chose que je voulais savoir était comment cela fonctionnait, et la toute premiÚre chose que je voulais faire était de créer mon propre langage.


Dans cette présentation, je vais vous montrer certains des mécanismes de travail du langage que vous utilisez tous - Java. La particularité est que j'utiliserai un projet appelé Graal , qui implémente le concept de Java en Java .


Graal n'est qu'un des composants du travail de Java - c'est un compilateur juste Ă  temps . Il s'agit de la partie de la machine virtuelle Java qui convertit le bytecode Java en code machine pendant l'exĂ©cution du programme, et est l'un des facteurs qui garantissent des performances Ă©levĂ©es de la plate-forme. C’est aussi, il me semble, ce que la plupart des gens considĂšrent comme l’une des parties les plus complexes et les plus vagues de la JVM, ce qui est au-delĂ  de leur comprĂ©hension. Changer cette opinion est le but de ce discours.


Si vous savez ce qu'est une JVM; comprendre généralement ce que signifient les termes bytecode et code machine ; et capable de lire du code écrit en Java, alors, j'espÚre, ce sera suffisant pour comprendre le matériel présenté.


Je commencerai par expliquer pourquoi nous pourrions vouloir un nouveau compilateur JIT pour la machine virtuelle Java Ă©crit en Java, et aprĂšs cela, je montrerai qu'il n'y a rien de spĂ©cial Ă  cela, comme vous pourriez le penser, en dĂ©composant la tĂąche en assembleur, utilisation et dĂ©monstration du compilateur. que son code est le mĂȘme que dans toute autre application.


J'aborderai un peu la thĂ©orie, puis je montrerai comment elle est appliquĂ©e pendant tout le processus de compilation, du bytecode au code machine. Je vais Ă©galement montrer quelques dĂ©tails, et Ă  la fin, nous parlerons des avantages de cette fonctionnalitĂ© en plus d'implĂ©menter Java en Java pour lui-mĂȘme.


J'utiliserai les captures d'écran du code dans Eclipse, au lieu de les lancer lors de la présentation, pour éviter les inévitables problÚmes de codage en direct.


Qu'est-ce qu'un compilateur JIT?


Je suis sĂ»r que beaucoup d’entre vous savent ce qu’est un compilateur JIT, mais je vais quand mĂȘme aborder les principes de base afin que personne ne reste assis ici sans avoir peur de poser cette question principale.


Lorsque vous exécutez la commande javac ou compile-on-save dans l'EDI, votre programme Java compile du code Java vers le bytecode JVM, qui est la représentation binaire du programme. Il est plus compact et simple que le code source Java. Cependant, le processeur standard de votre ordinateur portable ou serveur ne peut pas simplement exécuter le bytecode JVM.


Pour le fonctionnement de votre programme, la JVM interprÚte ce bytecode. Les interprÚtes sont généralement beaucoup plus lents que le code machine exécuté sur le processeur. Pour cette raison, la JVM, pendant l'exécution du programme, peut lancer un autre compilateur qui convertit votre bytecode en code machine, que le processeur est déjà capable d'exécuter.


Ce compilateur, généralement beaucoup plus sophistiqué que javac , effectue des optimisations complexes pour produire un code machine de haute qualité.


Pourquoi écrire un compilateur JIT en Java?


À ce jour, une implĂ©mentation JVM appelĂ©e OpenJDK comprend deux principaux compilateurs JIT. Le compilateur client, appelĂ© C1 , est conçu pour un fonctionnement plus rapide, mais produit en mĂȘme temps un code moins optimisĂ©. Le compilateur de serveur, appelĂ© opto ou C2 , nĂ©cessite un peu plus de temps pour fonctionner, mais produit un code plus optimisĂ©.


L'idĂ©e Ă©tait que le compilateur client Ă©tait mieux adaptĂ© aux applications de bureau, oĂč de longues pauses dans le compilateur JIT n'Ă©taient pas souhaitables, et le compilateur de serveur pour les applications de serveur Ă  longue durĂ©e de jeu, qui pouvaient passer plus de temps Ă  compiler.


Aujourd'hui, ils peuvent ĂȘtre combinĂ©s pour que le code soit d'abord compilĂ© par C1, puis, s'il continue d'ĂȘtre exĂ©cutĂ© intensivement et qu'il est logique de passer du temps supplĂ©mentaire, - C2. C'est ce qu'on appelle la compilation Ă  plusieurs niveaux .


ArrĂȘtons-nous sur C2, le compilateur de serveur qui effectue plus d'optimisations.


Nous pouvons cloner OpenJDK depuis le miroir sur GitHub , ou simplement ouvrir l'arborescence du projet sur le site.


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

Le code C2 se trouve dans openjdk / hotspot / src / share / vm / opto .


Source de code C2


Tout d'abord, il convient de noter que C2 est écrit en C ++ . Bien sûr, ce n'est pas quelque chose de mauvais, mais il y a certains inconvénients. C ++ est un langage dangereux. Cela signifie que les erreurs en C ++ peuvent planter la machine virtuelle. La raison en est probablement l'ùge du code, mais le code C2 C ++ est devenu trÚs difficile à maintenir et à développer.


L'une des figures clĂ©s du compilateur C2, Cliff Click, a dĂ©clarĂ© qu'il n'Ă©crirait plus jamais de machine virtuelle en C ++, et nous avons entendu l'Ă©quipe Twitter JVM dire que C2 stagne et doit ĂȘtre remplacĂ© pour une raison difficultĂ©s de dĂ©veloppement ultĂ©rieur.


Présentation falaise click


Ce que Cliff Click ne veut plus refaire


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


Donc, revenons Ă  la question, qu'est-ce que c'est en Java qui peut aider Ă  rĂ©soudre ces problĂšmes? La mĂȘme chose qui donne l'Ă©criture d'un programme en Java au lieu de C ++. Il s'agit probablement de sĂ©curitĂ© (exceptions au lieu de plantages, pas de vĂ©ritable fuite de mĂ©moire ou pointeurs pendants), de bons assistants (dĂ©bogueurs, profileurs et outils comme VisualVM ), un bon support IDE, etc.


Vous pourriez penser: Comment puis-je écrire quelque chose comme un compilateur Java JIT? , et que cela n'est possible que dans un langage de programmation systÚme de bas niveau tel que C ++. Dans cette présentation, j'espÚre vous convaincre que ce n'est pas du tout! Essentiellement, le compilateur JIT devrait simplement accepter le bytecode JVM et donner le code machine - vous lui donnez l' byte[] à l'entrée, et vous voulez aussi récupérer l' byte[] . Cela prend beaucoup de travail complexe pour ce faire, mais cela n'affecte pas le niveau du systÚme et ne nécessite donc pas un langage systÚme tel que C ou C ++.


Configuration de Graal


La premiÚre chose dont nous avons besoin est Java 9. L'interface Graal utilisée, appelée JVMCI, a été ajoutée à Java dans le cadre de l' interface du compilateur JVM de niveau Java JEP 243 et la premiÚre version qui l'inclut est Java 9. J'utilise 9 + 181 . En cas d'exigences particuliÚres, il existe des ports (backports) pour 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) 

La prochaine chose dont nous avons besoin est un systÚme de construction appelé mx . C'est un peu comme Maven ou Gradle , mais vous ne le choisiriez probablement pas pour votre application. Il implémente certaines fonctionnalités pour prendre en charge certains cas d'utilisation complexes, mais nous ne l'utiliserons que pour des assemblys simples.


Vous pouvez cloner mx avec GitHub. J'utilise le commit #7353064 . Maintenant, ajoutez simplement l'exécutable au chemin.


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

Maintenant, nous devons cloner Graal lui-mĂȘme. J'utilise une distribution appelĂ©e GraalVM version 0.28.2 .


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

Ce référentiel contient d'autres projets qui ne nous intéressent pas, nous allons donc simplement dans le sous-projet du compilateur , qui est le compilateur Graal JIT, et le compilons en utilisant mx .


 $ cd graal/compiler $ mx build 

Pour travailler avec le code Graal, j'utiliserai l' IDE Eclipse . J'utilise Eclipse 4.7.1. mx peut générer des fichiers de projet Eclipse pour nous.


 $ mx eclipseinit 

Pour ouvrir le rĂ©pertoire graal en tant qu'espace de travail, vous devez exĂ©cuter Fichier, Importer ..., GĂ©nĂ©ral, Projets existants et sĂ©lectionner Ă  nouveau le rĂ©pertoire graal . Si vous n'avez pas exĂ©cutĂ© Eclipse sur Java 9, vous devrez peut-ĂȘtre Ă©galement attacher les sources JDK.


Graal dans eclipse


Bon. Maintenant que tout est prĂȘt, voyons comment cela fonctionne. Nous utiliserons ce code trĂšs simple.


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

Tout d'abord, nous compilons ce code javac , puis exécutons la JVM. Tout d'abord, je vais vous montrer comment fonctionne le compilateur C2 JIT standard. Pour ce faire, nous indiquerons plusieurs indicateurs: -XX:+PrintCompilation , qui est nécessaire pour que la machine -XX:+PrintCompilation écrive un journal lors de la compilation de la méthode, et -XX:CompileOnly=Demo::workload , de sorte que seule cette méthode soit compilée. Si nous ne le faisons pas, alors trop d'informations seront affichées, et la JVM sera plus intelligente que nécessaire, et optimisera le code que nous voulons voir.


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

Je n'expliquerai pas cela en détail, mais je dirai seulement qu'il s'agit d'une sortie de journal qui montre que la méthode de workload été compilée.


Maintenant, en tant que compilateur JIT de notre JVM Java 9, nous utilisons le Graal fraßchement compilé. Pour ce faire, ajoutez quelques indicateurs supplémentaires.


--module-path=... et --upgrade-module-path=... ajoutez Graal au chemin du module . Permettez-moi de vous rappeler que le chemin du module est apparu dans Java 9 dans le cadre du systÚme de modules Jigsaw , et pour nos besoins, nous pouvons le considérer par analogie avec classpath .


Nous avons besoin de l' -XX:+UnlockExperimentalVMOptions car JVMCI (l'interface utilisée par Graal) dans cette version est une fonctionnalité expérimentale.


L'indicateur -XX:+EnableJVMCI nécessaire pour dire que nous voulons utiliser JVMCI, et -XX:+UseJVMCICompiler - pour activer et installer un nouveau compilateur JIT.


Afin de ne pas compliquer l'exemple et, au lieu d'utiliser C1 en conjonction avec le JVMCI, d'avoir uniquement le compilateur JVMCI, spécifiez l'indicateur -XX:-TieredCompilation , qui désactivera la compilation pas à pas.


Comme précédemment, nous -XX:+PrintCompilation les indicateurs -XX:+PrintCompilation et -XX:CompileOnly=Demo::workload .


Comme dans l'exemple précédent, nous voyons qu'une méthode a été compilée. Mais, cette fois, pour la compilation, nous avons utilisé Graal juste assemblé. Pour l'instant, prenez ma parole.


 $ 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) ... 

Interface du compilateur JVM


Ne pensez-vous pas que nous avons fait quelque chose d'assez inhabituel? Nous avons une JVM installĂ©e et nous avons remplacĂ© le compilateur JIT par le nouveau juste compilĂ© sans rien changer dans la JVM elle-mĂȘme. Cette fonctionnalitĂ© est fournie par une nouvelle interface JVM appelĂ©e JVMCI, l' interface du compilateur JVM , qui, comme je l'ai dit ci-dessus, Ă©tait JEP 243 et est venue en Java 9.


L'idée est similaire à certaines autres technologies JVM existantes.


Vous avez peut-ĂȘtre dĂ©jĂ  rencontrĂ© du traitement de code source supplĂ©mentaire dans javac Ă  l'aide de l' API de traitement d'annotations Java . Ce mĂ©canisme permet d'identifier les annotations et le modĂšle de code source dans lequel elles sont utilisĂ©es, et de crĂ©er de nouveaux fichiers Ă  partir de celles-ci.


De plus, vous avez peut-ĂȘtre utilisĂ© un traitement supplĂ©mentaire de bytecode dans la JVM Ă  l'aide d' agents Java . Ce mĂ©canisme vous permet de modifier le bytecode Java en l'interceptant au dĂ©marrage.


L'idée de JVMCI est similaire. Il vous permet de connecter votre propre compilateur Java JIT écrit en Java.


Maintenant, je veux dire quelques mots sur la façon dont je vais montrer le code lors de cette prĂ©sentation. Tout d'abord, pour comprendre l'idĂ©e, je vais montrer des identificateurs et une logique simplifiĂ©s sous forme de texte sur des diapositives, puis je passerai aux captures d'Ă©cran d'Eclipse et afficherai le vrai code, ce qui peut ĂȘtre un peu plus compliquĂ©, mais l'idĂ©e principale restera la mĂȘme. La partie principale de cette prĂ©sentation est destinĂ©e Ă  montrer qu'il est vraiment possible de travailler avec le vrai code du projet, et donc je ne veux pas le cacher, mĂȘme si cela peut ĂȘtre quelque peu compliquĂ©.


A partir de maintenant, je commence à dissiper l'opinion que vous pourriez avoir que le compilateur JIT est trÚs compliqué.


Qu'est-ce que le compilateur JIT accepte en entrée? Il accepte le bytecode de la méthode à compiler. Et le bytecode, comme son nom l'indique, n'est qu'un tableau d'octets.


Qu'est-ce que le compilateur JIT produit en conséquence? Il donne le code machine de la méthode. Le code machine n'est également qu'un tableau d'octets.


Par consĂ©quent, l'interface qui doit ĂȘtre implĂ©mentĂ©e lors de l'Ă©criture d'un nouveau compilateur JIT pour l'incorporer dans la JVM ressemblera Ă  ceci.


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

Par conséquent, si vous ne pouviez pas imaginer comment Java peut faire quelque chose d'aussi bas niveau que la compilation JIT dans le code machine, vous pouvez maintenant voir que ce n'est pas un travail de si bas niveau. Non? C'est juste une fonction de l' byte[] à l' byte[] .


En rĂ©alitĂ©, tout est un peu plus compliquĂ©. Le simple bytecode ne suffit pas - nous avons Ă©galement besoin de plus d'informations, telles que le nombre de variables locales, la taille de pile requise et les informations collectĂ©es par le profileur d'interprĂ©teur pour comprendre comment le code est exĂ©cutĂ© en fait. Par consĂ©quent, imaginez l'entrĂ©e sous la forme de CompilationRequest , qui nous renseignera sur la JavaMethod qui doit ĂȘtre compilĂ©e et fournira toutes les informations nĂ©cessaires.


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

De plus, l'interface ne nécessite pas le retour de code compilé. Au lieu de cela, une autre API est utilisée pour installer le code machine dans la JVM.


 HotSpot.installCode(targetCode); 

Maintenant, pour Ă©crire un nouveau compilateur JIT pour la JVM, il vous suffit d'implĂ©menter cette interface. Nous obtenons des informations sur la mĂ©thode qui doit ĂȘtre compilĂ©e, et nous devons la compiler en code machine et appeler installCode .


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

Passons Ă  l'IDE Eclipse avec Graal et jetons un Ɠil Ă  certaines interfaces et classes rĂ©elles. Comme mentionnĂ© prĂ©cĂ©demment, ils seront un peu plus compliquĂ©s, mais pas de beaucoup.


JVMCICompiler


HotSpotGraalCompiler


compileMethod


Maintenant, je veux montrer que nous pouvons apporter des modifications à Graal et les utiliser immédiatement en Java 9. J'ajouterai un nouveau message de journal qui sera affiché lors de la compilation de la méthode à l'aide de Graal. Ajoutez-le à la méthode d'interface implémentée, qui est appelée par le JVMCI.


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

Compilation


Pour l'instant, désactivez la journalisation de la compilation dans HotSpot. Nous pouvons maintenant voir notre message depuis la version modifiée du compilateur.


 $ 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 

Si vous essayez de rĂ©pĂ©ter cela vous-mĂȘme, vous remarquerez que vous n'avez mĂȘme pas besoin d'exĂ©cuter notre systĂšme de construction - mx build . Assez, normal pour Eclipse, compilez lors de la sauvegarde . Et nous n'avons certainement pas besoin de reconstruire la JVM elle-mĂȘme. Nous intĂ©grons simplement le compilateur modifiĂ© dans la JVM existante.


Comte Graal


Eh bien, nous savons que Graal convertit un byte[] en un autre byte[] . Parlons maintenant de la thĂ©orie et des structures de donnĂ©es qu'il utilise, car ils sont un peu inhabituels mĂȘme pour le compilateur.


Essentiellement, le compilateur gĂšre votre programme. Pour cela, le programme doit ĂȘtre prĂ©sentĂ© sous la forme d'une sorte de structure de donnĂ©es. Une option est le bytecode et les listes d'instructions similaires, mais elles ne sont pas trĂšs expressives.


Au lieu de cela, Graal utilise un graphique pour reprĂ©senter votre programme. Si nous prenons un opĂ©rateur d'addition simple qui rĂ©sume deux variables locales, le graphique comprendra un nƓud pour le chargement de chaque variable, un nƓud pour la somme et deux arĂȘtes qui montrent que le rĂ©sultat du chargement des variables locales est entrĂ© dans l'opĂ©rateur d'addition.


Ceci est parfois appelé un graphique de dépendance de programme .


Ayant une expression de la forme x + y on obtient des nƓuds pour les variables locales x et y , et un nƓud de leur somme.


Graphique de flux de données


Les bords bleus de ce graphique indiquent la direction du flux de données, de la lecture des variables locales à la sommation.


De plus, nous pouvons utiliser des bords pour reflĂ©ter l'ordre d'exĂ©cution du programme. Si, au lieu de lire des variables locales, nous appelons des mĂ©thodes, alors nous devons nous souvenir de l'ordre de l'appel, et nous ne pouvons pas les rĂ©organiser (sans connaĂźtre le code Ă  l'intĂ©rieur). Pour ce faire, des arĂȘtes supplĂ©mentaires dĂ©finissent cet ordre. Ils sont affichĂ©s en rouge.


Graphique de flux de contrĂŽle


Ainsi, le graal graal, en fait, est deux graphiques combinĂ©s en un seul. Les nƓuds sont les mĂȘmes, mais certains bords indiquent la direction du flux de donnĂ©es, tandis que d'autres montrent le transfert de contrĂŽle entre eux.


Pour voir le graphique Graal, vous pouvez utiliser un outil appelé IdealGraphVisualiser ou IGV . Le démarrage est effectué à l'aide de la mx igv .


IdealGraphVisualiser


AprÚs cela, exécutez la machine -Dgraal.Dump avec l'indicateur -Dgraal.Dump .


Un flux de donnĂ©es simple peut ĂȘtre vu en Ă©crivant une expression simple.


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

Moyenne arithmétique igv


Vous pouvez voir comment les paramÚtres 0 ( P(0 ) et 1 ( P(1) ) vont à l'entrée de l'opération d'addition, qui, avec la constante 2 ( C(2) ) va à l'entrée de l'opération de division, aprÚs quoi cette valeur est retournée.


Afin d'examiner un flux de données et de contrÎle plus complexe, nous introduisons un cycle.


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

Moyenne arithmétique igv avec cycle


Igv moyen arithmétique détaillé avec boucle


Dans ce cas, nous avons des nƓuds du dĂ©but et de la fin de la boucle, lisant les Ă©lĂ©ments du tableau et lisant la longueur du tableau. Comme prĂ©cĂ©demment, les lignes bleues indiquent la direction du flux de donnĂ©es et les lignes rouges indiquent le flux de contrĂŽle.


Vous pouvez maintenant voir pourquoi cette structure de donnĂ©es est parfois appelĂ©e la mer de nƓuds ou la soupe de nƓuds .


Je veux dire que C2 utilise une structure de donnĂ©es trĂšs similaire, et, en fait, c'est C2 qui a popularisĂ© l'idĂ©e d'un compilateur d'une mer de nƓuds , donc ce n'est pas une innovation de Graal.


Je ne montrerai pas le processus de construction de ce graphique jusqu'à la prochaine partie de la présentation, mais lorsque Graal reçoit le programme dans ce format, l'optimisation et la compilation sont effectuées en modifiant cette structure de données. Et c'est l'une des raisons pour lesquelles l'écriture d'un compilateur JIT en Java est logique. Java est un langage orienté objet, et un graphe est une collection d'objets reliés par des bords sous forme de liens.


Du bytecode au code machine


Voyons à quoi ces idées ressemblent dans la pratique et suivons quelques étapes du processus de compilation.


Obtenir le Bytecode


La compilation commence par le bytecode. Revenons Ă  notre petit exemple de sommation.


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

Nous afficherons le bytecode reçu à l'entrée juste avant le début de la compilation.


 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] 

Comme vous pouvez le voir, l'entrée du compilateur est du bytecode.


Analyseur de bytecode et générateur de graphiques


Le générateur , percevant ce tableau d'octets comme le bytecode JVM, le convertit en graal graal. Il s'agit d'une sorte d' interprétation abstraite - le générateur interprÚte le bytecode Java, mais, au lieu de transmettre des valeurs, manipule les extrémités libres des bords et les connecte progressivement les unes aux autres.


Profitons du fait que Graal est Ă©crit en Java et voyons comment cela fonctionne Ă  l'aide des outils de navigation Eclipse. Nous savons que dans notre exemple, il y a un nƓud d'addition, alors trouvons oĂč il est créé.


Addnode


Rechercher des appels AddNode.create


Analyseur


On peut voir qu'ils sont créés par l'analyseur de bytecode, et cela nous a conduit au IADD traitement IADD ( 96 , que nous avons vu dans le tableau d'entrée imprimé).


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

J'ai dit plus haut qu'il s'agit d'une interprĂ©tation abstraite, car tout cela est trĂšs similaire Ă  un interprĂ©teur de bytecode. S'il s'agissait d'un vĂ©ritable interprĂ©teur JVM, il faudrait alors retirer deux valeurs de la pile, effectuer un ajout et remettre le rĂ©sultat. Dans ce cas, nous supprimons deux nƓuds de la pile, qui, au dĂ©marrage du programme, seront des calculs, ajouterons, qui est le rĂ©sultat de la sommation, un nouveau nƓud Ă  ajouter et le placerons sur la pile.


Ainsi le graphe est construit Graal.


Obtention du code machine


Pour convertir un graphique Graal en code machine, vous devez gĂ©nĂ©rer des octets pour tous ses nƓuds. Cela se fait sĂ©parĂ©ment pour chaque nƓud en appelant sa mĂ©thode generate .


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

Je le répÚte, nous travaillons ici à un niveau d'abstraction trÚs élevé. Nous avons une classe avec laquelle nous émettons des instructions de code machine sans entrer dans les détails de la façon dont cela fonctionne.


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

Instruction de montage dans l'assembleur


Sortie d'octets


, , ByteBuffer — .



— .


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

Code machine d'impression


. 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 } } 

ProblĂšme de l'instruction AddNode


, , , .


 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 dans 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 , , .


Numérotation de valeur globale


Graphique de numérotation de la valeur globale


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


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

Un graphique qui ne peut pas ĂȘtre numĂ©rotĂ© en valeur globale


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

Simplification du verrouillage


, , , , 2 .


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

IGV . , , \ , , , 2 .


Simplifiez les verrous pour


Simplifiez les verrous aprĂšs



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 .


Projet Metropolis


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


Conclusions


, , , 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. Parce que 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/fr419637/


All Articles