Tenemos compiladores. E intérpretes. También transpiladores. Y por si fuera poco, cada vez más escuchamos los conceptos de compilación Just-In-Time y compilación Ahead-of-Time.

El objetivo de este post es simplemente poner un poco de orden en todos estos conceptos. Esta figura repasa los conceptos ya conocidos e introduce los nuevos, usando como ejemplo el ecosistema Java. El esquema sería prácticamente el mismo con cualquier otro lenguaje. Veamos:

  • Podemos pasar de otros lenguajes a Java mediante una transpilación (un tipo especial de compilación donde los lenguajes de entrada y salida están más o menos al mismo nivel)
  • Kotlin se puede combinar con Java. En este caso, no es una transpilación ya que no generamos código Java a partir de Kotlin si no que compilamos el código Kotlink directamente a la Java Virtual Machine. El mismo tipo de compilación nos permite pasar del código Java (ficheros .java) al bytecode de Java (ficheros .class)
  • El bytecode es el formato intermedio que permite que Java sea multiplataforma. A partir de aquí hay tres maneras (más múltiples combinaciones entre ellas) de convertir ese bytecode a código ensamblador nativo de la máquina/arquitectura donde estamos ejecutando el programa Java:
    • Interpretando el código bytecode línea a línea
    • Compilándolo a medida que lo vamos necesitando (compilación Just-in-Time)
    • Precompilándolo antes de la ejecución (compilación Ahead-of-Time)

La interpretación “clásica” del bytecode es la más fácil de entender. Simplificando, el intérprete va leyendo cada línea del código, la traduce al código nativo correspondiente y la ejecuta y así hasta el final. Lento pero seguro. Y justamente para mejorar lo de “lento” salieron las dos alternativas: JIT y AOT que tienen como objetivo la mejora de la eficiencia en la ejecución de programas que se ejecutan en una Virtual Machine (como decíamos antes, no sólo la de Java, sino también cualquier otra como la cada vez más conocida GraalVM, una máquina virtual que puede ejecutar una gran variedad de lenguajes, no sólo Java)

 

Compilación Just-In-Time (JIT)

Un compilador Just-In-Time compila a código nativo parte del bytecode que el intérprete va procesando. Normalmente lo que hace el compilador JIT es observar qué partes del bytecode se ejecutan más a menudo y compilar ésas para ejecutarlas más rápidamente la próxima vez que se necesiten. ¿Y porqué no todas? Pues porqué la compilación en sí también tarda un tiempo con lo que si se compila mucho bytecode que después no se usa nunca o casi nunca pues al final lo ganado por lo perdido.

Hay varios compiladores JIT disponibles. Sólo para Java tenemos el C1 y el C2 como parte del openJDK más el qué Oracle ha creado para su máquina virtual Graal. Para los interesados en los detalles más técnicos, este post es bastante útil para entender las diferencias entre los tipos de JIT.

 

Compilación Ahead-of-Time (AOT)

A diferencia de la interpretación y la compilación JIT, la compilación Ahead-of-Time se hace antes de la ejecución del programa, no durante. De manera parecida a la compilación tradicional, la AOT genera un fichero ejecutable de forma nativa por la plataforma donde queremos ejecutar el programa. Este fichero incluye la aplicación Java, las librerías que usa, los recursos que accede,…

La diferencia es que la compilación AOT parte de un formato intermedio tipo bytecode y no del código fuente original y, más importante aún, que muchas veces se combina con las otras variantes para optimizar el código generado. Por ejemplo, se puede primero utilizar un intérprete o JIT y monitorizar qué partes del código se utilizan (y cómo) para luego generar el programa nativo optimizado para los escenarios de utilización observados en ejecuciones anteriores del mismo programa. O bien se utiliza un analizador estático del código para ver hasta qué punto hay que incluir librerías externas en el ejecutable.

Igual que para el JIT, hay muchas maneras de configurar una compilación AOT (total, parcial, más o menos agresiva respecto a las optimizaciones que aplica,…), cada una con sus ventajas y desventajas.

Y como para el caso del JIT, tampoco es buena idea aplicarla siempre. Por ejemplo, la sincronización del proceso de desarrollo se vuelve un poco más difícil (y de hecho no se puede aplicar en todos los casos por limitaciones en la complejidad del programa que se puede llegar a compilar con AOT). Dependerá de hasta qué punto las mejoras en optimización justifican este preproceso del programa.

 

Comparando JIT vs AOT

Esta charla compara los dos métodos de compilación en base a un conjunto de dimensiones: eficiencia, memoria, latencia, throughput,… Esta imagen (vía Mario Fusco) ilustra también gráficamente algunas de las diferencias.

Aunque no hay un ganador universal, parece que AOT está comiendo mucho al terreno a JIT. AOT es más rápido (no hay una fase de inicialización y permite optimizaciones más globales) y la desventaja más obvia (hay que saber de antemano en qué plataforma vamos a ejecutar el programa Java) es cada vez menos importante ya que la gran mayoría de veces sabemos ya donde lo queremos ejecutar. Thomas Wuerthinger explica mejor el futuro de la AOT.