Ejecución del programa |
---|
Conceptos generales |
Types of code |
Compilation strategies |
Notable runtimes |
|
Notable compilers & toolchains |
|
En informática , la compilación justo a tiempo ( JIT ) (también traducción dinámica o compilaciones en tiempo de ejecución ) [1] es la compilación (de código informático ) durante la ejecución de un programa (en tiempo de ejecución ) en lugar de antes de la ejecución. [2] Esto puede consistir en la traducción del código fuente , pero es más comúnmente la traducción del código de bytes a 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 está ejecutando 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 máquina: compilación anticipada (AOT) e interpretación , y combina algunas ventajas y desventajas de ambos. [2] En términos generales, 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 las aceleraciones específicas de la microarquitectura . [nb 1] [3] La interpretación y la compilación JIT son particularmente adecuadas para lenguajes de programación dinámicos , ya que el sistema de tiempo de ejecución puede manejar tipos de datos enlazados tardíamente y hacer cumplir las garantías de seguridad.
El primer compilador JIT publicado se atribuye generalmente al trabajo en LISP de John McCarthy en 1960. [4] 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, lo que evita la necesidad de guardar la salida del compilador en tarjetas perforadas [5] (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 . [6] Para mayor velocidad, Thompson implementó la coincidencia de expresiones regulares mediante JITing al código IBM 7094 en el Sistema de tiempo compartido compatible . [4] 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² . [7] [8]
Smalltalk (c. 1983) fue pionero en nuevos aspectos de las compilaciones JIT. Por ejemplo, la traducción a código de máquina se hacía a pedido y el resultado se almacenaba en caché para su uso posterior. Cuando la memoria escaseaba, el sistema borraba parte de este código y lo regeneraba cuando era necesario nuevamente. [2] [9] El lenguaje Self de Sun mejoró estas técnicas considerablemente y en un momento fue el sistema Smalltalk más rápido del mundo, logrando hasta la mitad de la velocidad de C optimizado [10] pero con un lenguaje completamente orientado a objetos.
Sun abandonó Self, pero la investigación se centró en el lenguaje Java. El término "compilación Just-in-time" se tomó prestado del término de fabricación " Just in time " y fue popularizado por Java, con James Gosling utilizando el término a partir de 1993. [11] Actualmente, la mayoría de las implementaciones de la máquina virtual Java utilizan JITing , ya que 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 "bytecode" y el formato de código de máquina eran el mismo; el sistema optimizaba el código de máquina PA-8000 . [12] Contrariamente a la intuición, esto dio como resultado aumentos de velocidad, en algunos casos del 30%, ya que hacer esto permitió optimizaciones a nivel de código de máquina, por ejemplo, código en línea para un mejor uso de la memoria caché y optimizaciones de llamadas a bibliotecas dinámicas y muchas otras optimizaciones en tiempo de ejecución que los compiladores convencionales no pueden intentar. [13] [14]
En noviembre de 2020, PHP 8.0 introdujo un compilador JIT. [15]
En un sistema compilado con bytecode, el código fuente se traduce a una representación intermedia conocida como bytecode . El bytecode no es el código de máquina para ningún ordenador en particular, y puede ser portable entre arquitecturas informáticas. El bytecode puede ser interpretado por, 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 bytecode, generalmente con un rendimiento mucho menor. Algunos intérpretes incluso interpretan el código fuente, sin el paso de compilar primero en bytecode, con un rendimiento incluso 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ámico es uno en el que se puede utilizar el compilador 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 al mismo tiempo las ventajas de la interpretación del bytecode: Gran parte del "trabajo pesado" de analizar el código fuente original y realizar una optimización básica a menudo se maneja en el momento de la compilación, antes de la implementación: la compilación de bytecode a código de máquina es mucho más rápida que la compilación desde la fuente. El bytecode implementado es portátil, a diferencia del código nativo. Dado que el entorno de ejecución tiene control sobre la compilación, como el bytecode interpretado, puede ejecutarse en un entorno seguro. Los compiladores de bytecode a código de máquina son más fáciles de escribir, porque el compilador de bytecode portátil 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: [16] [17]
Debido a que un JIT debe renderizar y ejecutar una imagen binaria nativa en tiempo de ejecución, los JIT de código de máquina verdaderos necesitan plataformas que permitan ejecutar datos en tiempo de ejecución, lo que hace imposible el uso de dichos JIT en una máquina basada en la arquitectura Harvard ; lo mismo puede decirse de ciertos sistemas operativos y máquinas virtuales también. Sin embargo, un tipo especial de "JIT" puede potencialmente no apuntar a la arquitectura de CPU de la máquina física, sino más bien a un bytecode de VM optimizado donde prevalecen las limitaciones del código de máquina sin procesar, especialmente cuando la VM de ese bytecode eventualmente aprovecha un JIT para convertirlo en código nativo. [18]
JIT provoca un retraso leve o notable en la ejecución inicial de una aplicación, debido al tiempo que se tarda en cargar y compilar el código de entrada. A veces, este retraso se denomina "retraso del tiempo de inicio" o "tiempo de calentamiento". En general, cuanto 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 equilibrio 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 IO además de la compilación JIT: por ejemplo, el archivo de datos de la clase rt.jar para la máquina virtual Java (JVM) es de 40 MB y la JVM debe buscar una gran cantidad de datos en este archivo contextualmente enorme. [19]
Una posible optimización, utilizada por la máquina virtual Java HotSpot 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 bytecode que se ejecuta solo unas pocas veces, esto ahorra tiempo de compilación y reduce la latencia inicial; para el bytecode que se ejecuta con frecuencia, se utiliza la compilación JIT 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, se pueden recopilar estadísticas de ejecución antes de la compilación, lo que ayuda a realizar una mejor optimización. [20]
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 el modo cliente, se realiza una compilación y optimización mínimas para reducir el tiempo de inicio. En el modo servidor, se realiza una compilación y optimización extensas para maximizar el rendimiento una vez que la aplicación se está ejecutando sacrificando el tiempo de inicio. Otros compiladores de Java justo a tiempo han utilizado una medición del tiempo de ejecución de la cantidad de veces que se ha ejecutado un método combinado con el tamaño del código de bytes de un método como una heurística para decidir cuándo compilar. [21] Otro utiliza la cantidad de veces ejecutadas combinada con la detección de bucles. [22] 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 larga. [23]
Native Image Generator (Ngen) de Microsoft es otro enfoque para reducir el retraso inicial. [24] Ngen precompila (o "pre-JIT") el 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 DLL de la biblioteca de Microsoft justo después de la instalación. La pre-compilación proporciona una forma de mejorar el tiempo de inicio. Sin embargo, la calidad del código que genera puede no ser tan buena como la del código compilado 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 datos de creación de perfiles para impulsar, por ejemplo, el almacenamiento en caché en línea. [25]
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 ).
La compilación JIT puede no lograr de manera confiable su objetivo, es decir, entrar en un estado estable de rendimiento mejorado después de un breve período de calentamiento inicial. [26] [27] En ocho máquinas virtuales diferentes, Barrett et al. (2017) midieron seis microbenchmarks ampliamente utilizados que los implementadores de máquinas virtuales usan comúnmente como objetivos de optimización, ejecutándolos repetidamente dentro de una sola ejecución de proceso. [28] En Linux , encontraron que entre el 8,7% y el 9,6% de las ejecuciones de procesos no alcanzaron un estado estable de rendimiento, entre el 16,7% y el 17,9% entraron en un estado estable de rendimiento reducido después de un período de calentamiento y el 56,5% de los emparejamientos de una máquina virtual específica que ejecutaba un punto de referencia específico no vieron consistentemente una no degradación del rendimiento en estado estable en múltiples ejecuciones (es decir, al menos una ejecución no alcanzó un estado estable o vio un rendimiento reducido en el estado estable). Incluso cuando se alcanzó un estado estable mejorado, a veces se necesitaron muchos cientos de iteraciones. [29] Traini et al. (2022) en cambio se centró en la máquina virtual HotSpot pero con una gama mucho más amplia de puntos de referencia, [30] encontrando que el 10,9% de las ejecuciones de procesos no lograron alcanzar un estado estable de rendimiento, y el 43,5% de los puntos de referencia no alcanzaron consistentemente un estado estable en múltiples ejecuciones. [31]
La compilación JIT utiliza fundamentalmente datos ejecutables, por lo que plantea desafíos de seguridad y posibles vulnerabilidades.
La implementación de la compilación JIT consiste en compilar el código fuente o el código de bytes en código de máquina y ejecutarlo. Esto se hace generalmente directamente en la memoria: el compilador JIT envía el código de máquina directamente a 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 previa habitual. En las arquitecturas modernas esto se topa con un problema debido a la protección del espacio ejecutable : no se puede ejecutar 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 escribible/ejecutable es un agujero de seguridad (ver W^X ). [32] Por ejemplo, el compilador JIT de Firefox para Javascript introdujo esta protección en una versión de lanzamiento con Firefox 46. [33]
La pulverización JIT es una clase de exploits de seguridad informática que utilizan la compilación JIT para la pulverización de montón : la memoria resultante es luego ejecutable, lo que permite una explotación si la ejecución se puede mover al montón.
La compilación JIT se puede aplicar a algunos programas o se puede utilizar para ciertas capacidades, en particular capacidades dinámicas como las expresiones regulares . Por ejemplo, un editor de texto puede compilar una expresión regular proporcionada en tiempo de ejecución a código de máquina para permitir una coincidencia más rápida: esto no se puede hacer con anticipación, ya que el patrón solo se proporciona en tiempo de ejecución. Varios entornos de ejecución modernos dependen de la compilación JIT para la ejecución de código a alta velocidad, incluidas 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 a código de bytes o a código de máquina. La compilación JIT también se utiliza en algunos emuladores, para traducir código de 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 a bytecode ( código de máquina virtual ), conocida como compilación de bytecode , y luego tener la compilación JIT a código de máquina (compilación dinámica), en lugar de la interpretación del bytecode. Esto mejora el rendimiento en 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, al igual que 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 significativamente menos retraso que si se compilara todo el programa antes de la ejecución.
{{cite journal}}
: Requiere citar revista |journal=
( ayuda ){{cite web}}
: CS1 maint: unfit URL (link)