Compilación Just-in-time (JIT)
En informática, la compilación en tiempo de ejecución o compilación just-in-time (JIT) (también traducción dinámica) es una forma de ejecutar código informático que implica la compilación durante la ejecución de un programa (en tiempo de ejecución) en lugar de antes de la ejecución. Esto puede consistir en la traducción del código fuente, pero es más común la traducción del código de bytes al código de máquina, que luego se ejecuta directamente. Un sistema que implementa un compilador JIT generalmente analiza continuamente el código que se ejecuta e identifica partes del código donde la aceleración obtenida de la compilación o recompilación superaría la sobrecarga de compilar ese código.
La compilación JIT es una combinación de los dos enfoques tradicionales para la traducción a código de máquina, la compilación anticipada (AOT) y la interpretación, y combina algunas ventajas y desventajas de ambos. Aproximadamente, la compilación JIT combina la velocidad del código compilado con la flexibilidad de la interpretación, con la sobrecarga de un intérprete y la sobrecarga adicional de compilar y vincular (no solo interpretar). La compilación JIT es una forma de compilación dinámica y permite la optimización adaptativa, como la recompilación dinámica y aceleraciones específicas de microarquitectura. La interpretación y la compilación JIT son particularmente adecuadas para los lenguajes de programación dinámicos, ya que el sistema de tiempo de ejecución puede manejar tipos de datos enlazados en tiempo de ejecución y hacer cumplir las garantías de seguridad.
Historia
El primer compilador JIT publicado generalmente se atribuye al trabajo en LISP de John McCarthy en 1960. En su artículo seminal Funciones recursivas de expresiones simbólicas y su cálculo por máquina, Parte I, menciona funciones que se traducen durante el tiempo de ejecución, ahorrando así la necesidad de guarde la salida del compilador en tarjetas perforadas (aunque esto se conocería con más precisión como un "sistema de compilación y listo"). Otro ejemplo temprano fue el de Ken Thompson, quien en 1968 dio una de las primeras aplicaciones de expresiones regulares, aquí para la coincidencia de patrones en el editor de texto QED. Para mayor velocidad, Thompson implementó la coincidencia de expresiones regulares mediante JITing con el código IBM 7094 en el sistema de tiempo compartido compatible.Una técnica influyente para derivar código compilado a partir de la interpretación fue iniciada por James G. Mitchell en 1970, que implementó para el lenguaje experimental LC².
Smalltalk (c. 1983) fue pionero en nuevos aspectos de las compilaciones JIT. Por ejemplo, la traducción a código de máquina se realizó bajo demanda y el resultado se almacenó en caché para su uso posterior. Cuando la memoria escaseaba, el sistema borraba parte de este código y lo regeneraba cuando se necesitaba de nuevo. El lenguaje Self de Sun mejoró ampliamente estas técnicas y en un momento fue el sistema Smalltalk más rápido del mundo, logrando hasta la mitad de la velocidad de C optimizado pero con un lenguaje totalmente orientado a objetos.
Self fue abandonado por Sun, pero la investigación se centró en el lenguaje Java. El término "Compilación justo a tiempo" se tomó prestado del término de fabricación "Justo a tiempo" y popularizado por Java, con James Gosling usando el término de 1993. Actualmente, JITing es utilizado por la mayoría de las implementaciones de Java Virtual Machine, como HotSpot se basa en esta base de investigación y la utiliza ampliamente.
El proyecto Dynamo de HP era un compilador JIT experimental en el que el formato de 'código de bytes' y el formato de código de máquina eran los mismos; el sistema convirtió el código de máquina PA-6000 en código de máquina PA-8000. Contrariamente a la intuición, esto resultó en aceleraciones, en algunos casos del 30 %, ya que al hacer esto se permitieron optimizaciones a nivel de código de máquina, por ejemplo, insertar código para un mejor uso de caché y optimizaciones de llamadas a bibliotecas dinámicas y muchas otras optimizaciones en tiempo de ejecución que los convencionales los compiladores no pueden intentarlo.
En noviembre de 2020, PHP 8.0 introdujo un compilador JIT.
Diseño
En un sistema compilado por bytecode, el código fuente se traduce a una representación intermedia conocida como bytecode. Bytecode no es el código de máquina para ninguna computadora en particular y puede ser portátil entre arquitecturas de computadora. El código de bytes puede ser interpretado o ejecutado en una máquina virtual. El compilador JIT lee los bytecodes en muchas secciones (o en su totalidad, rara vez) y los compila dinámicamente en código de máquina para que el programa pueda ejecutarse más rápido. Esto se puede hacer por archivo, por función o incluso en cualquier fragmento de código arbitrario; el código se puede compilar cuando está a punto de ejecutarse (de ahí el nombre "justo a tiempo"), y luego almacenarse en caché y reutilizarse más tarde sin necesidad de volver a compilarlo.
Por el contrario, una máquina virtual interpretada tradicional simplemente interpretará el código de bytes, generalmente con un rendimiento mucho menor. Algunos intérpretes incluso interpretan el código fuente, sin el paso de compilar primero en el código de bytes, con un rendimiento aún peor. El código compilado estáticamente o el código nativo se compila antes de la implementación. Un entorno de compilación dinámicoes aquel en el que el compilador se puede utilizar durante la ejecución. Un objetivo común del uso de técnicas JIT es alcanzar o superar el rendimiento de la compilación estática, manteniendo las ventajas de la interpretación del código de bytes: gran parte del "trabajo pesado" de analizar el código fuente original y realizar la optimización básica a menudo se maneja en el momento de la compilación. antes de la implementación: la compilación del código de bytes al código de máquina es mucho más rápida que la compilación desde el origen. El código de bytes implementado es portátil, a diferencia del código nativo. Dado que el tiempo de ejecución tiene control sobre la compilación, como el código de bytes interpretado, puede ejecutarse en un espacio aislado seguro. Los compiladores de código de bytes a código de máquina son más fáciles de escribir, porque el compilador portátil de códigos de bytes ya ha hecho gran parte del trabajo.
El código JIT generalmente ofrece un rendimiento mucho mejor que los intérpretes. Además, en algunos casos puede ofrecer un mejor rendimiento que la compilación estática, ya que muchas optimizaciones solo son factibles en tiempo de ejecución:
- La compilación se puede optimizar para la CPU objetivo y el modelo de sistema operativo donde se ejecuta la aplicación. Por ejemplo, JIT puede elegir instrucciones de CPU de vector SSE2 cuando detecta que la CPU las admite. Para obtener este nivel de especificidad de optimización con un compilador estático, se debe compilar un binario para cada plataforma/arquitectura deseada o bien incluir varias versiones de partes del código dentro de un solo binario.
- El sistema puede recopilar estadísticas sobre cómo se ejecuta realmente el programa en el entorno en el que se encuentra, y puede reorganizar y recompilar para un rendimiento óptimo. Sin embargo, algunos compiladores estáticos también pueden tomar información de perfil como entrada.
- El sistema puede realizar optimizaciones de código global (p. ej., incorporación de funciones de biblioteca) sin perder las ventajas de los enlaces dinámicos y sin los gastos generales inherentes a los compiladores y enlazadores estáticos. Específicamente, cuando se realizan sustituciones globales en línea, un proceso de compilación estática puede necesitar verificaciones en tiempo de ejecución y garantizar que se produzca una llamada virtual si la clase real del objeto anula el método en línea, y es posible que sea necesario procesar las verificaciones de condiciones límite en los accesos a matrices. dentro de bucles. Con la compilación justo a tiempo, en muchos casos este procesamiento se puede sacar de los bucles, lo que a menudo proporciona grandes aumentos de velocidad.
- Aunque esto es posible con lenguajes recolectados de basura compilados estáticamente, un sistema de código de bytes puede reorganizar más fácilmente el código ejecutado para una mejor utilización de la memoria caché.
Debido a que un JIT debe representar y ejecutar una imagen binaria nativa en tiempo de ejecución, los verdaderos JIT de código de máquina necesitan plataformas que permitan que los datos se ejecuten en tiempo de ejecución, lo que hace imposible el uso de dichos JIT en una máquina basada en la arquitectura de Harvard; lo mismo puede decirse de ciertos sistemas operativos y máquinas virtuales también. Sin embargo, es posible que un tipo especial de "JIT" no se dirija a la arquitectura de CPU de la máquina física, sino a un código de bytes de VM optimizado donde prevalecen las limitaciones en el código de máquina sin procesar, especialmente donde la VM de ese código de bytes finalmente aprovecha un JIT para código nativo.
Actuación
JIT provoca un retraso leve a notable en la ejecución inicial de una aplicación, debido al tiempo que se tarda en cargar y compilar el código de bytes. A veces, este retraso se denomina "retraso del tiempo de inicio" o "tiempo de calentamiento". En general, cuanta más optimización realice JIT, mejor será el código que generará, pero el retraso inicial también aumentará. Por lo tanto, un compilador JIT tiene que hacer un balance entre el tiempo de compilación y la calidad del código que espera generar. El tiempo de inicio puede incluir un aumento de las operaciones vinculadas a E/S además de la compilación JIT: por ejemplo, el archivo de datos de clase rt.jar para la máquina virtual de Java (JVM) es de 40 MB y la JVM debe buscar una gran cantidad de datos en este archivo contextualmente enorme..
Una posible optimización, utilizada por HotSpot Java Virtual Machine de Sun, es combinar la interpretación y la compilación JIT. El código de la aplicación se interpreta inicialmente, pero la JVM supervisa qué secuencias de bytecode se ejecutan con frecuencia y las traduce a código de máquina para su ejecución directa en el hardware. Para el código de bytes que se ejecuta solo unas pocas veces, esto ahorra tiempo de compilación y reduce la latencia inicial; para el código de bytes que se ejecuta con frecuencia, la compilación JIT se utiliza para ejecutarse a alta velocidad, después de una fase inicial de interpretación lenta. Además, dado que un programa pasa la mayor parte del tiempo ejecutando una minoría de su código, el tiempo de compilación reducido es significativo. Finalmente, durante la interpretación inicial del código, las estadísticas de ejecución se pueden recopilar antes de la compilación, lo que ayuda a realizar una mejor optimización.
La compensación correcta puede variar según las circunstancias. Por ejemplo, la máquina virtual Java de Sun tiene dos modos principales: cliente y servidor. En modo cliente, se realiza una compilación y optimización mínimas para reducir el tiempo de inicio. En el modo de servidor, se realiza una compilación y optimización exhaustivas para maximizar el rendimiento una vez que la aplicación se ejecuta sacrificando el tiempo de inicio. Otros compiladores justo a tiempo de Java han utilizado una medida de tiempo de ejecución del número de veces que se ha ejecutado un método combinado con el tamaño del código de bytes de un método como heurística para decidir cuándo compilar. Todavía otro usa el número de veces ejecutado combinado con la detección de bucles. En general, es mucho más difícil predecir con precisión qué métodos optimizar en aplicaciones de ejecución corta que en las de ejecución prolongada.
Native Image Generator (Ngen) de Microsoft es otro enfoque para reducir el retraso inicial. Ngen precompila (o "pre-JIT") código de bytes en una imagen de lenguaje intermedio común en código nativo de máquina. Como resultado, no se necesita compilación en tiempo de ejecución..NET Framework 2.0 incluido con Visual Studio 2005 ejecuta Ngen en todas las bibliotecas DLL de Microsoft inmediatamente después de la instalación. Pre-jitting proporciona una forma de mejorar el tiempo de arranque. Sin embargo, la calidad del código que genera podría no ser tan buena como la del JIT, por las mismas razones por las que el código compilado estáticamente, sin optimización guiada por perfiles, no puede ser tan bueno como el código compilado JIT en el caso extremo: la falta de perfiles de datos para impulsar, por ejemplo, el almacenamiento en caché en línea.
También existen implementaciones de Java que combinan un compilador AOT (ahead-of-time) con un compilador JIT (Excelsior JET) o un intérprete (GNU Compiler for Java).
Seguridad
La compilación JIT utiliza fundamentalmente datos ejecutables y, por lo tanto, plantea desafíos de seguridad y posibles vulnerabilidades.
La implementación de la compilación JIT consiste en compilar código fuente o código de bytes en código de máquina y ejecutarlo. Esto generalmente se hace directamente en la memoria: el compilador JIT genera el código de la máquina directamente en la memoria y lo ejecuta inmediatamente, en lugar de enviarlo al disco y luego invocar el código como un programa separado, como en la compilación anticipada habitual. En las arquitecturas modernas, esto se topa con un problema debido a la protección del espacio ejecutable: no se puede ejecutar una memoria arbitraria, ya que de lo contrario existe un potencial agujero de seguridad. Por lo tanto, la memoria debe marcarse como ejecutable; por razones de seguridad, esto debe hacerse después de que el código se haya escrito en la memoria y se haya marcado como de solo lectura, ya que la memoria grabable/ejecutable es un agujero de seguridad (ver W^X).Por ejemplo, el compilador JIT de Firefox para Javascript introdujo esta protección en una versión de lanzamiento con Firefox 46.
El rociado JIT es una clase de exploits de seguridad informática que utilizan la compilación JIT para el rociado de montón: la memoria resultante es luego ejecutable, lo que permite un exploit si la ejecución se puede mover al montón.
Usos
La compilación JIT se puede aplicar a algunos programas, o se puede usar para ciertas capacidades, particularmente capacidades dinámicas como expresiones regulares. Por ejemplo, un editor de texto puede compilar una expresión regular proporcionada en tiempo de ejecución en código de máquina para permitir una coincidencia más rápida: esto no se puede hacer antes de tiempo, ya que el patrón solo se proporciona en tiempo de ejecución. Varios entornos de tiempo de ejecución modernos se basan en la compilación JIT para la ejecución de código de alta velocidad, incluida la mayoría de las implementaciones de Java, junto con.NET de Microsoft. De manera similar, muchas bibliotecas de expresiones regulares cuentan con compilación JIT de expresiones regulares, ya sea en código de bytes o en código de máquina. La compilación JIT también se usa en algunos emuladores para traducir el código de la máquina de una arquitectura de CPU a otra.
Una implementación común de la compilación JIT es tener primero la compilación AOT en código de bytes (código de máquina virtual), conocida como compilación de código de bytes, y luego tener la compilación JIT en código de máquina (compilación dinámica), en lugar de la interpretación del código de bytes. Esto mejora el rendimiento del tiempo de ejecución en comparación con la interpretación, a costa del retraso debido a la compilación. Los compiladores JIT traducen continuamente, como con los intérpretes, pero el almacenamiento en caché del código compilado minimiza el retraso en la ejecución futura del mismo código durante una ejecución determinada. Dado que solo se compila una parte del programa, hay un retraso significativamente menor que si se compilara todo el programa antes de la ejecución.
Contenido relacionado
Vértice (teoría de grafos)
Algoritmo de Shor
Tabla de asignación de archivos