C#compilación JIT y.NET
(4)
Me he confundido un poco sobre los detalles de cómo funciona el compilador JIT. Sé que C # compila a IL. La primera vez que se ejecuta es JIT''d. ¿Esto implica que se traduzca al código nativo? ¿El tiempo de ejecución .NET (como una máquina virtual?) ¿Interactúa con el código JIT? Sé que esto es ingenuo, pero realmente me he confundido. Mi impresión siempre ha sido que los ensamblados no son interpretados por .NET Runtime, pero no entiendo los detalles de la interacción.
.NET usa un lenguaje intermedio llamado MSIL, a veces abreviado como IL. El compilador lee su código fuente y produce MSIL. Cuando ejecuta el programa, el compilador .NET Just In Time (JIT) lee su código MSIL y produce una aplicación ejecutable en la memoria. No verá nada de esto, pero es una buena idea saber qué está sucediendo detrás de escena.
Describiré la compilación del código IL en las instrucciones nativas de la CPU mediante el siguiente ejemplo.
public class Example
{
static void Main()
{
Console.WriteLine("Hey IL!!!");
}
}
Principalmente CLR conoce todos los detalles sobre el tipo y el método que se llama desde ese tipo, esto se debe a los metadatos.
Cuando CLR comienza a ejecutar IL en instrucciones de CPU nativas ese tiempo, CLR asigna estructuras de datos internas para cada tipo al que hace referencia el código de Main.
En nuestro caso, solo tenemos un tipo de Consola, por lo que CLR asignará una estructura de datos interna a través de esa estructura interna, gestionaremos el acceso a los tipos a los que se hace referencia
dentro de esa estructura de datos CLR tiene entradas sobre todos los métodos definidos por ese tipo. Cada entrada contiene la dirección donde se puede encontrar la implementación del método.
Al inicializar esta estructura, CLR establece cada entrada en FUNCTION no documentada contenida dentro de CLR. Y como puede adivinar, esta FUNCIÓN es lo que llamamos JIT Compiler.
En general, podría considerar JIT Compiler como una función CLR que compila IL en instrucciones de CPU nativas. Permítame mostrarle en detalle cómo será este proceso en nuestro ejemplo.
1. Cuando Main hace su primera llamada a WriteLine se llama a la función JITCompiler.
2. La función de compilación JIT sabe qué método se está llamando y qué tipo define este método.
3.Entonces Jit Compiler busca el ensamblado donde se definió ese tipo y obtiene el código IL para el método definido por ese tipo en nuestro caso código IL del método WriteLine.
4. El compilador JIT asigna el bloque de memoria DINÁMICO , luego de que JIT verifica y compila el código IL en el código nativo de la CPU y guarda ese código CPU en ese bloque de memoria.
5.Entonces el compilador JIT vuelve a la entrada de la estructura interna de datos y reemplaza la dirección (que principalmente hace referencia a la implementación del código IL de WriteLine) con la dirección del nuevo bloque de memoria creado dinámicamente que contiene instrucciones nativas de CPU de WriteLine.
6. Finalmente, la función JIT Compiler salta al código en el bloque de memoria. Este código es la implementación del método WriteLine.
7.Después de la implementación de WriteLine, el código vuelve al código de red que continúa su ejecución de forma normal.
El código se "compila" en el Microsoft Intermediate Language, que es similar al formato de ensamblaje.
Cuando hace doble clic en un ejecutable, Windows carga mscoree.dll
que luego configura el entorno CLR e inicia el código de su programa. El compilador JIT comienza a leer el código MSIL en su programa y compila dinámicamente el código en instrucciones x86, que la CPU puede ejecutar.
Sí, el código JIT''ing IL implica traducir el IL a las instrucciones nativas de la máquina.
Sí, el tiempo de ejecución de .NET interactúa con el código máquina nativo JIT, en el sentido de que el tiempo de ejecución posee los bloques de memoria ocupados por el código máquina nativo, las llamadas de tiempo de ejecución en el código máquina nativo, etc.
Tiene razón en que el tiempo de ejecución .NET no interpreta el código IL en sus ensamblajes.
Lo que ocurre es que cuando la ejecución llega a una función o bloque de código (como, una cláusula else de un bloque if) que aún no se ha compilado JIT en código máquina nativo, se invoca JIT''r para compilar ese bloque de IL en código máquina nativo . Cuando se hace eso, la ejecución del programa ingresa el código máquina recién emitido para ejecutar su lógica de programa. Si al ejecutar esa ejecución del código máquina nativo llega a una llamada de función a una función que aún no se ha compilado en el código de máquina, se invoca el JIT''r para compilar esa función "justo a tiempo". Y así.
El JIT''r no compila necesariamente toda la lógica de un cuerpo de función en el código de máquina a la vez. Si la función tiene sentencias if, los bloques de declaración de las cláusulas if o else pueden no ser compilados por JIT hasta que la ejecución realmente pase a través de ese bloque. Las rutas de código que no se han ejecutado permanecen en forma de IL hasta que se ejecutan.
El código de máquina nativo compilado se guarda en la memoria para que pueda volver a usarse la próxima vez que se ejecute esa sección de código. La segunda vez que llamas a una función, se ejecutará más rápido que la primera vez que la llamas porque no es necesario un paso JIT la segunda vez.
En el escritorio .NET, el código máquina nativo se mantiene en la memoria durante el tiempo de vida del dominio de la aplicación. En .NET CF, el código máquina nativo puede descartarse si la aplicación se está quedando sin memoria. Se volverá a compilar JIT a partir del código IL original la próxima vez que la ejecución pase por ese código.