Visión de conjunto
Un intérprete para el lenguaje X es un programa (o una máquina, o simplemente algún tipo de mecanismo en general) que ejecuta cualquier programa p escrito en lenguaje X de tal manera que realiza los efectos y evalúa los resultados según lo prescrito por la especificación de X . Las CPU suelen ser intérpretes para sus respectivos conjuntos de instrucciones, aunque las CPU modernas de estaciones de trabajo de alto rendimiento son en realidad más complejas que eso; en realidad pueden tener un conjunto de instrucciones privadas propietarias subyacentes y traducir o compilar o interpretar el conjunto de instrucciones públicas visible desde el exterior.
Un compilador de X a Y es un programa (o una máquina, o simplemente algún tipo de mecanismo en general) que traduce cualquier programa p de algún lenguaje X a un programa semánticamente equivalente p ' en algún lenguaje Y de tal manera que la semántica del programa se conservan, es decir, que la interpretación de p ' con un intérprete para y se obtendrán los mismos resultados y tienen los mismos efectos que la interpretación de p con un intérprete para X . (Tenga en cuenta que X e Y pueden ser el mismo idioma).
Los términos Ahead-of-Time (AOT) y Just-in-Time (JIT) se refieren cuando se realiza la compilación: el "tiempo" al que se hace referencia en esos términos es "tiempo de ejecución", es decir, un compilador JIT compila el programa tal como está En ejecución , un compilador AOT compila el programa antes de que se ejecute . Tenga en cuenta que esto requiere que un compilador JIT del lenguaje X al lenguaje Y de alguna manera trabaje junto con un intérprete para el lenguaje Y, de lo contrario no habría forma de ejecutar el programa. (Entonces, por ejemplo, un compilador JIT que compila JavaScript a código de máquina x86 no tiene sentido sin una CPU x86; compila el programa mientras se está ejecutando, pero sin la CPU x86 el programa no estaría funcionando).
Tenga en cuenta que esta distinción no tiene sentido para los intérpretes: un intérprete ejecuta el programa. La idea de un intérprete AOT que ejecute un programa antes de que se ejecute o un intérprete JIT que ejecute un programa mientras se está ejecutando no tiene sentido.
Entonces tenemos:
- Compilador AOT: compila antes de ejecutar
- Compilador JIT: compila mientras se ejecuta
- intérprete: corre
Compiladores JIT
Dentro de la familia de compiladores JIT, todavía hay muchas diferencias en cuanto a cuándo se compilan exactamente , con qué frecuencia y con qué granularidad.
El compilador JIT en CLR de Microsoft, por ejemplo, solo compila código una vez (cuando se carga) y compila un ensamblaje completo a la vez. Otros compiladores pueden recopilar información mientras se ejecuta el programa y volver a compilar el código varias veces a medida que hay nueva información disponible que les permite optimizarla mejor. Algunos compiladores JIT son incluso capaces de des-optimizar el código. Ahora, podrías preguntarte por qué alguien querría hacer eso. La des-optimización le permite realizar optimizaciones muy agresivas que en realidad podrían ser inseguras: si resulta que fue demasiado agresivo, puede retroceder nuevamente, mientras que, con un compilador JIT que no puede des-optimizar, no podría haber ejecutado el optimizaciones agresivas en primer lugar.
Los compiladores JIT pueden compilar alguna unidad estática de código de una vez (un módulo, una clase, una función, un método, ...; por lo general, se denominan JIT de método a la vez ) o pueden rastrear la dinámica ejecución de código para encontrar rastreos dinámicos (generalmente bucles) que luego compilarán (se denominan JIT de rastreo ).
Combinando intérpretes y compiladores
Los intérpretes y compiladores se pueden combinar en un solo motor de ejecución de lenguaje. Hay dos escenarios típicos donde esto se hace.
La combinación de un compilador AOT de X a Y con un intérprete para Y . Aquí, típicamente X es un lenguaje de nivel superior optimizado para la legibilidad por los humanos, mientras que Yes un lenguaje compacto (a menudo algún tipo de código de bytes) optimizado para que las máquinas puedan interpretarlo. Por ejemplo, el motor de ejecución CPython Python tiene un compilador AOT que compila el código fuente de Python en el bytecode de CPython y un intérprete que interpreta el bytecode de CPython. Del mismo modo, el motor de ejecución YARV Ruby tiene un compilador AOT que compila el código fuente de Ruby en el código de bytes YARV y un intérprete que interpreta el código de bytes YARV. ¿Por qué querrías hacer eso? Ruby y Python son lenguajes de alto nivel y algo complejos, por lo que primero los compilamos en un lenguaje que es más fácil de analizar y más fácil de interpretar, y luego interpretar ese lenguaje.
La otra forma de combinar un intérprete y un compilador es un motor de ejecución de modo mixto . En este sentido, "Mix" dos "modos" de la aplicación de la misma lengua en conjunto, es decir, un intérprete para X y un compilador JIT de X a Y . (Entonces, la diferencia aquí es que en el caso anterior, tuvimos múltiples "etapas" con el compilador compilando el programa y luego enviando el resultado al intérprete, aquí tenemos los dos trabajando lado a lado en el mismo idioma. ) El código que ha sido compilado por un compilador tiende a ejecutarse más rápido que el código ejecutado por un intérprete, pero en realidad compilar el código primero lleva tiempo (y particularmente, si desea optimizar en gran medida el código para que se ejecutemuy rápido, lleva mucho tiempo). Entonces, para cerrar esta vez donde el compilador JIT está ocupado compilando el código, el intérprete ya puede comenzar a ejecutar el código, y una vez que el JIT haya terminado de compilar, podemos cambiar la ejecución al código compilado. Esto significa que obtenemos el mejor rendimiento posible del código compilado, pero no tenemos que esperar a que termine la compilación, y nuestra aplicación comienza a ejecutarse de inmediato (aunque no tan rápido como podría ser).
Esta es en realidad la aplicación más simple posible de un motor de ejecución de modo mixto. Las posibilidades más interesantes son, por ejemplo, no comenzar a compilar de inmediato, sino dejar que el intérprete se ejecute un poco y recopilar estadísticas, información de perfil, información de tipo, información sobre la probabilidad de qué ramas condicionales específicas se toman, qué métodos se llaman más a menudo, etc., y luego envíe esta información dinámica al compilador para que pueda generar un código más optimizado. Esta es también una forma de implementar la des-optimización de la que hablé anteriormente: si resulta que fuiste demasiado agresivo en la optimización, puedes tirar (una parte de) el código y volver a la interpretación. El HotSpot JVM hace esto, por ejemplo. Contiene tanto un intérprete para el código de bytes JVM como un compilador para el código de bytes JVM. (De hecho,dos compiladores!)
También es posible y de hecho común combinar estos dos enfoques: dos fases con siendo el primero un compilador AOT que compila X a Y y siendo la segunda fase de un motor de modo mixto que tanto interpreta Y y compila Y a Z . El motor de ejecución Rubinius Ruby funciona de esta manera, por ejemplo: tiene un compilador AOT que compila el código fuente de Ruby en el bytecode de Rubinius y un motor de modo mixto que primero interpreta el bytecode de Rubinius y una vez que ha reunido cierta información, compila los métodos más comúnmente llamados métodos nativos codigo de maquina.
Tenga en cuenta que el papel que desempeña el intérprete en el caso de un motor de ejecución de modo mixto, es decir, proporcionar un inicio rápido y también potencialmente recopilar información y proporcionar capacidad de recuperación, también puede ser desempeñado por un segundo compilador JIT. Así es como funciona V8, por ejemplo. V8 nunca interpreta, siempre compila. El primer compilador es un compilador muy rápido y muy delgado que se inicia muy rápido. Sin embargo, el código que produce no es muy rápido. Este compilador también inyecta código de creación de perfiles en el código que genera. El otro compilador es más lento y usa más memoria, pero produce un código mucho más rápido y puede usar la información de perfil recopilada al ejecutar el código compilado por el primer compilador.