Ejecución del programa |
---|
Conceptos generales |
Tipos de código |
Estrategias de compilación |
|
Tiempos de ejecución notables |
|
Compiladores y cadenas de herramientas notables |
El seguimiento de la compilación Just-in-Time es una técnica que utilizan las máquinas virtuales para optimizar la ejecución de un programa en tiempo de ejecución . Esto se hace registrando una secuencia lineal de operaciones que se ejecutan con frecuencia, compilándolas en código de máquina nativo y ejecutándolas. Esto se opone a los compiladores Just-in-Time (JIT) tradicionales que funcionan por método.
La compilación justo a tiempo es una técnica que permite aumentar la velocidad de ejecución de los programas compilando partes de un programa en código de máquina en tiempo de ejecución. Una forma de clasificar los distintos compiladores JIT es por su ámbito de compilación. Mientras que los compiladores JIT basados en métodos traducen un método a la vez en código de máquina, los JIT de seguimiento utilizan bucles ejecutados con frecuencia como unidad de compilación. Los JIT de seguimiento se basan en la suposición de que los programas pasan la mayor parte de su tiempo en algunos bucles del programa ("bucles activos") y las iteraciones de bucle posteriores suelen seguir rutas similares. Las máquinas virtuales que tienen un JIT de seguimiento suelen ser entornos de ejecución de modo mixto, lo que significa que tienen un intérprete o un compilador de métodos además del JIT de seguimiento.
Un compilador JIT de seguimiento pasa por varias fases en tiempo de ejecución. En primer lugar, se recopila información de creación de perfiles para los bucles. Una vez identificado un bucle activo, se pasa a una fase de seguimiento especial , que registra todas las operaciones ejecutadas de ese bucle. Esta secuencia de operaciones se denomina seguimiento. A continuación, el seguimiento se optimiza y se compila en código de máquina. Cuando se vuelve a ejecutar este bucle, se llama al seguimiento compilado en lugar del programa homólogo.
Estos pasos se explican en detalle a continuación:
El objetivo de la creación de perfiles es identificar los bucles activos. Esto se suele hacer contando la cantidad de iteraciones de cada bucle. Una vez que el recuento de un bucle supera un umbral determinado, se considera que el bucle está activo y se inicia la fase de seguimiento.
En la fase de seguimiento, la ejecución del bucle se lleva a cabo de forma normal, pero además, cada operación ejecutada se registra en un seguimiento. Las operaciones registradas se almacenan normalmente en el árbol de seguimiento , a menudo en una representación intermedia (IR). El seguimiento sigue a las llamadas de función, lo que hace que se incorporen en el seguimiento. El seguimiento continúa hasta que el bucle llega a su final y vuelve al inicio.
Dado que el seguimiento se registra siguiendo una ruta de ejecución concreta del bucle, las ejecuciones posteriores de ese seguimiento pueden desviarse de esa ruta. Para identificar los lugares donde esto puede suceder, se insertan instrucciones de protección especiales en el seguimiento. Un ejemplo de un lugar así son las instrucciones if. La protección es una comprobación rápida para determinar si la condición original sigue siendo verdadera. Si una protección falla, se cancela la ejecución del seguimiento.
Dado que el seguimiento se realiza durante la ejecución, se puede hacer que el seguimiento contenga información de tiempo de ejecución (por ejemplo, información de tipo ). Esta información se puede utilizar posteriormente en la fase de optimización para aumentar la eficiencia del código.
Los rastros son fáciles de optimizar, ya que representan solo una ruta de ejecución, lo que significa que no existe un flujo de control y no necesita manejo. Las optimizaciones típicas incluyen la eliminación de subexpresiones comunes , la eliminación de código muerto , la asignación de registros , el movimiento de código invariante , el plegado constante y el análisis de escape . [1]
Después de la optimización, el rastro se convierte en código de máquina. Al igual que la optimización, esto es fácil debido a la naturaleza lineal de los rastros.
Una vez compilado el seguimiento en código de máquina, se puede ejecutar en iteraciones posteriores del bucle. La ejecución del seguimiento continúa hasta que falla una protección.
Si bien la idea de los JIT se remonta a la década de 1960, los JIT de seguimiento se han utilizado con más frecuencia solo recientemente. La primera mención de una idea similar a la idea actual de los JIT de seguimiento fue en 1970. [2] Se observó que el código compilado se podía derivar de un intérprete en tiempo de ejecución simplemente almacenando las acciones realizadas durante la interpretación.
La primera implementación del rastreo es Dynamo, "un sistema de optimización dinámica de software que es capaz de mejorar de forma transparente el rendimiento de un flujo de instrucciones nativo a medida que se ejecuta en el procesador". [3] Para ello, se interpreta el flujo de instrucciones nativo hasta que se encuentra una secuencia de instrucciones "caliente". Para esta secuencia se genera una versión optimizada, se almacena en caché y se ejecuta.
Dynamo se amplió posteriormente a DynamoRIO . Un proyecto basado en DynamoRIO fue un marco para la construcción de intérpretes que combina el seguimiento y la evaluación parcial. Se utilizó para "eliminar dinámicamente la sobrecarga de los intérpretes de las implementaciones de lenguaje". [4]
En 2006, se desarrolló HotpathVM, el primer compilador JIT de rastreo para un lenguaje de alto nivel [ cita requerida ] . [5] Esta máquina virtual era capaz de identificar dinámicamente instrucciones de código de bytes ejecutadas con frecuencia, que se rastrean y luego se compilan en código de máquina mediante la construcción de asignación única estática (SSA). La motivación para HotpathVM era tener una JVM eficiente para dispositivos móviles con recursos limitados.
Otro ejemplo de un JIT de seguimiento es TraceMonkey , una de las implementaciones de JavaScript de Mozilla para Firefox (2009). [6] TraceMonkey compila seguimientos de bucles ejecutados con frecuencia en el lenguaje dinámico JavaScript en tiempo de ejecución y especializa el código generado para los tipos dinámicos reales que ocurren en cada ruta.
Otro proyecto que utiliza el rastreo de JIT es PyPy . Permite el uso de JIT de rastreo para implementaciones de lenguaje que se escribieron con la cadena de herramientas de traducción de PyPy, mejorando así el rendimiento de cualquier programa que se ejecute utilizando ese intérprete. Esto es posible al rastrear al intérprete en sí, en lugar del programa que ejecuta el intérprete. [7]
Microsoft también ha explorado el seguimiento de JIT en el proyecto SPUR para su lenguaje intermedio común (CIL). SPUR es un rastreador genérico para CIL, que también se puede utilizar para rastrear una implementación de JavaScript. [8]
Considere el siguiente programa Python que calcula una suma de cuadrados de números enteros sucesivos hasta que esa suma exceda 100000:
def cuadrado ( x ): devuelve x * xi = 0 y = 0 mientras sea verdadero : y += cuadrado ( i ) si y > 100000 : romper i = i + 1
Un seguimiento de este programa podría verse así:
loopstart ( i1 , y1 ) i2 = int_mul ( i1 , i1 ) # i*i y2 = int_add ( y1 , i2 ) # y += i*i b1 = int_gt ( y2 , 100000 ) guard_false ( b1 ) i3 = int_add ( i1 , 1 ) # i = i+1 jump ( i3 , y2 )
Observe cómo la llamada de función square
se incorpora al seguimiento y cómo la declaración if se convierte en un guard_false
.