C++/CLI y C#: Asombrosas diferencias en el optimizador de Código (II)

En mi anterior entrada demostraba la poca calidad del optimizador de C# frente a la del de C++/CLI. Comentarios vertidos en dicha entrada me han puesto en duda sobre si el jitter era capaz de realizar las optimizaciones oportunas o no sobre el código anteriormente citado. Lo cierto es que por mi experiencia práctica, la respuesta es que no, que por muy moderno, listo y guapo que fuera, como mucho cambiaría el orden de las instrucciones para mejorar los pipelines y el predictor de salto del procesador (aparte de, evidentemente, traducir el pseudoensamblador MSIL en código máquina del procesador que se trate), pero que desde luego no era capaz de reordenar bucles y comerse código muerto.

La explicación lógica es bastante sencilla desde mi modo de ver: mientras que el compilador tiene acceso al código fuente y puede ver qué está haciendo el programador, el jitter (cualquier jitter en realidad) no lo tiene, y podría ser muy peligroso cambiar el orden de un bucle ya que podría tener efectos laterales no deseados.

A ello se añade el hecho de que es un elemento de tiempo de ejecución, y cuanto menos tarde a realizar sus tareas, mejor.

Pero dejémonos de rollos y vayamos a lo práctico. Tomemos el código siguiente (que aparece en la anterior entrada y que en la solución suminstrada se llama TestCS2):

using System; 
using System.Collections.Generic; 
using System.Text; 

namespace TestCS1 
{ 
    class Program 
    { 
        static void CallPrint() 
        { 
            //Console.WriteLine("CallPrint"); 
        } 
        static void DoWork() 
        { 
            for (int i = 0; i < 1000; i++) 
                CallPrint(); 
        } 
        static void Main(string[] args) 
        { 
            DoWork(); 
            Console.WriteLine("Hello World"); 
        } 
    } 
} 

En la anterior entrada se demostró que el compilador de C# no eliminaba todo el código muerto que hay en la llamada a DoWork() y que el de C++/CLI sí lo hacía. Luego surgió el tema de que el jitter sería capaz de terminar la pobre acción del compilador. Para ello, pongamos un punto de interrupción en la llamada a DoWork() dentro de main(), lancemos el programa y esperemos a que se pare en el punto de interrupción:

 

Ahora nos vamos al menú «Debug», «Windows» y seleccionemos la ventana «Disassembly», o pulsemos directamente ALT-F8:

En el punto de interrupción vemos que hay una llamada de subrutina en código nativo a una dirección absoluta: call dword ptr ds:[001D9774h].

Pulsemos F11 o Step Into:

Vaya, el bucle muerto sigue ahí, con todas sus instrucciones. Pero sigamos ejecutando paso a paso hasta llegar al call que se puede ver en la línea 0x00000016, y luego volvamos a pulsar F11:

¡Hasta la llamada a la rutina de código muerto! ¡No ha sido capaz ni de eliminar un salto a algo que simplemente retorna sin hacer nada…

Demostramos así que el jitter ni elimina el código muerto, ni nada de nada.

Y no nos hemos fijado ni en la serie de nop que rellenan todo el código, ni en esos extraños call 79885AAE (que me imagino qué son), ni la extraña forma que tiene el bucle…

Solamente, para echar más leña al fuego, veamos cómo queda el bucle del primer programa realizado en C++/CLI (el que hace la llamada a WtiteLine() pero que el optimizador mete todo dentro de main():

La llama a WriteLine() es el call de la línea 0x00000020.

Ahora comparemos ese bucle con el que ha realizado el jitter para el programa hecho en C#.

Para aquellos que no lo tengan muy claro, el bucle lo forman las líneas que van desde 0x00000015 a 0x00000026, es decir, traer desde memoria el valor de la cadena, llamar a WriteLine(), decrementar el valor de 1000 y saltar si no es cero.

La única diferencia entre este código y el de un teórico programa en C++ que hiciera lo mismo es la línea que trae el puntero a la cadena, que estaría fuera del bucle, pero como estamos en .NET y nunca tendremos la certeza de dónde está realmente la cadena, tenemos que traerla no sea que el recolector de basura la haya movido.

5 comentarios sobre “C++/CLI y C#: Asombrosas diferencias en el optimizador de Código (II)”

  1. La comparativa que haces no puede tomarse como válida porque no tienes en cuenta los detalles de funcionamiento del jitter.

    Según la documentación del jitter de .Net el proceso de compilado lo realiza en varias fases. A parte del echo de que en depuración no actuan los optimizadores, el jitter primero realiza una compilación con optimizaciones muy suaves. En posteriores fases realiza optimizaciones sobre el código teniendo en cuenta, entre otros, los detalles del procesador sobre el que está ejecutándose, y otros detalles del entorno en el que funciona así como la información de ejecución que la propia maquina virtual recopila durante la ejecución de la aplicación.

    Por supuesto estas optimizaciones no se ejecutan inmediatamente y sólo pueden apreciarse en aplicaciones que se ejecuten durante grandes periodos o de forma permanente.

    Es cierto que hoy por hoy el jitter de .Net no puede competir con una aplicación c/c++ compilada con optimizaciones ajustadas pero si estoy convencido que a medio o largo plazo los jitters llegarán a ser capaces de optimizar el código hasta compararse con aplicaciones desarrolladas en ensamblador.

  2. El código que debugeas desde el visual no es optimizado tampoco por el jitter. Dicho de mejor forma:

    «First of all, it’s pretty obvious that in order to see JIT optimizations, you must look at the assembly code emitted by the JIT in run-time for the release build of your application. However, there is a minor caveat: if you try to look at the assembly code from within a Visual Studio debug session, you will not see optimized code. This is due to the fact JIT optimizations are disabled by default when the process is launched under the debugger (for convenience reasons). Therefore, in order to see JIT-optimized code, you must attach Visual Studio to a running process, or run the process from within CorDbg with the JitOptimizations flag on (by issuing the «mode JitOptimizations 1″ command from within the cordbg> command prompt).»

    Leete este post sobre el tema que está bastante interesante:

    http://blogs.microsoft.co.il/blogs/sasha/archive/2007/02/27/JIT-Optimizations_2C00_-Inlining_2C00_-and-Interface-Method-Dispatching-_2800_Part-1-of-N_2900_.aspx

    Como ves, el jitter es capaz de hacer inlines y varias optimizaciones sobre el código. Y según pase el tiempo irá acercándose más al código generado por un compilador (aún le queda, pero tiempo al tiempo).

    Un saludo!

    Vicente

  3. Vicente, tienes razón pero a medias… la optimización no es que sea muy buena, pero al menos se ha eliminado la llamada al método vacío. Aun así, sigue siendo mucho menos óptimo.

Deja un comentario

Tu dirección de correo electrónico no será publicada. Los campos obligatorios están marcados con *