[Traducción] Cómo funciona Graal - Compilador JVM JVM de Java

Hola Habr! Le presento la traducción del artículo " Comprender cómo funciona Graal: un compilador JIT de Java escrito en Java ".


Introduccion


Una de las razones por las cuales me convertí en investigador en lenguajes de programación es que, en una gran comunidad de personas involucradas en tecnología informática, casi todos usan lenguajes de programación, y muchos están interesados ​​en cómo funcionan. Cuando conocí la programación cuando era niño y me familiaricé con un lenguaje de programación, lo primero que quería saber era cómo funciona, y lo primero que quería hacer era crear mi propio lenguaje.


En esta presentación, le mostraré algunos de los mecanismos de trabajo del lenguaje que todos usan: Java. La peculiaridad es que usaré un proyecto llamado Graal , que implementa el concepto de Java en Java .


Graal es solo uno de los componentes en el trabajo de Java: es un compilador justo a tiempo . Esta es la parte de la JVM que convierte el bytecode de Java en código de máquina durante la ejecución del programa, y ​​es uno de los factores que aseguran un alto rendimiento de la plataforma. También es, me parece, lo que la mayoría de las personas considera una de las partes más complejas y vagas de la JVM, que está más allá de su comprensión. Cambiar esta opinión es el propósito de este discurso.


Si sabes lo que es una JVM; generalmente entiendo qué significan los términos bytecode y machine code y capaz de leer código escrito en Java, entonces, espero, esto será suficiente para entender el material presentado.


Comenzaré discutiendo por qué podríamos querer un nuevo compilador JIT para la JVM escrito en Java, y luego mostraré que no hay nada más especial en esto, como podría pensar, al dividir la tarea en ensamblador, uso y demostración de compilación. que su código es el mismo que en cualquier otra aplicación.


Tocaré un poco la teoría y luego mostraré cómo se aplica durante todo el proceso de compilación, desde el código de bytes hasta el código de máquina. También mostraré algunos detalles, y al final hablaremos sobre los beneficios de esta característica además de implementar Java en Java por sí mismo.


Usaré las capturas de pantalla del código en Eclipse, en lugar de lanzarlas durante la presentación, para evitar los problemas inevitables de la codificación en vivo.


¿Qué es un compilador JIT?


Estoy seguro de que muchos de ustedes saben lo que es un compilador JIT, pero aún voy a tocar los conceptos básicos para que nadie esté sentado aquí con miedo de hacer esta pregunta principal.


Cuando ejecuta el comando javac o compile-on-save en el IDE, su programa Java compila del código Java al código de bytes JVM, que es la representación binaria del programa. Es más compacto y simple que el código fuente de Java. Sin embargo, el procesador normal de su computadora portátil o servidor no puede simplemente ejecutar el código de bytes JVM.


Para el funcionamiento de su programa, la JVM interpreta este código de bytes. Los intérpretes suelen ser mucho más lentos que el código de máquina que se ejecuta en el procesador. Por esta razón, la JVM, mientras el programa se está ejecutando, puede lanzar otro compilador que convierte su código de bytes en código de máquina, que el procesador ya puede ejecutar.


Este compilador, generalmente mucho más sofisticado que javac , realiza optimizaciones complejas para producir código de máquina de alta calidad como resultado.


¿Por qué escribir un compilador JIT en Java?


Hasta la fecha, una implementación de JVM llamada OpenJDK incluye dos compiladores JIT principales. El compilador del cliente, conocido como C1 , está diseñado para una operación más rápida, pero al mismo tiempo produce un código menos optimizado. El compilador del servidor, conocido como opto o C2 , requiere un poco más de tiempo para funcionar, pero produce un código más optimizado.


La idea era que el compilador del cliente era más adecuado para aplicaciones de escritorio, donde las pausas largas en el compilador JIT no eran deseables, y el compilador del servidor para aplicaciones de servidor de larga duración, que podrían pasar más tiempo compilando.


Hoy se pueden combinar para que C1 compile primero el código y luego, si continúa ejecutándose intensamente y tiene sentido gastar tiempo adicional, - C2. Esto se llama compilación escalonada .


Hablemos de C2, el compilador del servidor que realiza más optimizaciones.


Podemos clonar OpenJDK desde el espejo en GitHub , o simplemente abrir el árbol del proyecto en el sitio.


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

El código C2 está en openjdk / hotspot / src / share / vm / opto .


Código fuente c2


En primer lugar, vale la pena señalar que C2 está escrito en C ++ . Por supuesto, esto no es algo malo, pero hay ciertas desventajas. C ++ es un lenguaje inseguro. Esto significa que los errores en C ++ pueden bloquear la VM. La razón de esto es probablemente la antigüedad del código, pero el código C2 C ++ se ha vuelto muy difícil de mantener y desarrollar.


Una de las figuras clave detrás del compilador de C2, Cliff Click dijo que nunca más volvería a escribir VM en C ++, y escuchamos al equipo de Twitter JVM decir que C2 se estancó y necesita ser reemplazado por alguna razón. dificultades de mayor desarrollo.


Presentación de cliff click


Lo que Cliff Click no quiere volver a hacer


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


Entonces, volviendo a la pregunta, ¿qué es esto en Java que puede ayudar a resolver estos problemas? Lo mismo que da escribir un programa en Java en lugar de C ++. Probablemente sea seguridad (excepciones en lugar de bloqueos, sin pérdida de memoria real o punteros colgantes), buenos ayudantes (depuradores, perfiladores y herramientas como VisualVM ), buen soporte IDE, etc.


Puede pensar: ¿cómo puedo escribir algo como un compilador JIT de Java? , y que esto solo es posible en un lenguaje de programación de sistema de bajo nivel como C ++. ¡En esta presentación, espero convencerlos de que esto no es en absoluto! Esencialmente, el compilador JIT solo debe aceptar el código de byte[] JVM y entregar el código de la máquina; usted le da el byte[] en la entrada y también desea recuperar el byte[] . Se necesita mucho trabajo complejo para hacer esto, pero no afecta el nivel del sistema y, por lo tanto, no requiere un lenguaje del sistema como C o C ++.


Configuración de Graal


Lo primero que necesitamos es Java 9. La interfaz de Graal llamada JVMCI utilizada se agregó a Java como parte de la interfaz del compilador JVM de nivel Java JEP 243, y la primera versión que la incluye es Java 9. Uso 9 + 181 . En caso de requisitos especiales, hay puertos (backports) para 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) 

Lo siguiente que necesitamos es un sistema de compilación llamado mx . Es un poco como Maven o Gradle , pero lo más probable es que no lo elija para su aplicación. Implementa cierta funcionalidad para admitir algunos casos de uso complejos, pero la usaremos solo para ensamblajes simples.


Puedes clonar mx con GitHub. Estoy usando commit #7353064 . Ahora solo agregue el ejecutable a la ruta.


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

Ahora necesitamos clonar a Graal. Estoy usando una distribución llamada GraalVM versión 0.28.2 .


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

Este repositorio contiene otros proyectos en los que no estamos interesados, por lo que simplemente vamos al subproyecto del compilador , que es el compilador Graal JIT, y lo mx usando mx .


 $ cd graal/compiler $ mx build 

Para trabajar con el código Graal, usaré el IDE de Eclipse . Estoy usando Eclipse 4.7.1. mx puede generar archivos de proyecto de Eclipse para nosotros.


 $ mx eclipseinit 

Para abrir el directorio graal como un espacio de trabajo, debe ejecutar Archivo, Importar ..., General, Proyectos existentes y seleccionar nuevamente el directorio graal . Si no ejecutó Eclipse en Java 9, es posible que también necesite adjuntar las fuentes JDK.


Graal en eclipse


Bueno Ahora que todo está listo, veamos cómo funciona. Usaremos este código muy 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; } } 

Primero, compilamos este código javac y luego ejecutamos la JVM. Primero, le mostraré cómo funciona el compilador JIT C2 estándar. Para hacer esto, especificaremos varios indicadores: -XX:+PrintCompilation , que es necesario para que la JVM escriba un registro al compilar un método, y -XX:CompileOnly=Demo::workload , para que solo este método se compile. Si no lo hacemos, se mostrará demasiada información y la JVM será más inteligente de lo que necesitamos y optimizará el código que queremos ver.


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

No explicaré esto en detalle, pero solo diré que este es un resultado de registro que muestra que el método de workload sido compilado.


Ahora, como compilador JIT de nuestra JVM Java 9, utilizamos el Graal recién compilado. Para hacer esto, agregue algunas banderas más.


--module-path=... y --upgrade-module-path=... agregan Graal a la ruta del módulo . Permítame recordarle que la ruta del módulo apareció en Java 9 como parte del sistema de módulos Jigsaw , y para nuestros propósitos podemos considerarlo por analogía con classpath .


Necesitamos el -XX:+UnlockExperimentalVMOptions debido a que JVMCI (la interfaz utilizada por Graal) en esta versión es una característica experimental.


La bandera -XX:+EnableJVMCI necesaria para decir que queremos usar JVMCI, y -XX:+UseJVMCICompiler - para habilitar e instalar un nuevo compilador JIT.


Para no complicar el ejemplo, y, en lugar de utilizar C1 junto con JVMCI, tener solo el compilador JVMCI, especifique el indicador -XX:-TieredCompilation , que deshabilitará la compilación por pasos.


Como antes, especificamos las banderas -XX:+PrintCompilation y -XX:CompileOnly=Demo::workload .


Como en el ejemplo anterior, vemos que se compiló un método. Pero, esta vez, para la compilación utilizamos el ensamblado de Graal. Por ahora, solo toma mi palabra.


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

Interfaz del compilador JVM


¿No crees que hicimos algo bastante inusual? Tenemos una JVM instalada, y reemplazamos el compilador JIT por uno nuevo compilado sin cambiar nada en la propia JVM. Esta característica es proporcionada por una nueva interfaz JVM llamada JVMCI, la interfaz del compilador JVM , que, como dije anteriormente, era JEP 243 y llegó en Java 9.


La idea es similar a algunas otras tecnologías JVM existentes.


Quizás haya encontrado procesamiento de código fuente adicional en javac utilizando la API de procesamiento de anotaciones de Java . Este mecanismo permite identificar anotaciones y el modelo de código fuente en el que se utilizan, y crear nuevos archivos basados ​​en ellas.


Además, es posible que haya utilizado un procesamiento de código de bytes adicional en la JVM utilizando agentes Java . Este mecanismo le permite modificar el código de bytes de Java interceptándolo en el momento del arranque.


La idea de JVMCI es similar. Le permite conectar su propio compilador JIT de Java escrito en Java.


Ahora quiero decir algunas palabras sobre cómo mostraré el código durante esta presentación. Primero, para comprender la idea, mostraré algunos identificadores y lógica simplificados en forma de texto en diapositivas, y luego cambiaré a capturas de pantalla de Eclipse y mostraré el código real, que puede ser un poco más complicado, pero la idea principal seguirá siendo la misma. La parte principal de esta presentación está destinada a mostrar que es realmente posible trabajar con el código real del proyecto y, por lo tanto, no quiero ocultarlo, aunque puede ser algo complicado.


A partir de ahora, empiezo a disipar la opinión de que podría tener que el compilador JIT es muy complicado.


¿Qué acepta el compilador JIT para la entrada? Acepta el bytecode del método a compilar. Y el bytecode, como su nombre lo indica, es solo una matriz de bytes.


¿Qué produce el compilador JIT como resultado? Da el código de máquina del método. El código de máquina también es solo una matriz de bytes.


Como resultado, la interfaz que debe implementarse al escribir un nuevo compilador JIT para incrustarlo en la JVM se verá más o menos así.


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

Por lo tanto, si no puede imaginar cómo Java puede hacer algo tan bajo como la compilación JIT en código máquina, ahora puede ver que este no es un trabajo de tan bajo nivel. Derecho? Esto es solo una función del byte[] al byte[] .


En realidad, todo es algo más complicado. Solo el bytecode no es suficiente; también necesitamos más información, como el número de variables locales, el tamaño de pila requerido y la información recopilada por el perfilador del intérprete para comprender cómo se ejecuta el código. Por lo tanto, imagine la entrada en forma de JavaMethod de CompilationRequest , que nos informará sobre el JavaMethod que debe compilarse y proporcionará toda la información necesaria.


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

Además, la interfaz no requiere la devolución del código compilado. En cambio, se usa otra API para instalar el código de máquina en la JVM.


 HotSpot.installCode(targetCode); 

Ahora, para escribir un nuevo compilador JIT para JVM, solo necesita implementar esta interfaz. Obtenemos información sobre el método que debe compilarse, y debemos compilarlo en código máquina y llamar a installCode .


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

Cambiemos al IDE de Eclipse con Graal y echemos un vistazo a algunas interfaces y clases reales. Como se mencionó anteriormente, serán algo más complicados, pero no por mucho.


JVMCICompiler


HotSpotGraalCompiler


método de compilación


Ahora quiero mostrar que podemos hacer cambios en Graal e inmediatamente usarlos en Java 9. Agregaré un nuevo mensaje de registro que se mostrará al compilar el método usando Graal. Agréguelo al método de interfaz implementado, que es llamado por JVMCI.


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

Compilación


Por ahora, deshabilite el registro de compilación en HotSpot. Ahora podemos ver nuestro mensaje desde la versión modificada del compilador.


 $ 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 intenta repetir esto usted mismo, notará que ni siquiera necesita ejecutar nuestro sistema de compilación: mx build . Suficiente, normal para Eclipse, compilar al guardar . Y ciertamente no necesitamos reconstruir la JVM en sí. Simplemente incorporamos el compilador modificado en la JVM existente.


Conde Graal


Bueno, sabemos que Graal convierte un byte[] a otro byte[] . Ahora hablemos sobre la teoría y las estructuras de datos que usa, porque son un poco inusuales incluso para el compilador.


Esencialmente, el compilador maneja su programa. Para esto, el programa debe presentarse en forma de algún tipo de estructura de datos. Una opción es bytecode y listas de instrucciones similares, pero no son muy expresivas.


En cambio, Graal usa un gráfico para representar su programa. Si tomamos un operador de suma simple que suma dos variables locales, entonces el gráfico incluirá un nodo para cargar cada variable, un nodo para la suma y dos bordes que muestran que el resultado de cargar variables locales va a la entrada del operador de suma.


Esto a veces se llama un gráfico de dependencia del programa .


Teniendo una expresión de la forma x + y obtenemos nodos para las variables locales x e y , y un nodo de su suma.


Gráfico de flujo de datos


Los bordes azules en este gráfico muestran la dirección del flujo de datos desde la lectura de las variables locales hasta la suma.


Además, podemos usar aristas para reflejar el orden de ejecución del programa. Si, en lugar de leer variables locales, llamamos a métodos, entonces necesitamos recordar el orden de la llamada, y no podemos reorganizarlos (sin conocer el código interno). Para hacer esto, hay bordes adicionales que definen este orden. Se muestran en rojo.


Gráfico de flujo de control


Entonces, el gráfico de Graal, de hecho, es dos gráficos combinados en uno. Los nodos son iguales, pero algunos bordes indican la dirección del flujo de datos, mientras que otros muestran la transferencia de control entre ellos.


Para ver el gráfico de Graal, puede usar una herramienta llamada IdealGraphVisualiser o IGV . El inicio se realiza utilizando el mx igv .


IdealGraphVisualiser


Después de eso, ejecute la JVM con el indicador -Dgraal.Dump .


Se puede ver un flujo de datos simple escribiendo una expresión simple.


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

Media aritmética igv


Puede ver cómo los parámetros 0 ( P(0 ) y 1 ( P(1) ) van a la entrada de la operación de suma, que, junto con la constante 2 ( C(2) ) va a la entrada de la operación de división, después de lo cual se devuelve este valor.


Para observar un flujo de datos y control más complejo, presentamos un ciclo.


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

Media aritmética igv con ciclo


Medio aritmético igv detallado con bucle


En este caso, tenemos nodos del principio y el final del ciclo, que leen los elementos de la matriz y leen la longitud de la matriz. Como antes, las líneas azules indican la dirección del flujo de datos, y las líneas rojas indican el flujo de control.


Ahora puede ver por qué esta estructura de datos a veces se llama mar de nodos o sopa de nodos .


Quiero decir que C2 utiliza una estructura de datos muy similar y, de hecho, fue C2 que popularizó la idea de un compilador de un mar de nodos , por lo que esto no es una innovación de Graal.


No mostraré el proceso de construcción de este gráfico hasta la siguiente parte de la presentación, pero cuando Graal recibe el programa en este formato, la optimización y la compilación se realizan modificando esta estructura de datos. Y esta es una de las razones por las cuales escribir un compilador JIT en Java tiene sentido. Java es un lenguaje orientado a objetos, y un gráfico es una colección de objetos conectados por bordes en forma de enlaces.


Del código de bytes al código de máquina


Veamos cómo se ven estas ideas en la práctica y sigamos algunos pasos del proceso de compilación.


Obteniendo Bytecode


La compilación comienza con bytecode. Volvamos a nuestro pequeño ejemplo de resumen.


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

Emitiremos el código de bytes recibido en la entrada justo antes del inicio de la compilación.


 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] 

Como puede ver, la entrada al compilador es bytecode.


Analizador de código de bytes y generador de gráficos


El constructor , al percibir esta matriz de bytes como el código de bytes JVM, la convierte en un gráfico de Graal. Este es un tipo de interpretación abstracta : el constructor interpreta el código de bytes de Java, pero, en lugar de pasar valores, manipula los extremos libres de los bordes y los conecta gradualmente entre sí.


Aprovechemos el hecho de que Graal está escrito en Java y veamos cómo funciona usando las herramientas de navegación de Eclipse. Sabemos que en nuestro ejemplo hay un nodo de suma, así que busquemos dónde se crea.


Addnode


Llamadas de búsqueda AddNode.create


Analizador


Se puede ver que son creados por el analizador de bytecode, y esto nos llevó al IADD procesamiento de IADD ( 96 , que vimos en la matriz de entrada impresa).


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

Dije anteriormente que esta es una interpretación abstracta, porque Todo esto es muy similar a un intérprete de bytecode. Si se tratara de un verdadero intérprete de JVM, tomaría dos valores de la pila, realizaría la suma y devolvería el resultado. En este caso, eliminamos dos nodos de la pila, que, cuando se inicia el programa, serán cálculos, sumar, que es el resultado de la suma, un nuevo nodo para sumar, y colocarlo en la pila.


Así se construye el gráfico Graal.


Obtener el código de la máquina


Para convertir un gráfico de Graal a código de máquina, debe generar bytes para todos sus nodos. Esto se hace por separado para cada nodo llamando a su método de generate .


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

Repito, aquí trabajamos a un nivel muy alto de abstracción. Tenemos una clase con la que emitimos instrucciones de código de máquina sin entrar en detalles de cómo funciona esto.


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

Instrucciones de montaje en ensamblador


Salida de bytes


, , ByteBuffer — .



— .


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

Código de máquina de impresión


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

Problema de la instrucción 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 en 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 , , .


Numeración de valor global


Gráfico de numeración de valor global


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


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

Un gráfico que no puede tener un valor global numerado


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

Simplificación de bloqueo


, , , , 2 .


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

IGV . , , \ , , , 2 .


Simplifique las cerraduras a


Simplifica los bloqueos despué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 .


Proyecto Metrópolis


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


Conclusiones


, , , 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. Porque 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/es419637/


All Articles