Programar no es solo escribir el código en un editor. Es todo el proceso desde que preparamos el proyecto, escribimos el pseudocódigo y lo convertimos en código hasta que lo compilamos y depuramos y comprobamos que, en efecto, se ejecuta correctamente. Todos estos pasos son importantes dentro de un proyecto. Pero uno de los que menos solemos saber cómo funcionan, y los distintos tipos que hay es el último de ellos, la compilación. Y esto es lo que vamos a aprender hoy.
¿Qué es compilar?
Salvo que estemos programando en binario, o en un lenguaje de muy muy bajo nivel, como ensamblador, las máquinas no entienden las líneas de código que escribimos. Y, cuanto de más alto nivel es el lenguaje que utilizamos, más natural será para nosotros, pero más complejo para la máquina. Y es por ello por lo que, para convertir nuestro lenguaje de alto nivel en lenguaje de máquina necesitamos compilar el código.
Compilar el código es el proceso por el cual convertimos nuestras líneas de código de alto nivel a lenguaje máquina. Para ello es necesario tener, por un lado, con el fichero de texto plano con todo el código, y por otro con un programa, el compilador, que es el que se encarga de convertir cada una de las líneas de código en binario o en el lenguaje de bajo nivel correspondiente.
Gracias al uso de estos compiladores, programar es muy sencillo, y un mismo código puede utilizarse, con algunos ajustes, en varios tipos de máquinas diferentes. Además, como estos programas están optimizados para funcionar en arquitecturas concretas, suelen ofrecer un buen rendimiento en general. Sin embargo, no todo son ventajas. Un programa compilado solo funcionará en la máquina para la que ha sido diseñado el compilador, por ejemplo, una CPU x64 o un procesador ARM. También es necesario compilar un mismo programa varias veces en función de los sistemas operativos (Windows, macOS, Linux, Android, iOS, etc) donde lo vayamos a ejecutar.
Diferencias con el intérprete
Los intérpretes nacen precisamente con el fin de solucionar los dos problemas que acabamos de ver en los compiladores. Estos son programas que se ejecutan entre nuestro código original y nuestra máquina y se encargan de interpretar cada una de las instrucciones en función de la máquina o el sistema operativo donde lo estemos ejecutando.
Estos intérpretes se sitúan en el mismo punto donde los compiladores comenzarían a traducir el código. Así eliminan todas las limitaciones de sistema operativo o de plataforma, pudiendo usar un mismo código para todo.
Eso sí, no podemos pensar que un intérprete es perfecto. Lo primero que debemos tener en cuenta es que estos no valen para todo tipo de lenguajes de programación. Los intérpretes pueden funcionar, por ejemplo, con Python o JavaScript, pero no serían viables en otros lenguajes, como C++. Además, tener que interpretar el código a la vez que se ejecuta implica una pérdida de rendimiento considerable al tener que traducir y manejar cada instrucción como si fuera un compilador independiente.
Y es aquí donde entran en juego los compiladores JIT.
Qué es un compilador Just-In-Time
Mientras que un compilador normal se encarga de compilar todo el código cuando vamos a ejecutar el programa, convertir el código a binario y generar el ejecutable, el compilador JIT lo que hace es optimizar esta labor compilando tan solo el código de cada función cuando es necesario.
De esta manera, cuando vamos a ejecutar un programa, el compilador Just-In-Time, o JIT, solo compilará las funciones que se vayan a utilizar en ese momento, guardando el resultado en una caché. A medida que vamos utilizando el programa, cuando nos encontramos con una nueva función que aún no se ha compilado, esta se compila de nuevo. Pero, cuando nos encontramos con una función que ya se ha utilizado, en lugar de compilarla de nuevo se busca en la caché, ahorrando una importante cantidad de tiempo.
Algunos ejemplos de uso de compiladores JIT son los siguientes:
- Java: la máquina virtual de Java, JVM, utiliza Just-In-Time.
- .NET Framework: el entorno de programación de Microsoft.
- C#: CLR (Common Language Runtime).
- Android: cuando se usa con DVM (Dalvik Virtual Machine) o ART (Android RunTime).
- Emuladores: también se utilizan estos compiladores en emuladores de consolas y otros PCs. Así se traduce el código de máquina de una arquitectura de CPU a otra.
Este tipo de compiladores cuentan con un rendimiento superior al de los intérpretes, ya que, en lugar de interpretar todo el código, compilan lo que necesitan a medida que lo necesitan. Sin embargo, tener que compilar el código en el tiempo de ejecución sí que tiene un impacto, en mayor o menor medida, sobre el rendimiento en comparación a usar un compilador estándar que genere el binario y nos permita ejecutarlo directamente en la máquina. Y cuanto más grande es el programa que estamos intentando ejecutar, mayor será el impacto sobre el rendimiento. Esto hace que algunos programas muy grandes tarden hasta un minuto en ejecutar las primeras funciones.
Para reducir este impacto existen algunos pre-compiladores, como el Native Image Generator (Ngen) de Microsoft, que se encargan de eliminar el tiempo de ejecución y hacer que el compilador JIT pueda trabajar desde el primer momento.
Además, como la compilación Just-In-Time utiliza fundamentalmente datos ejecutables, protegerla de posibles exploits supone un reto muy importante para los desarrolladores. Se debe vigilar mucho la memoria y protegerla con técnicas de seguridad avanzadas, como el aislamiento, para evitar correr riesgos innecesarios.
Optimizar el compilador JIT (Just-In-Time)
Dependiendo del tipo de compilador que utilicemos es posible encontrarnos con distintos niveles de optimización de código. Por ejemplo, en el caso de OpenJ9 (compilador JIT de Eclipse para código Java), es posible elegir el nivel de optimización de código que queremos. Cuanto más alta sea la optimización del compilador Just-In-Time, más rápido se ejecutará el código en nuestro ordenador, eso sí, a costa de un uso de memoria RAM y CPU mucho mayor.
Además, estos compiladores están diseñados para analizar y rastrear las funciones de un programa y detectar cuáles son las que más se repiten. Así aplican determinadas optimizaciones para ellas, y cuales a las que menos se llama, dejándolas un poco en segundo plano para evitar el uso innecesario de recursos.