GraalVM: One VM to Rule Them All (I)

 

 

A finales del año pasado tuve la oportunidad de asistir al Commit en Madrid, donde participé en  algunas charlas relacionadas con el mundo de Java en las que se mencionó, brevemente, el nombre de GraalVM seguido de una frase del tipo "está muy bien, tenéis que echarle un vistazo". Si no recuerdo mal, en una de ellas llegaron a decir "puede generar ejecutables a partir de código Java".

En esta serie vamos a ver de qué se trata y alguna de las características que más me han llamado la atención.

 

¿Que es GraalVM? One VM to Rule Them All

 

GraalVM es una nueva máquina virtual poliglota de alto rendimiento de Oracle que permite interactuar con lenguajes basados en la JVM (Java, Scala, Kotlin, Clojure…), y también con lenguajes de programación invitados escritos en JavaScript, Python, Ruby, R, C o C++ a través del framework Truffle. Por ello, el propio equipo de GraalVM lo define como One VM to Rule Them All (Una máquina virtual para gobernarlos a todos). Esta frase la podéis ver en la web en el título de uno de los videos, así como en alguna documentación que podéis encontrar al buscar información sobre el tema.

 

Ya solo con eso promete bastante, pero además tiene otras características como por ejemplo:

  • Permite combinar esos lenguajes entre si
  • Crear ejecutables nativos para Linux y MacOS (Windows aún no está soportado)
  • Usado en el plugin Multilingual Engine (MLE), permite ejecutar funciones de JavaScript (y Python próximamente) como procedimientos almacenados en bases de datos Oracle y MySQL
  • Implementar tu propio lenguaje
  • Incluye herramientas para realizar debug o monitorización

 

Y estas son solo algunas.

Durante esta serie, revisaremos las más llamativas como, por ejemplo, el de combinar los lenguajes o crear esos ejecutables.

Pero antes veamos cómo está montada la arquitectura de GraalVM y qué nos aporta cada una de sus capas.

 

Arquitectura

 

Como digo, GraalVM es un producto compuesto por varias capas.

 

graalvm

 

Tenemos el compilador de Graal, que se trata de un compilador Just-In-Time (JIT) escrito en Java. Al estar escrito en Java, permite que el equipo de GraalVM tenga mayor control, por ejemplo para la gestión de errores, y les permita usar herramientas actuales como IDEs e independencia del HotSpot de Java. Este compilador lo usaremos para los lenguajes basados en la JVM (Java, Scala, Kotlin, Clojure…), que se comunica con el HotSpot de Java a través del interfaz del compilador de la JVM (JVMCI).

Por encima tenemos el framework Truffle, que permite crear tu propio lenguaje a través de un árbol de sintaxis abstracto (AST). En la distribución de GraalVM ya podemos usar las implementaciones de JavaScript, Ruby, Python o R que se han realizado para este framework. Y como digo, podríamos crear nuestro propio lenguaje.

A su vez tenemos por encima Sulong, un intérprete de bitcode de la máquina virtual de bajo nivel (LLVM) que se usa para ejecutar C/C++ o Fortran.

Finalmente tenemos SubstrateVM por debajo del compilador de Graal. Se trata de un framework que permite compilación Ahead-Of-Time (AOT) con el que se crean los ejecutables nativos.

 

JIT vs AOT

 

Acabamos de ver estos conceptos en la descripción de la arquitectura y convendría un pequeño repaso para tenerlos claros antes de empezar a usar GraalVM.

 

El bytecode que se genera tras la compilación no puede ser ejecutado por el sistema operativo y solo lo podría hacer a través de un intérprete. Ejecutarlo con un intérprete es más lento que ejecutar código nativo. Por ello la JVM realiza una compilación durante la propia ejecución (Just-In-Time) para pasar del bytecode a código nativo y que pueda ser ejecutado por el sistema.

La JVM tiene dos tipos de compiladores JIT, Cliente y Servidor (llamados C1 y C2 respectivamente). El compilador C1 se caracteriza por poder realizar la compilación rápidamente, para que la aplicación tenga un inicio rápido, pero genera un código poco optimizado. En cambio, el C2 es más lento ejecutándose pero genera un código mucho más optimizado. Deberíamos usar C1 para programas de corta duración y C2 para los de larga. Pero si no especificamos nada la JVM lanza inicialmente en un modo interpretado, monitorizando las llamadas a métodos y compilando con C1 aquellas que se realizan más frecuentemente. Y no queda ahí la cosa, ya que si el número de llamadas sobre esos métodos se incrementa recompilara de nuevo estos métodos, pero en este caso con la compilación C2.
De este modo el compilador va monitorizando la ejecución del programa y realiza un análisis de manera dinámica, aprendiendo del comportamiento de la aplicación durante su ejecución. Así puede realizar optimizaciones dinámicamente.

Algunas de estas optimizaciones son:

  • Elimina el código que nunca se ejecuta o que no produce un efecto sobre el resultado.
  • Cambia las variables por constantes allí donde sea posible.
  • Extrae las condiciones que se encuentran dentro de bucles.
  • Reemplaza llamadas a ciertos métodos por el contenido de su cuerpo.
  • Si en algún momento el análisis muestra que una optimización que ha aplicado no es positiva, la deshace.

Aún con todas estas optimizaciones, la carga inicial de la aplicación es más lenta al tener que aplicar a una compilación en tiempo de ejecución para obtener el código nativo.

Por ello, de manera alternativa, podemos realizar una compilación anticipada (Ahead-Of-Time) para pasar el bytecode a código nativo una única vez. La carga inicial es más rápida, ya que hemos eliminado la compilación en tiempo de ejecución, pero al mismo tiempo el compilador no puede realizar todas las optimizaciones de las que sería capaz el JIT porque, al contrario que el JIT, el compilador AOT no tiene acceso al comportamiento dinámico de la aplicación ejecución.

Tras aclarar estos conceptos, ya podemos pensar que deberemos usar la compilación JIT cuando el proceso vaya a ser de larga ejecución, por ejemplo procesos batch o para aplicaciones ejecutándose en el servidor como puede ser un API REST. Y deberemos usar la compilación AOT para procesos cortos en los que nos interese un inicio rápido y en los que no dé tiempo a aplicar optimizaciones, por ejemplo procesos de línea de comandos o, aplicado a servicios en la nube, Funciones como servicio (FaaS).

Veremos en uno de los siguientes post cómo realizando pruebas vemos que el mismo programa se ejecuta más rápido con una u otra compilación, según la duración.

 

Instalación

 

GraalVM está disponible para Linux y MacOS, y recientemente se ha añadido una versión incompleta para Windows.

Hay dos ediciones, Community (open source y gratis para uso en producción) y Enterprise.

Para instalarlo solo hay que descargarlo, descomprimirlo y añadirlo al path. Os dejo aquí el link donde se explica más detalladamente.

Yo, pese a usar un Mac, he optado por usar la imagen oficial de GraalVM para Docker, por eso de no cambiar nada en mi equipo y poder empezar a probarlo más rápidamente.

$ docker pull oracle/graalvm-ce
$ docker run -it -v $(pwd):/demo -w /demo oracle/graalvm-ce bash

Primero descargo la imagen de GraalVM con docker pull, en mi caso contiene la versión 19.0.0 de GraalVM que salió en Mayo, y, a continuación, lanzo el contenedor a partir de esa imagen con docker run.

Los argumentos que le paso al comando run son los siguientes:

  • -it para poder interactuar con el contenedor a través de la terminal.
  • -v para mapear el directorio en el que estoy actualmente con una ruta dentro del contenedor y, de ese modo, poder guardar en mi sistema de ficheros todo lo que vaya generando durante las pruebas. $(pwd) establecerá mi ruta actual como argumento, pero con la Command Prompt de Windows tendréis que escribir el path directamente.
  • -w establece el path donde nos encontraremos una vez se lance el contenedor, que coincide con la ruta que le indicamos en el paso anterior.

 

Si accedemos a la carpeta bin de la instalación podremos ver todos los ejecutables disponibles:

bash-4.2# ls /opt/graalvm-ce-19.0.0/bin/
appletviewer    javadoc       jjs         keytool       rmid
clhsdb          javah         jmap        lli           rmiregistry
extcheck        javap         jps         native2ascii  schemagen
gu              java-rmi.cgi  jrunscript  node          serialver
hsdb            jcmd          js          npm           servertool
idlj            jconsole      jsadebugd   orbd          tnameserv
jar             jdb           jstack      pack200       unpack200
jarsigner       jdeps         jstat       policytool    wsgen
java            jhat          jstatd      polyglot      wsimport
javac           jinfo         jvisualvm   rmic          xjc

 

Como podéis ver se listan entre otros: java, javac, java, gu, js, node o npm.

Pero no vemos Ruby, Python o R. En caso de necesitarlos tendríamos que usar el Graal Updater, podríamos lanzarlo usando el comando gu del directorio bin.

Vamos a aprovechar que durante esta serie de posts crearemos alguna imagen nativa y que la herramienta native-image no se encuentra en la distribución por defecto. Empecemos comprobando qué componentes hay disponibles para instalar con el subcomando available.

bash-4.2# gu available
Downloading: Component catalog from www.graalvm.org
ComponentId              Version             Component name      Origin 
--------------------------------------------------------------------------------
native-image             19.0.0              Native Image        github.com
python                   19.0.0              Graal.Python        github.com
R                        19.0.0              FastR               github.com
ruby                     19.0.0              TruffleRuby         github.com

 

Podéis ver que native-image está en el listado. Por lo que podemos pasar a instalarlo, para ello usaremos el subcomando install:

bash-4.2# gu install native-image
Downloading: Component catalog from www.graalvm.org
Processing component archive: Native Image
Downloading: Component native-image: Native Image  from github.com
Installing new component: Native Image licence files (org.graalvm.native-image, version 19.0.0)
Refreshed alternative links in /usr/bin/

 

Si quisiésemos desinstalarlo, usaríamos el subcomando uninstall.

bash-4.2# gu uninstall native-image

 

Ejecutando aplicaciones con GraalVM

 

Ya con GraalVM instalado y añadido al path, vamos a probar unos *Hola Mundo*:

 

  • Java

bash-4.2# javac HelloWorld.java && java HelloWorld
Hello World!

 

  • JavaScript

bash-4.2# js
const sayHello = () => console.log('Hello World!')
sayHello()
Hello World!

 

bash-4.2# js HelloWorld.js
Hello World!

 

  • NodeJS
bash-4.2# node HelloWorld.js
Hello World!

 

Nada llamativo, funciona como esperábamos. Aunque hay que recordar que estamos usando la distribución de GraalVM y no las respectivas plataformas.

 

Conclusión

 

En este post hemos visto las características principales de GraalVM, los distintos tipos de compilación, cómo instalar GraalVM y cómo ejecutar nuestras aplicaciones.

El siguiente post será algo más práctico, por fin empezaremos a ver código y jugaremos un poco con la API de poliglotismo.

 

¡Suscríbete para no perdértelo!