C++ y C#: Enumeraciones compartidas

Acabo de leer en uno de los blogs a los que estoy suscrito un tema que considero bastante curioso e interesante, no porque sea una cosa que se vaya a utilizar todos los días (de hecho desaconsejo su uso), pero tiene su aplicación y su motivo.


El artículo se titula “Sharing enums across C# and C++”, es decir, “Compartiendo enumeraciones entre C# y C++”. Ojo, no C++/CLI, sino C++. El de toda la vida.


Para aquellos que no se aclaren mucho con el inglés, lo que básicamente hace el autor es explicar que a veces es necesario compartir código nativo con manejado, y que dicha compartición necesita de enumeraciones que son iguales en ambos lados. La solución obvia es mantener dos copias de la enumeración, pero eso resulta propenso a errores (lo más fácil es que se te olvide actualizar uno de los dos lados). Por ello, lo que comenta es crearte un fichero con la extensión .CS que contenga la citada enumeración e incluirla en la parte de C++, ya que el preprocesador de C++ no fuerza que los ficheros tengan ciertas extensiones en concreto. Si las tiene, él sabe cómo operar con ellas por defecto. Si no las tiene, pues para eso están las opciones de compilación, para decirle que un archivo con la extensión “.halakerollo” es un fichero de C++.


Extendiendo un poco más el tema, podríamos incluso hacernos un pre-preprocesador para que tomara un fichero fuente en C# y nos generara uno válido en C++, que es lo que hace, por ejemplo, la utilidad “MOC” de QT, que traduce el código generado por el diseñador visual a C++.


Y siempre te queda el uso de macros del preprocesador. Por ejemplo, supongamos que necesitamos una clase adaptadora muy sencilla:



ref class Adap
{
    static String ^Adapta
 (String ^cad){…}
};


Y que dicha clase ha de estar disponible y compartida entre C++/CLI y C++. ¿Cómo lo haríamos con un solo código fuente? Sencillo:



#ifndef __cplusplus_cli
    #define ref
    #define ^  *
    #define String std::string
#endif


 ref class Adap
{
    static String ^Adapta
 (String ^cad){…}
};


Y estaremos trabajando con strings .NET o strings STL según la clase esté compilada en C++/CLI o C++. Lo mismo puede hacerse con C# y C++/CLI ó C++, aunque en este caso debemos forzar que las definiciones estén del lado de C++ o de C++/CLI, ya que el preprocesador de C# es meramente testimonial.


[Nota: no he comprobado el ejempo, pero tiene que funcionar]

Refactoriza, refactoriza (que algo quedará)

La mayoría de programadores de C++, tanto nativos como de .NET, achacamos a Microsoft el hecho de que Visual Studio 2008 nos deje un poco en la cuneta en cuanto a las opciones de refactorización, lo que han dado en llamar IntelliSense, y el soporte para diseño visual de clases. Realmente se trata de elementos que no hemos tenido presentes hasta hace poco, pero una vez acostumbrados a disponer de ellos, echamos de menos seriamente su falta porque aceleran y simplifican drásticamente nuestra labor como desarrolladores.


El diseñador visual de clases viene ya incorporado en Visual Studio 2008, así que ya falta menos. Mientras, pues podemos usar herramientas de terceros como Enterprise Architect, aunque como a mi nos quede demasiado grande.


El IntelliSense no funciona en C++/CLI y apenas lo hace en C++. El mayor problema es la lentitud en interpertar nuestro código fuente; si escribimos deprisa no nos servirá de nada. También tenemos una solución de terceros, que creo es la única: Visual Assist X. En C++/CLI no es perfecto, ya que no entiende dicho lenguaje, pero funciona mucho mejor dándonos las posibilidades más comunes y ofreciéndonos terminar por nosotros cuando ya sabe qué estás haciendo. Para C++ es una gozada, a veces casi se adelanta por completo a lo que queremos hacer.


El único inconveniente de esas dos utilidades es que son de pago, y aunque no son muy caras, hay que pagar por ellas.


Pero si hasta ahora la refactorización ha sido otra batalla perdida (recordemos que tanto C# como VB.NET traen un sistema muy bueno y potente), Microsoft al menos es consciente de ello y hace lo que está en su mano para facilitarnos las cosas. Ya que Visual Studio 2008 sigue sin traer refactorización para C++ y C++/CLI (que yo sepa), nos ofrece gratuitamente una alternativa de terceros.


Sí, has leído bien:


Refactorización gratis para C++ y C++/CLI.


La empresa se llama DevExpress, y aparte de tener una espuerta de componentes de todo tipo, también dispone de dicha utilidad, que nos ofrece gratuitamente.


En http://msdn2.microsoft.com/en-us/visualc/bb737896.aspx Microsoft nos explica cómo funciona. Y en http://www.devexpress.com/Products/NET/IDETools/RefactorCPP/ podemos bajarnos el programa.


Que ustedes refactoricen bien.


 

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

Tras la petición de Vicente en la entrada anterior, me he decidido a realizar unas pruebas de rendimiento sintético, utilizando el perfilador de código del propio Visual Studio 2005.


En su momento hice una serie de pruebas con el programa original, es decir, el que imprime una línea de texto por pantalla en la llamada a CallPrint(), pero descubrí una variación de tiempos asombrosa en diferentes ejecuciones del mismo programa. Tras mirar el código generado en la llamada, y pensar seriamente, llegué a la conclusión de que realizar un perfilado con una llamada a Console::WriteLine() no era una algo válido, ya que dicha llamada depende de un montón de cosas que no se pueden controlar, como rodajas de acceso a la memoria de vídeo, el propio sistema actualizando la pantalla y demás. Así que para las pruebas de rendimiento he decidido cambiar la línea citada por otra más sintética: una suma. Veamos el código de ambos proyectos (A la izquierda el código en C++/CLI y a la derecha en C#):











#include “stdafx.h”


using namespace System;


ref class Program
{
    static Int32 i=0;
public: 
    static void CallPrint(void) 
    { 
        i+=1; 
    } 
    static void DoWork(void) 
    { 
        for(int i=0;i<1000;i++) 
            CallPrint(); 
    }
};


int main(array<System::String ^> ^args)

    Program::DoWork();     
    Console::WriteLine(L”Hello World”); 
    return 0;


}


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


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


Ahora, nos vamos al menú Tools -> Performance Tools -> Performance Wizard y creamos un perfilado para ambos proyectos, del tipo Instrumentation. Y los ejecutamos, por supuesto ambos en Release.


Tras el análisis, podemos observar que en el programa C++/CLI el método CallPrint() no existe y ha sido integrado con el DoWork(), tal y como era de esperar. Veamos primero las imágenes combinadas:



Los resultados son lo esperado. La llamada a DoWork() en C++/CLI ha tardado 0.003489 milisegundos a realizarse, mientras que la suma de DoWork() y las 1000 a CallPrint() ha sido de 0.319240 milisegundos. Un cálculo rápido nos dice que la ejecución del bloque ha sido unas 91.5 veces más rápida. O en otras palabras: mientras que el programa en C# hace un bucle de 1000, el de C++/CLI podría hacerlo de 91 500.


Ahora bien, lo que no se ve en la imagen es el tiempo total de ejecución. El programa en C++/CLI ha tardado algo menos de 60 ms, mientras que el de C# apenas ha sido de 1,5 ms. Igual que antes, la diferencia es asombrosamente dispar, pero aquí sí que tiene una justificación lógica: un programa en C++/CLI necesita cargar, inicializar y sincronizar dos entornos de ejecución. Por un lado ha de cargar toda la parafernalia del .NET, y por otro llamar al startup nativo. Luego ha de mezclar ambos entornos mediante varias sincronizaciones y llamadas a métodos para unir los dos contextos. Pero luego, como se puede ver, no hay color en cuanto al rendimiento.

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

Vamos para atrás, como los cangrejos. Vamos a finalizar esta serie de entradas no planeadas con un rápido repaso a nuestro código en C++/CLI compilado bajo Visual Studio 2008.


En primer lugar, recordemos el código en cuestión:

// TestCpp1.cpp : main project file. 

#include “stdafx.h”

using namespace System;

ref class Program
{
public:
static void CallPrint(void)
{
Console::WriteLine(“CallPrint”);
}
static void DoWork(void)
{
for(int i=0;i<1000;i++)
CallPrint();
}
};

int main(array<System::String ^> ^args)
{
Program::DoWork();
Console::WriteLine(L“Hello World”);
return 0;
}


Luego nos vamos a una instalación limpia del Visual Studio 2008 y creamos un proyecto, cambiando la configuración a Release.


Compilemos y comprobemos:


image


Lo que digo, un paso hacia atrás.


¡Ahora el C++/CLI genera el mismo código mierdoso que el C#!


Ya sabemos por qué compila más rápido, o con otras palabras: compila igual de mal que el C#.


Pero si nos vamos a las opciones del proyecto, y cambiamos el nivel de optimización por Full Optimization, podemos ver que el compilador sigue siendo igual de bueno, excepto que le han cambiado el comportamiento por defecto:


image


Vemos que todo ha sido un mal sueño.


Ahora volvamos al código en C#, el que nos permite mirar cómo actua el jitter:

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)
{
for(uint i=0;i<UInt32.MaxValue;i++)
//while(true)
DoWork();
Console.WriteLine(“Hello World”);
}
}
}


Hagamos los pasos adecuados para obtener el código desensamblado y pasado por el optimizador, es decir, compilar y lanzar el proyecto sin el depurador y luego capturarlo con el Visual Studio:


image


No hace falta ni que miremos el código en ensamblador: lo hemos parado dentro del bucle que debería haber desaparecido.


Resumiendo: un pasito para atrás en cuanto a C++/CLI, y por otro lado, siempre lo mismo en cuanto al jitter.

Fucowski. Memorias de un ingeniero

No, no me he equivocado de blog. Esta entrada va aquí. A sones de Música en torno a la Capilla Real de José Herrando, y por un fallo en la carga del iLiad (es decir, se me olvidó ponerlo a cargar al mediodía y se está cargando ahora), me he leído el libro del asunto de esta entrada en el ordenador (con un monitor plano de 24 pulgadas, una fuente bien gorda, y despachurrado en el sillón no se lee tan mal).

El libro no es un libro normal, o al menos no muy común en estos tiempos. Pese a estar en ese formato, no son más que algunas entradas del blog homónimo, con cierta continuidad más o menos hilada. Quizás alguien haya leído algún pedazo por ahí.

No voy a comentar nada sobre él. Mejor que lo leas. Te lo recomiendo encarecidamente, y sólo tardarás un par de horas. Depende de qué lado estés, te gustará o no, pero seguro que no te deja indiferente.

A mi me ha encantado, por el desparpajo del lenguaje. Por los giros. Como este, que no tiene que ver con el tema:

Lib me pasó mi guitarra y dijo:
-Tócame algo.
Yo dejé la guitarra en el suelo, y me lo tomé al pie de la letra.

Por la forma de narrar. Por lo crudo. Por las metáforas. Por todo.

Me he partido de la risa, sobre todo con la escena de los huevos. Y no porque sea humorística, no.

Por otra cosa.

Hazme caso y échale un vistazo.

Léelo.

Prueba con Windows Live Writer…

Code snippet en C#:

   1: using System;
   2: using System.Collections.Generic;
   3: using System.Text;
   4:  
   5: namespace TestCS1
   6: {
   7:     class Program
   8:     {
   9:         static void CallPrint()
  10:         {
  11:             //Console.WriteLine("CallPrint");
  12:         }
  13:         static void DoWork()
  14:         {
  15:             for (int i = 0; i < 1000; i++)
  16:                 CallPrint();
  17:         }
  18:         static void Main(string[] args)
  19:         {
  20:             for(uint i=0;i<UInt32.MaxValue;i++)
  21:             //while(true)
  22:                 DoWork();
  23:             Console.WriteLine("Hello World");
  24:         }
  25:     }
  26: }

 

Pues no tiene mala pinta, no.

   1: // TestCpp1.cpp : main project file.
   2:  
   3: #include "stdafx.h"
   4:  
   5: using namespace System;
   6:  
   7: ref class Program
   8: {
   9: public:
  10:     static void CallPrint(void)
  11:     {
  12:         Console::WriteLine("CallPrint");
  13:     }
  14:     static void DoWork(void)
  15:     {
  16:         for(int i=0;i<1000;i++)
  17:             CallPrint();
  18:     }
  19: };
  20: int main(array<System::String ^> ^args)
  21: {
  22:     Program::DoWork();
  23:     Console::WriteLine(L"Hello World");
  24:     return 0;
  25: }

Mmmm… El de C# vale para C++…

Si no da problemas…

¡¡ME LO QUEDO!!

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

Por fin algo de coherencia en el tema. Vicente, en la segunda parte de esta serie, da en el clavo, pero el jitter sigue siendo bastante malo. ¿Por qué no hace el compilador de C# lo mismo que el de C++/CLI y deja más suelto el jitter. Sigo viéndolo un error bastante garrafal.

Vamos a ello.

Modifiquemos el programa y dejémoslo así:

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) 
        { 
            for(uint i=0;i<UInt32.MaxValue;i++) 
            //while(true) 
                DoWork(); 
            Console.WriteLine("Hello World"); 
        } 
    } 
} 

El cambio consiste en cambiar el bucle for dentro de main() por un bucle prácticamente infinito. Como nos comenta Vicente, si queremos ver al jitter en todo su esplendor, debemos o bien utilizar el depurador de línea de comando o bien anexar el Visual Studio a un proceso ya en ejecución (esto no lo sabía, yo pensaba que ejecutando bajo el depurador entraba todo en acción).

Compilamos el programa y lo lanzamos sin el depurador, es decir, pulsamos May-Alt-F5 o seleccionamos la opción correspondiente desde el menú Debug.

Una vez que hayamos lanzado el programa, nos vamos al menú Debug y seleccionamos “Attach to process”, buscamos TestCS3 en la lista de procesos y una vez hecho eso, pausamos el programa anexado.

Después, vemos la ventana de “Disassembly” (quizás tengamos que ejecutar unos cuantos pasos o “Step out” para volver a main():

Vemos algo menos de código que en la versión sin jittear (permítaseme la expresión), pero seguimos teniendo la llamada a DoWork() que, aunque ya no llama al método vacío, sigue repitiendo un bucle bastante absurdo y sin sentido:

Supongo que el jitter habrá tenido tiempo de optimizar el código en las miles de pasadas que el programa haya podido realizar entre que lo lanzamos y lo anexamos al depurador (de hecho ya no vemos esas extrañas llamadas dentro del código).

Resumiendo: que sigue siendo mejor optimizar en la compilación (aunque evidentemente, también es bueno tener un jitter) que en tiempo de ejecución. De hecho, el código realizado en C++/CLI sigue siendo sensiblemente más rápido sin la pasada del jitter que el de C# con varias miles de vueltas…

Pero ahora surge otro problema muy serio: si el código en Release no es el mismo que cuando la máquina se ejecuta sola, seguro que habrá muchas diferencias entre el código que se ejcuta dentro del Visual Studio y el que lo hace fuera de él, es decir, no estamos completamente seguros de que nuestro código vaya a funcionar bien en otras máquinas (y es ahora cuando yo me explico los problemas que he tenido y sigo teniendo con el código en producción, que a veces no funciona como debiera…).

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

Seguimos con el tema. En un comentario puesto en la segunda parte de esta entrega se comenta que conforme vaya el jitter dando vueltas sobre el mismo código, optimizará mejor el resultado. Es una de las cosas que también he leído por ahí, pero por desgracia no es cierto.

Vamos a ello.

Creemos una nueva solución con el nombre TestCS3 y piquemos el siguiente código:

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) 
        { 
            for(int i=0;i<1000;i++) 
                DoWork(); 
            Console.WriteLine("Hello World"); 
        } 
    } 
} 
 

Hemos añadido dentro de main() un bucle que hace 1000 llamadas al método DoWork(). Para comprobar si es cierto que el jitter realiza en varias llamadas mejores optimizaciones, vamos a poner un punto de interrupción condicional en el DoWork() que se dispare a las 500 pasadas. Para ello, una vez puesto en su lugar, mostramos la ventana de Breakpoints (Debug|Windows|Breakpoints ó Alt-F9), seleccionamos el punto en la lista y con el botón derecho del ratón elegimos “Hit Count”. Cambiamos los valores para que se pare a las 500 veces:

Y luego ejecutamos el programa hasta que el punto de interrupción salte:

Como antes, pulsamos Alt-F8 o abrimos la ventana de desensamblado y miramos. Ejecutamos varios pasos con Step Into o F11 y llegamos, de nuevo, al punto en el que se está llamando al método vacío:

Podemos incluso observar que el código ha cambiado, pero casi a peor, ya que ahora hay más códigos de saltos y sigue ese extraño call 0x79C05A9E, que podría ser una llamada al recolector o a cualquier otro lugar. Como es una aplicación de consola no creo que sea una llamada a ningún bucle principal.

De todo esto podemos ir extrayendo varias conclusiones, ninguna excesivamente buena…

De todos modos, existe todavía una comprobación mucho más fácil, y es ir depurando paso a paso sobre el propio código fuente. En una aplicación en C++ ó C++/CLI, vemos como hay bloques de código que se saltan, mientras que en C# no se salta ni uno. 

Y todavía no he hecho ninguna medición de rendimiento más o menos seria…

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.

C++/CLI y C#: Asombrosas diferencias en el optimizador de código

Lo que son las cosas. Esta pretendía ser una larga, sesuda y razonada entrada sobre las diferencias entre el optimizador de código del C# y el de C++/CLI, pero se ha quedado en apenas una nonada, ya que con el primer ejemplo tumbo de raíz el tema. Es decir, la cosa queda demostrada y bien demostrada.

Todo el asunto viene por una entrada en el grupo de C# en la que se me discutía que el lenguaje de C# puede equipararse a C++/CLI e incluso a C++. Comparados con éstos dos últimos, el C# no es más que una m*riconada de lenguaje, lenta en su ejecución y que carece de un buen número de características esenciales. Aparte está el hecho de que las partes que van mal dentro del Visual Studio (léase el diseñador visual de fichas, y los asistentes esos para bases de datos, están hechos por los chicos de C#, mientras que el resto del IDE sigue estando construido en C++). Y quizás sea ese el motivo por el cual C++/CLI se comporta tan mal dentro del IDE, ya que el propio lenguaje es una gozada, y ya mayoría de sus bugs no están en el propio compilador, sino en la parte del .NET (que por cierto, también está hecha en C#).

Bueno, volviendo al tema que nos ocupa.

Observemos el siguiente código (el de la izquierda está escrito en C++/CLI, el de la derecha en C#). La estructura es lo más parecida que puede ser en ambos casos.

// TestCpp1.cpp : main project file. 

#include "stdafx.h" 

using namespace System; 

ref class Program 
{ 
public: 
    static void CallPrint(void) 
    { 
        Console::WriteLine("CallPrint"); 
    } 
    static void DoWork(void) 
    { 
        for(int i=0;i<1000;i++) 
            CallPrint(); 
    } 
}; 

int main(array<System::String ^> ^args) 
{ 
    Program::DoWork(); 
    Console::WriteLine(L"Hello World"); 
    return 0; 
} 
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"); 
    } 
    } 
} 

Para los perezosos diremos que main() llama a un método estático de una clase cuyo nombre es DoWork(), que a su vez llama 1000 veces mediante un bucle a otro método estático que simplemente saca por pantalla una cadena.

La solución está disponible para quien quiera bajársela, así también se ahorra esfuerzo de picar código. La única observación a realizar es que hemos cambiado la construcción de Debug a Release para que entren ambos optimizadores. Tampoco se han tocado las opciones del proyecto (recordemos que el optimizado del C++/CLI por defecto es suave, y que todavía podemos subir dicho nivel, pero realmente no hace falta).

Ahora veamos el código generado por el programa realizado en C#:

Arriba del todo tenemos el listado de métodos que nos da el Ildasm. Aparte del constructor observamos los tres métodos citados. La siguiente imagen muestra el método main(), que en la línea IL_0000 hace una llamada al método DoWork(). Debajo tenemos dicho método. Vemos perfectamente el bucle, que ocupa 21 bytes y consta de 11 instrucciones. También podemos ver la llamada al método CallPrint(), que simplemente carga la cadena y llama a su vez a WriteLine().

Veamos ahora el programa en C++/CLI:

De la misma forma que antes, arriba mostramos parte del listado (recordemos que C++/CLI es bastante más complejo que C# y necesita y carga por defecto más cosas). Pero, cosa curiosa, sólo tenemos el método main(), ni está el CallPrint() ni el DoWork().

Estudiemos ese main(). La primera instrucción es una carga del valor hexadecimal 0x3e8 ¡que es 1000 en decimal! Luego vemos que carga la cadena a imprimir que debería estar en el método CallPrint(). El siguiente paso es hacer una llamada a WriteLine(). Y luego viene todo el código de mantenimiento del bucle. Al final del todo se imprime el Hello World.

Demoledor, ¿no? Nos hemos saltado dos llamadas a métodos, una de ellas dentro de un bucle de 1000 vueltas, es decir, nos hemos ahorrado mil llamadas al método CallPrint(), lo que supone un enorme ahorro de tiempo de proceso. También nos hemos saltado la llamada a DoWork() que evidentemente no hacía falta.

Pero sigamos mirando. Veamos el detalle de ambos bucles. En el de C#, se toma una variable que se va incrementando y al final se compara con el valor tope, 1000. En el de C++/CLI, pese a que el bucle original era de cero a mil, el funcionamiento es justo el contrario: se carga una variable con el valor de 1000 y se va decrementando hasta que llegue a cero, momento en el cual la instrucción btt.un.s no salta y la ejecución del bucle se detiene. ¿Qué es más caro, cargar una variable en un registro, comparar otro registro con ella y saltar, o simplemente saltar si un registro no es cero? En ensamblador de Intel, la parte de comparación conforme la hace C# son al menos dos instrucciones, en la forma en que lo hace el de C++/CLI, una solo, y posiblemente de un solo ciclo máquina.

La única cosa que queda por demostrar es que el optimizador interno del .NET fuera capaz de equiparar ambos códigos MSIL con el mismo ensamblador para Intel, pero dudo mucho que sea capaz de invertir el bucle de cero a mil, y menos aún saltarse todas esas llamadas a métodos que realmente no son necesarias.

Pero demos todavía un paso más. Retiremos el WriteLine() que hay dentro de CallPrint() y recompilemos ambos códigos (he creado dos nuevos proyectos llamados “TestCpp2” y “TestCS2”).

Primero el resultado en C#:

Y ahora el generado por el compilador de C++/CLI:

Impresionante, ¿no? Se ha comido todo el código inútil.