Yoda Code y el Optimizador de .NET

Dicen que a veces la vida te da sorpresas, sorpresas te da la vida. Y en este caso es completamente cierto. A raíz de la entrada anterior, un par de amigos comentaron en Twitter sobre la misma, haciendo uno de ellos hincapié en el Yoda Code que había puesto.

No soy yo muy partidario de Yoda, más que nada porque todos los lenguajes con los que programo suelen avisarte del uso de un solo igual en las comparaciones, aparte de que, como perteneciente a la vieja escuela, veo raro poner eso al revés. No es que sea gratuito, sino que viene de la forma en que aprendimos los lenguajes.

Digamos que para nosotros, en null==handler, null es un lvalue y debe ir a la derecha sí o sí. A ver, que es un poco tontería porque es una comparación, no una asignación, pero así lo veo al menos yo.

Evidentemente no hay nada en contra de utilizar al amigo Yoda (esperemos que Spielberg no se nos enfade y empiece a pedirnos derecho de marca). Simplemente yo lo veo extraño.

***

Pues nada, el RFOG pone las manos en la masa y decide investigar si hay alguna diferencia en el código generado por el compilador entre ponerlo de una forma u otra. Y ahí es donde se lleva una sorpresa que seguro vosotros tampoco os esperáis (no, no es la que pensáis. Paciencia, joven padawan).

Para ello se crea un proyecto de consola con el siguiente código fuente en C#:

        static void Main(string[] args)
        {
            var a = 33;
            if (40 == a)
                Console.WriteLine("Es diferente");
            else
                Console.WriteLine("Es igual");
        
            if (a == 40)
                Console.WriteLine("Es diferente");
            else
                Console.WriteLine("Es igual");
        }

Y en C++ (más bien C, pero a efectos de nuestro interés da igual):

            auto a = 33;
            if (40 == a)
                printf("Es diferente");
            else
                printf("Es igual");
        
            if (a == 40)
                printf("Es diferente");
            else
                printf("Es igual");
			return 0;

Y finalmente en C++/CLI, más que nada por mor de completitud:

int main(array<System::String ^> ^args)
{
	auto a = 33;
	if (40 == a)
		Console::WriteLine("Es diferente");
	else
		Console::WriteLine("Es igual");

	if (a == 40)
		Console::WriteLine("Es diferente");
	else
		Console::WriteLine("Es igual");
	return 0;
}

Si os fijáis, el código es idéntico excepto sus diferencias de sintaxis. Para ver el código resultante, se pone un punto de interrupción en la asignación de la variable a, se lanza el programa y luego nos vamos a Debug -> Windows -> Disassembly (o pulsamos Alt-8). Y miramos el código resultante en los tres casos. Primero en Debug y para C#:

clip_image002

Nítido como el agua: tanto monta, monta tanto, a==40 que 40==a. Perfecto. Vamos con C++/CLI:

clip_image004

Más o menos como en C#, de hecho el código es prácticamente el mismo. Finalmente C++ nativo:

clip_image006

Otra vez como lo esperado. Ninguna diferencia en la generación de código en la parte de la comparación. Bueno, no, algo peor porque el código generado en C++ es a todas luces peor que el de C#, cosa que me extraña. Esta es la primera sorpresa, pero todavía hay más.

De todos modos, si te preocupaba la generación de código tanto usando a Yoda como sin usarlo, ves que no hay diferencia.

Aun así, te recomiendo que sigas leyendo. Lo interesante viene ahora.

***

Pasemos a las versiones Release, que se suponen generan mejor código. La versión en C# genera exactamente el mismo código, por lo que realmente el compilador no está haciendo nada de nada. No hace falta que ponga ninguna captura: el código es idéntico.

En C++/CLI ya vemos algo diferente, y es que el breakpoint se ha movido él solo hacia abajo. Eso quiere decir que ha entrado el optimizador de código:

clip_image008

La versión en ensamblador es terroríficamente simple:

clip_image010

Como el compilador ya sabe el resultado, simplemente ejecuta el código adecuado y punto. El que el primer bloque de ensamblador sea tan largo se debe al prefijo de carga de main, y ese je 00000015 no es más que la comparación de a con 40, que el optimizador todavía realiza.

¿Por qué lo hace? Buena pregunta: otro hilo podría haber cambiado el valor de a, por lo tanto es necesaria la comparación. Ya sé que en este contexto no es válido, pero en otro sí que podría serlo.

Lo mismo nos pasa con C++ nativo, y ahora sí que vemos cómo el código está más que optimizado y apenas son unas cuantas línea de código ensamblador. La siguiente captura está dedicada a todos aquellos que piensan que no vale la pena desarrollar en C++:

clip_image012

***

Hasta ahora está todo meridianamente claro: No hay diferencia entre ir con Yoda o sin él y que el compilador de C++ es una caña generando código.

¿Estamos todos de acuerdo? Yo al menos lo estaba hasta hace bien poco. Ahora os dedico dos nuevas capturas de pantalla:

clip_image014

clip_image016

Y este es el bombazo: el código en .NET es casi idéntico al nativo. Cosa que hace unos años no pasaba. ¿Cómo he obtenido esas capturas? Os vais a Tools -> Options -> Debugging -> General -> Supress JIT optimization on module load y desmarcáis la casilla.

Bueno, el código es casi tan bueno como el nativo, pero fijaos que es mucho menor que las versiones anteriores. Esto me lleva a confirmar una sospecha: la gente de Microsoft está haciendo bien sus deberes, llevando al compilador de C# a cotas cada vez más altas.

Y es que con los años parece ser que han mejorado mucho:

Deja un comentario

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