La compilación JIT ( inglés Just-in-Time , compilación "exactamente en el momento adecuado"), la compilación dinámica ( traducción dinámica en inglés ) es una tecnología para aumentar el rendimiento de los sistemas de software que utilizan bytecode compilando bytecode en código de máquina o en otro formato directamente mientras se ejecuta el programa. Por lo tanto, se logra una alta velocidad de ejecución en comparación con el bytecode interpretado [1] (comparable a los lenguajes compilados) debido al mayor consumo de memoria (para almacenar los resultados de la compilación) y el tiempo de compilación. JIT se basa en dos ideas anteriores sobre el entorno de tiempo de ejecución:compilación de bytecode y compilación dinámica .
Dado que la compilación JIT es, de hecho, una forma de compilación dinámica, permite el uso de tecnologías como la optimización adaptativa y la recompilación dinámica . Debido a esto, la compilación JIT puede funcionar mejor en términos de rendimiento que la compilación estática. La interpretación y la compilación JIT se adaptan particularmente bien a los lenguajes de programación dinámicos , mientras que el tiempo de ejecución maneja el enlace de tipo tardío y garantiza la seguridad del tiempo de ejecución.
Los proyectos LLVM , GNU Lightning [2] , libJIT (parte del proyecto DotGNU ) y RPython (parte del proyecto PyPy ) se pueden usar para crear intérpretes JIT para cualquier lenguaje de secuencias de comandos.
La compilación JIT se puede aplicar tanto al programa completo como a sus partes individuales. Por ejemplo, un editor de texto puede compilar expresiones regulares sobre la marcha para búsquedas de texto más rápidas. Con la compilación AOT, esto no es posible en los casos en que los datos se proporcionan durante la ejecución del programa y no en el momento de la compilación. JIT se usa en implementaciones de Java (JRE), JavaScript , .NET Framework , en una de las implementaciones de Python - PyPy . [3] Los intérpretes más comunes existentes para PHP , Ruby , Perl , Python y similares tienen JIT limitados o incompletos.
La mayoría de las implementaciones JIT tienen una estructura secuencial: primero, la aplicación se compila en el código de bytes de la máquina virtual en tiempo de ejecución (compilación AOT), y luego JIT compila el código de bytes directamente en el código de la máquina. Como resultado, se pierde tiempo adicional al iniciar la aplicación, que posteriormente se compensa con su funcionamiento más rápido.
En lenguajes como Java , PHP , C# , Lua , Perl , GNU CLISP , el código fuente se traduce en una de las representaciones intermedias denominada bytecode . El código de bytes no es el código de máquina de ningún procesador en particular y puede trasladarse a diferentes arquitecturas informáticas y ejecutarse exactamente de la misma manera. El bytecode es interpretado (ejecutado) por la máquina virtual . JIT lee el código de bytes de algunos sectores (rara vez de todos a la vez) y los compila en código de máquina. Este sector puede ser un archivo, una función o cualquier pieza de código. Una vez compilado, el código se puede almacenar en caché y luego reutilizar sin volver a compilar.
Un entorno compilado dinámicamente es un entorno en el que una aplicación puede llamar al compilador en tiempo de ejecución. Por ejemplo, la mayoría de las implementaciones de Common Lisp contienen una función compileque puede crear una función en tiempo de ejecución; en Python, esta es una función eval. Esto es conveniente para el programador, ya que puede controlar qué partes del código se compilan realmente. También es posible compilar código generado dinámicamente utilizando esta técnica, lo que en algunos casos conduce a un rendimiento incluso mejor que la implementación en código compilado estáticamente. Sin embargo, vale la pena recordar que tales funciones pueden ser peligrosas, especialmente cuando los datos se transfieren desde fuentes no confiables. [cuatro]
El objetivo principal de usar JIT es lograr y superar el rendimiento de la compilación estática mientras conserva los beneficios de la compilación dinámica:
JIT es generalmente más eficiente que la interpretación de código. Además, en algunos casos, JIT puede mostrar un mejor rendimiento en comparación con la compilación estática debido a optimizaciones que solo son posibles en tiempo de ejecución:
Una razón típica de un retraso al iniciar un compilador JIT es el costo de cargar el entorno y compilar la aplicación en código nativo. En general, cuanto mejor y más optimizaciones realice el JIT, mayor será la demora. Por lo tanto, los desarrolladores JIT deben encontrar un compromiso entre la calidad del código generado y el tiempo de inicio. Sin embargo, a menudo resulta que el cuello de botella en el proceso de compilación no es el proceso de compilación en sí, sino los retrasos del sistema de E/S (por ejemplo, rt.jar en Java Virtual Machine (JVM) tiene un tamaño de 40 MB , y la búsqueda de metadatos lleva bastante tiempo).
Otra herramienta de optimización es compilar solo aquellas partes de la aplicación que se usan con más frecuencia. Este enfoque se implementa en PyPy y en la máquina virtual HotSpot Java de Sun Microsystems .
Como heurística, se puede utilizar el recuento de lanzamientos de la sección de la aplicación, el tamaño del código de bytes o el detector de ciclos.
A veces es difícil encontrar el compromiso adecuado. Por ejemplo, la máquina virtual Java de Sun tiene dos modos de funcionamiento: cliente y servidor. En modo cliente, la cantidad de compilaciones y optimizaciones es mínima para un inicio más rápido, mientras que en modo servidor se logra el máximo rendimiento, pero debido a esto, se aumenta el tiempo de inicio.
Otra técnica llamada pre-JIT compila el código antes de que se ejecute. La ventaja de esta técnica es el tiempo de inicio reducido, mientras que la desventaja es la mala calidad del código compilado en comparación con el tiempo de ejecución JIT.
La primera implementación JIT se puede atribuir a LISP, escrito por McCarthy en 1960 [5] . En su libro Funciones recursivas de expresiones simbólicas y su cálculo por máquina, Parte I , menciona funciones que se compilan en tiempo de ejecución, eliminando así la necesidad de enviar el trabajo del compilador a tarjetas perforadas .
Otra de las primeras referencias a JIT se puede atribuir a Ken Thompson , quien en 1968 fue pionero en el uso de expresiones regulares para buscar subcadenas en el editor de texto QED . Para acelerar el algoritmo, Thompson implementó la compilación de expresiones regulares en el código de máquina IBM 7094 .
Mitchell propuso un método para obtener código compilado en 1970 cuando implementó el lenguaje experimental LC 2 . [6] [7]
Smalltalk (1983) fue un pionero en la tecnología JIT. La traducción al código nativo se realizó bajo demanda y se almacenó en caché para su uso posterior. Cuando se agotó la memoria, el sistema podría eliminar parte del código almacenado en caché de la RAM y restaurarlo cuando se vuelva a necesitar. El lenguaje de programación Self fue durante algún tiempo la implementación más rápida de Smalltalk y fue solo el doble de lento que C , siendo completamente orientado a objetos.
Self fue abandonado por Sun, pero la investigación continuó dentro del lenguaje Java. El término "compilación Just-in-time" se tomó prestado del término de la industria "Just in Time" y fue popularizado por James Gosling , quien usó el término en 1993. [8] JIT ahora se usa en casi todas las implementaciones de Java Virtual Machine . .
También es de gran interés la tesis defendida en 1994 en la Universidad ETH (Suiza, Zúrich) por Michael Franz "Dynamic code generation - the key to portable software" [9] y el sistema Juice [10] implementado por él para la generación dinámica de código de un árbol semántico portátil para el lenguaje Oberon . El sistema Juice se ofreció como un complemento para los navegadores de Internet.
Dado que JIT compone código ejecutable a partir de datos, existe una cuestión de seguridad y posibles vulnerabilidades.
La compilación JIT implica compilar código fuente o código de bytes en código de máquina y ejecutarlo. Como regla general, el resultado se escribe en la memoria y se ejecuta inmediatamente, sin guardarlo en el disco o llamarlo como un programa separado. En las arquitecturas modernas, para mejorar la seguridad, las secciones arbitrarias de la memoria no se pueden ejecutar como código de máquina ( bit NX ). Para un lanzamiento correcto, las regiones de memoria deben marcarse previamente como ejecutables, mientras que para mayor seguridad, el indicador de ejecución se puede configurar solo después de eliminar el indicador de permiso de escritura (esquema de protección W^X) [11] .