Articulo de referencia

Compilación justo a tiempo

La compilación justo a tiempo ( JIT ) (también conocida como traducción dinámica o compilación en tiempo de ejecución ) [ 1 ] consiste en la compilación de código informático du...

La compilación justo a tiempo ( JIT ) (también conocida como traducción dinámica o compilación en tiempo de ejecución ) [ 1 ] consiste en la compilación de código informático durante la ejecución de un programa, en lugar de antes de la ejecución. [ 2 ] Esto puede implicar la traducción del código fuente , pero es más común la traducción del código de bytes a código máquina , que luego se ejecuta directamente. Un sistema que implementa un compilador JIT normalmente analiza continuamente el código que se está ejecutando e identifica las partes del código donde la mejora de velocidad obtenida mediante la compilación o recompilación compensarí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: la compilación anticipada (AOT) y la interpretación , que 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 enlazar (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 de enlace tardío y aplicar garantías de seguridad.

Historia

El primer compilador JIT publicado se atribuye generalmente al trabajo de John McCarthy en LISP en 1960. [ 4 ] En su artículo fundamental Funciones recursivas de expresiones simbólicas y su computación por máquina, Parte I , menciona funciones que se traducen durante el tiempo de ejecución, evitando así la necesidad de guardar la salida del compilador en tarjetas perforadas [ 5 ] (aunque esto se conocería con mayor precisión como un " sistema de compilación y ejecución "). Otro ejemplo temprano fue el de Ken Thompson , quien en 1968 dio una de las primeras aplicaciones de expresiones regulares , en este caso para la coincidencia de patrones en el editor de texto QED . [ 6 ] Para mayor velocidad, Thompson implementó la coincidencia de expresiones regulares mediante JIT para el código IBM 7094 en el Compatible Time-Sharing System . [ 4 ] Una técnica influyente para derivar código compilado a partir de la interpretación fue iniciada por James G. Mitchell en 1970, quien la implementó para el lenguaje experimental LC² . [ 7 ] [ 8 ]

Smalltalk (c. 1980) fue pionero en nuevos aspectos de las compilaciones JIT. Por ejemplo, la traducción a código máquina se realizaba bajo demanda y el resultado se almacenaba en caché para su uso posterior. Cuando la memoria escaseaba, el sistema eliminaba parte de este código y lo regeneraba cuando se necesitaba de nuevo. [ 2 ] [ 9 ] El lenguaje Self de Sun mejoró ampliamente estas técnicas y llegó a ser el sistema Smalltalk más rápido del mundo, alcanzando hasta la mitad de la velocidad de C optimizado [ 10 ] pero con un lenguaje de programación totalmente orientado a objetos .

Sun abandonó Self, 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 fue popularizado por Java, con James Gosling utilizando el término desde 1993. [ 11 ] Actualmente, la mayoría de las implementaciones de la máquina virtual Java utilizan la compilación justo a tiempo , ya que HotSpot se basa en esta investigación y la utiliza ampliamente.

El proyecto HP Dynamo fue un compilador JIT experimental donde el formato de "bytecode" y el formato de código máquina eran iguales; el sistema optimizaba el código máquina PA-8000 . [ 12 ] Contrariamente a lo que se podría pensar, esto resultó en aumentos de velocidad, en algunos casos del 30%, ya que esto permitió optimizaciones a nivel de código máquina, por ejemplo, la inserción de código en línea para un mejor uso de la 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 octubre de 2024, CPython introdujo un compilador JIT experimental. [ 16 ]

Diseño

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 máquina de ningún ordenador en particular y puede ser portable entre diferentes arquitecturas informáticas. Posteriormente, el bytecode puede ser interpretado o ejecutado por una máquina virtual . El compilador JIT lee los bytecodes en varias secciones (o, rara vez, en su totalidad) y los compila dinámicamente a código máquina para que el programa se ejecute más rápido. Esto puede hacerse archivo por archivo, función por función o incluso en cualquier fragmento de código arbitrario; el código puede compilarse justo antes de su ejecución (de ahí el nombre "just-in-time"), y luego almacenarse en caché y reutilizarse posteriormente sin necesidad de volver a compilarse.

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 compilarlo primero a código de bytes, con un rendimiento aún peor. El código compilado estáticamente o código nativo se compila antes de la implementación. Un entorno de compilación dinámica es aquel en el que el compilador se puede usar durante la ejecución. Un objetivo común de usar 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 tiempo de compilación, antes de la implementación: la compilación de código de bytes a código máquina es mucho más rápida que la compilación desde el código fuente. El código de bytes implementado es portable, a diferencia del código nativo. Dado que el entorno de ejecución tiene control sobre la compilación, como el código de bytes interpretado, puede ejecutarse en un entorno seguro. Los compiladores de código de bytes a código máquina son más fáciles de escribir, porque el compilador de código de bytes portable 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: [ 17 ] [ 18 ]

  1. La compilación se puede optimizar para la CPU de destino y el modelo de sistema operativo donde se ejecuta la aplicación. Por ejemplo, JIT puede seleccionar instrucciones vectoriales SSE2 para la CPU cuando detecta que esta las admite. Para lograr este nivel de optimización con un compilador estático, es necesario compilar un binario para cada plataforma/arquitectura prevista, o bien incluir varias versiones de partes del código dentro de un mismo binario.
  2. El sistema puede recopilar estadísticas sobre el rendimiento real del programa en su entorno y reorganizarlo y recompilarlo para optimizarlo. Sin embargo, algunos compiladores estáticos también aceptan información de perfil como entrada.
  3. El sistema puede realizar optimizaciones de código globales (por ejemplo, la inserción de funciones de biblioteca ) sin perder las ventajas del enlace dinámico y sin la sobrecarga inherente a los compiladores y enlazadores estáticos. En concreto, al realizar sustituciones globales en línea, un proceso de compilación estática puede requerir comprobaciones en tiempo de ejecución y garantizar que se produzca una llamada virtual si la clase real del objeto sobrescribe el método insertado, y las comprobaciones de condiciones límite en los accesos a matrices pueden requerir procesamiento dentro de bucles. Con la compilación justo a tiempo, en muchos casos este procesamiento puede trasladarse fuera de los bucles, lo que suele proporcionar grandes aumentos de velocidad.
  4. Si bien esto es posible con lenguajes compilados estáticamente y con recolección de basura, un sistema de bytecode puede reorganizar más fácilmente el código ejecutado para una mejor utilización de la caché.

Dado que un JIT debe renderizar y ejecutar una imagen binaria nativa en tiempo de ejecución, los JIT de código máquina verdaderos requieren plataformas que permitan la ejecución de datos en tiempo de ejecución, lo que imposibilita 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. Sin embargo, un tipo especial de "JIT" podría no estar dirigido a la arquitectura de la CPU de la máquina física, sino a un código de bytes de máquina virtual optimizado donde prevalecen las limitaciones del código máquina puro, especialmente cuando la máquina virtual de ese código de bytes finalmente aprovecha un JIT para generar código nativo. [ 19 ]

Actuación

JIT provoca un retraso, de leve a notable, en la ejecución inicial de una aplicación, debido al tiempo necesario para cargar y compilar el código de entrada. A veces, este retraso se denomina "tiempo de arranque" o "tiempo de calentamiento". En general, cuanta más optimización realice JIT, mejor será el código que genere, pero el retraso inicial también aumentará. Por lo tanto, un compilador JIT debe encontrar un equilibrio entre el tiempo de compilación y la calidad del código que espera generar. El tiempo de arranque puede incluir un aumento de las operaciones de E/S, además de la compilación JIT: por ejemplo, el archivo de datos de clase rt.jar para la máquina virtual Java (JVM) tiene un tamaño de 40 MB y la JVM debe buscar una gran cantidad de datos en este archivo, que es considerablemente grande. [ 20 ]

Una posible optimización, utilizada por la máquina virtual Java HotSpot de Sun , consiste en combinar la interpretación y la compilación JIT. El código de la aplicación se interpreta inicialmente, pero la JVM monitoriza qué secuencias de bytecode se ejecutan con frecuencia y las traduce a código 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 de ejecución frecuente, se utiliza la compilación JIT para ejecutarse a alta velocidad, tras una fase inicial de interpretación lenta. Además, dado que un programa dedica la mayor parte del tiempo a ejecutar una minoría de su código, la reducción del tiempo de compilación es significativa. Por último, 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. [ 21 ]

La compensación adecuada 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 exhaustivas para maximizar el rendimiento una vez que la aplicación está en ejecución, a costa de un mayor tiempo de inicio. Otros compiladores Java Just-In-Time han utilizado una medición en tiempo de ejecución del número de veces que se ha ejecutado un método, combinada con el tamaño del código de bytes de un método, como heurística para decidir cuándo compilar. [ 22 ] Otro utiliza el número de ejecuciones combinado con la detección de bucles. [ 23 ] En general, es mucho más difícil predecir con precisión qué métodos optimizar en aplicaciones de corta duración que en aplicaciones de larga duración. [ 24 ]

Native Image Generator (Ngen) de Microsoft es otro enfoque para reducir el retraso inicial. [ 25 ] Ngen precompila (o "pre-JIT") el código de bytes en una imagen de Common Intermediate Language a código nativo de máquina. Como resultado, no se necesita compilación en tiempo de ejecución. .NET Framework 2.0, incluido en Visual Studio 2005, ejecuta Ngen en todos los archivos de biblioteca de vínculos dinámicos (DLL) de Microsoft justo después de la instalación. El pre-JIT proporciona una forma de reducir el tiempo de inicio. Sin embargo, la calidad del código que genera puede ser inferior a la del código compilado con 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 con JIT en el caso extremo: la falta de datos de perfilado para impulsar, por ejemplo, el almacenamiento en caché en línea. [ 26 ]

También existen implementaciones de Java que combinan un compilador AOT (compilación anticipada) 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, a saber, entrar en un estado estable de rendimiento mejorado después de un breve período de calentamiento inicial. [ 27 ] [ 28 ] En ocho máquinas virtuales diferentes, Barrett et al. (2017) midieron seis microbenchmarks ampliamente utilizados que son comúnmente utilizados por los implementadores de máquinas virtuales como objetivos de optimización, ejecutándolos repetidamente dentro de una única ejecución de proceso. [ 29 ] En Linux , encontraron que del 8,7% al 9,6% de las ejecuciones de procesos no lograron alcanzar un estado estable de rendimiento, del 16,7% al 17,9% entraron en un estado estable de rendimiento reducido después de un período de calentamiento, y el 56,5% de los pares de una máquina virtual específica que ejecutaba un benchmark específico no lograron ver consistentemente una no degradación del rendimiento en estado estable en múltiples ejecuciones (es decir, al menos una ejecución no logró alcanzar un estado estable o vio un rendimiento reducido en el estado estable). Incluso cuando se alcanzó un estado estable mejorado, a veces tomó muchos cientos de iteraciones. [ 30 ] 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, [ 31 ] 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. [ 32 ]

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 el código fuente o el código de bytes a código máquina y ejecutarlo. Esto generalmente se hace directamente en la memoria: el compilador JIT genera el código máquina directamente en la memoria y lo ejecuta inmediatamente, en lugar de generarlo en el disco y luego invocar el código como un programa separado, como en la compilación anticipada habitual. En las arquitecturas modernas, esto presenta un problema debido a la protección del espacio ejecutable : no se puede ejecutar memoria arbitraria, ya que de lo contrario existe una posible vulnerabilidad 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 una vulnerabilidad de seguridad (véase W^X ). [ 33 ] Por ejemplo, el compilador JIT de Firefox para JavaScript introdujo esta protección en una versión de lanzamiento con Firefox 46. [ 34 ]

El JIT spraying es una clase de exploits de seguridad informática que utilizan la compilación JIT para el heap spraying : la memoria resultante es ejecutable, lo que permite un exploit si la ejecución se puede trasladar al heap.

Usos

La compilación JIT se puede aplicar a algunos programas o utilizarse para ciertas funcionalidades, especialmente para funcionalidades 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 máquina para permitir una coincidencia más rápida: esto no se puede hacer con antelació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 una ejecución de código de alta velocidad, incluyendo la mayoría de las implementaciones de Java , junto con .NET de Microsoft . Del mismo modo, muchas bibliotecas de expresiones regulares incluyen la compilación JIT de expresiones regulares, ya sea a bytecode o a código máquina. La compilación JIT también se utiliza en algunos emuladores para traducir código máquina de una arquitectura de CPU a otra.

Una implementación común de la compilación JIT consiste en realizar primero una compilación AOT a código de bytes ( código de máquina virtual ), conocida como compilación de código de bytes , y luego una compilación JIT a código máquina (compilación dinámica), en lugar de interpretar el código de bytes. Esto mejora el rendimiento en tiempo de ejecución en comparación con la interpretación, a costa de un retardo 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 retardo en futuras ejecuciones del mismo código durante una ejecución determinada. Dado que solo se compila una parte del programa, el retardo es significativamente menor que si se compilara todo el programa antes de su ejecución.

Véase también

Notas

  1. Los compiladores AOT (Ahead-of-Time) también pueden dirigirse a microarquitecturas específicas, pero la diferencia entre AOT y JIT en este aspecto radica en la portabilidad. Un JIT puede generar código adaptado a la CPU que se está ejecutando en tiempo de ejecución, mientras que un AOT, en lugar de optimizar para un subconjunto generalizado de arquitecturas, debe conocer la CPU de destino de antemano: dicho código no solo puede tener un rendimiento deficiente en otros tipos de CPU, sino que puede ser directamente inestable.

Referencias

  1. Lenguajes, compiladores y sistemas de ejecución , Universidad de Michigan, Ciencias de la Computación e Ingeniería, archivado del original el 26 de marzo de 2018 , recuperado el 15 de marzo de 2018.
  2. 1 2 3 Aycock 2003 .
  3. "¿El JIT aprovecha mi CPU?" . Blog de David Notario . Consultado el 3 de diciembre de 2018 .
  4. 1 2 Aycock 2003 , 2. Técnicas de compilación JIT, 2.1 Génesis, pág. 98.
  5. McCarthy, J. (abril de 1960). "Funciones recursivas de expresiones simbólicas y su cálculo por máquina, Parte I". Communications of the ACM . 3 (4): 184– 195. CiteSeerX 10.1.1.111.8833 . doi : 10.1145/367177.367199 . S2CID 1489409 .  
  6. Thompson 1968 .
  7. Aycock 2003 , 2. Técnicas de compilación JIT, 2.2 LC², págs. 98–99.
  8. Mitchell, JG (1970). "El diseño y la construcción de sistemas de programación interactivos flexibles y eficientes".{{cite journal}}: Para citar una revista se requiere |journal=( ayuda )
  9. Deutsch, LP; Schiffman, AM (1984). "Implementación eficiente del sistema smalltalk-80" (PDF) . Actas del 11.º simposio ACM SIGACT-SIGPLAN sobre principios de lenguajes de programación - POPL '84 . págs. 297–302 . doi : 10.1145/800017.800542 . ISBN  0-89791-125-3. S2CID 3045432 . Archivado del original (PDF) el 18-06-2004. 
  10. "97-pep.ps" . research.sun.com . Archivado del original el 24 de noviembre de 2006. Consultado el 15 de enero de 2022 .
  11. Aycock 2003 , 2.14 Java, pág. 107, nota al pie 13.
  12. "Dynamo: Un sistema de optimización dinámica transparente". Vasanth Bala, Evelyn Duesterwald, Sanjeev Banerjia. Actas de PLDI '00 de la conferencia ACM SIGPLAN 2000 sobre diseño e implementación de lenguajes de programación. Páginas 1 a 12. DOI 10.1145/349299.349303. Consultado el 28 de marzo de 2012.
  13. John Jannotti. "HP's Dynamo" . Ars Technica . Consultado el 5 de julio de 2013 .
  14. "El proyecto HP Dynamo" . Archivado del original el 19 de octubre de 2002. Consultado el 12 de abril de 2016 .
  15. Tung, Liam (27 de noviembre de 2020). "Ya está disponible el lenguaje de programación PHP 8: este nuevo compilador JIT apunta a un mejor rendimiento" . ZDNet . Consultado el 28 de noviembre de 2020 .
  16. "Novedades de Python 3.13" . Documentación de Python . Consultado el 27 de noviembre de 2024 .
  17. Croce, Louis. "Compilación Just in Time" (PDF) . Universidad de Columbia . Archivado del original (PDF) el 3 de mayo de 2018.
  18. "¿Cuáles son las ventajas de la compilación JIT frente a la compilación AOT?" . Stack Overflow . 21 de enero de 2010.
  19. "Compilar un lenguaje basado en JIT a WebAssembly" . Stack Overflow . Consultado el 4 de diciembre de 2018 .
  20. Haase, Chet (mayo de 2007). "Consumer JRE: Tecnología Java más ágil y eficiente" . Sun Microsystems . Consultado el 27 de julio de 2007 .
  21. "La arquitectura del motor de rendimiento Java HotSpot" . Oracle.com . Consultado el 5 de julio de 2013 .
  22. Schilling, Jonathan L. (febrero de 2003). "Las heurísticas más simples pueden ser las mejores en los compiladores JIT de Java" (PDF) . SIGPLAN Notices . 38 (2): 36–46 . doi : 10.1145/772970.772975 . S2CID 15117148. Archivado del original (PDF) el 24 de septiembre de 2015. 
  23. ^ Toshio Suganuma, Toshiaki Yasue, Motohiro Kawahito, Hideaki Komatsu, Toshio Nakatani, "Un marco de optimización dinámica para un compilador justo a tiempo de Java", Actas de la 16ª conferencia ACM SIGPLAN sobre programación, sistemas, lenguajes y aplicaciones orientados a objetos (OOPSLA '01), págs. 180-195, 14-18 de octubre de 2001.
  24. Matthew Arnold, Michael Hind, Barbara G. Ryder, "Un estudio empírico de optimización selectiva", Actas del 13.º Taller Internacional sobre Lenguajes y Compiladores para Computación Paralela - Artículos revisados , págs. 49-67, 10-12 de agosto de 2000.
  25. «Generador de imágenes nativo (Ngen.exe)» . msdn2.microsoft.com. 5 de diciembre de 2006 . Consultado el 5 de julio de 2013 .
  26. Sweeney, Arnold (febrero de 2005). "Un estudio sobre la optimización adaptativa en máquinas virtuales" (PDF) . Actas del IEEE . 92 (2): 449–466 . Archivado del original (PDF) el 29 de junio de 2016.
  27. Barrett et al. 2017 , pág. 3.
  28. ^ Traini y col. 2022 , pág. 1.
  29. Barrett et al. 2017 , págs. 5-6.
  30. Barrett et al. 2017 , págs. 12-13.
  31. ^ Traini y col. 2022 , pág. 17-23.
  32. ^ Traini y col. 2022 , pág. 26-29.
  33. "Cómo implementar JIT  : una introducción", Eli Bendersky, 5 de noviembre de 2013 a las 5:59 a. m.
  34. De Mooij, enero. "Código JIT W^X habilitado en Firefox" . Jan De Mooij . Consultado el 11 de mayo de 2016 .

Bibliografía

  • Barrett, Ed; Bolz-Tereick, Carl Friedrich; Killick, Rebecca ; Mount, Sarah; Tratt, Laurence (12 de octubre de 2017). "El calentamiento de la máquina virtual produce resultados inconsistentes". Actas de la ACM sobre lenguajes de programación . 1 : 1–27 . arXiv : 1602.00602 . doi : 10.1145/3133876 . S2CID 1036324 . 
  • Traini, Luca; Cortellessa, Vittorio; Di Pompeo, Daniele; Tucci, Michele ( 30 de septiembre de 2022). "Hacia una evaluación efectiva del rendimiento en estado estacionario en el software Java: ¿Hemos llegado ya?". Ingeniería de Software Empírica . 28. arXiv : 2209.15369 . doi : 10.1007/s10664-022-10247-x . S2CID 252668652 . 
  • Aycock, J. (junio de 2003). "Una breve historia del sistema justo a tiempo". ACM Computing Surveys . 35 (2): 97– 113. CiteSeerX 10.1.1.97.3985 . doi : 10.1145/857076.857077 . S2CID 15345671 .  
  • Thompson, K. (1968). "Técnicas de programación: algoritmo de búsqueda de expresiones regulares" . Communications of the ACM . 11 (6): 419– 422. doi : 10.1145/363347.363387 . S2CID 21260384 . 
  • Entrada del diccionario gratuito en línea de informática
  • Mozilla Nanojit archivado el 9 de mayo de 2012 en Wayback Machine : una pequeña biblioteca de software C++ multiplataforma que genera código máquina. Se utiliza como JIT para los motores JavaScript Mozilla Tamarin y SpiderMonkey .
  • Análisis del rendimiento del código generado e interpretado en tiempo de ejecución mediante el analizador de rendimiento VTune.