Rendimiento de C#frente a C++: ¿por qué.NET no realiza las optimizaciones más básicas(como la eliminación de código muerto)?
performance optimization (1)
Estoy dudando seriamente de si los compiladores C # o .NET JIT realizan optimizaciones útiles, y mucho menos si son realmente competitivos con los más básicos en compiladores C ++.
Considere este programa extremadamente simple, que convenientemente hice para que sea válido tanto en C ++ como en C #:
#if __cplusplus
#else
static class Program
{
#endif
static void Rem()
{
for (int i = 0; i < 1 << 30; i++) ;
}
#if __cplusplus
int main()
#else
static void Main()
#endif
{
for (int i = 0; i < 1 << 30; i++)
Rem();
}
#if __cplusplus
#else
}
#endif
Cuando lo compilo y lo ejecuto en la versión más reciente de C # (VS 2013) en modo de lanzamiento, no termina en un período de tiempo razonable.
Edición : Aquí hay otro ejemplo:
static class Program
{
private static void Test2() { }
private static void Test1()
{
#if TEST
Test2(); Test2(); Test2(); Test2(); Test2(); Test2(); Test2(); Test2();
Test2(); Test2(); Test2(); Test2(); Test2(); Test2(); Test2(); Test2();
Test2(); Test2(); Test2(); Test2(); Test2(); Test2(); Test2(); Test2();
Test2(); Test2(); Test2(); Test2(); Test2(); Test2(); Test2(); Test2();
Test2(); Test2(); Test2(); Test2(); Test2(); Test2(); Test2(); Test2();
Test2(); Test2(); Test2(); Test2(); Test2(); Test2(); Test2(); Test2();
Test2(); Test2(); Test2(); Test2(); Test2(); Test2(); Test2(); Test2();
#else
Test2();
#endif
}
static void Main()
{
for (int i = 0; i < 0x7FFFFFFF; i++)
Test1();
}
}
Cuando ejecuto este, se demora mucho más si se define la TEST
, aunque todo sea un no-op y la Test2
debe estar en línea.
Sin embargo, incluso los compiladores de C ++ más antiguos a los que puedo acceder, optimizo todo, haciendo que los programas regresen de inmediato.
¿Qué impide que el optimizador JIT .NET pueda realizar optimizaciones tan simples? ¿Por qué?
El .NET JIT es un compilador pobre, esto es cierto. Afortunadamente, un nuevo JIT ( RyuJIT ) y un NGEN que parece estar basado en el compilador VC están en las obras (creo que esto es lo que utiliza el compilador de la nube de Windows Phone ).
Aunque es un compilador muy simple, hace en línea pequeñas funciones y elimina hasta cierto punto los bucles libres de efectos secundarios. No es bueno en todo esto pero sucede.
Antes de entrar en los resultados detallados, tenga en cuenta que los JIT x86 y x64 son diferentes bases de código, tienen un rendimiento diferente y tienen diferentes errores.
Prueba 1:Ejecutó el programa en modo Release en modo de 32 bits. Puedo reproducir tus hallazgos en .NET 4.5 con modo de 32 bits. Sí, esto es embarazoso.
Sin embargo, en el modo de 64 bits, Rem
en el primer ejemplo está en línea y se elimina el más interno de los dos bucles anidados:
He marcado las tres instrucciones de bucle. El bucle exterior todavía está allí. No creo que eso importe en la práctica porque rara vez tienes dos bucles muertos anidados.
Tenga en cuenta que el bucle se desenrolló 4 veces, luego las iteraciones desenrrolladas se colapsaron en una sola iteración (el desenrollamiento produjo i += 1; i+= 1; i+= 1; i+= 1;
y eso se colapsó en i += 4;
). Por supuesto, todo el bucle podría optimizarse, pero el JIT realizó las cosas que más importan en la práctica: desenrollar los bucles y simplificar el código.
También agregué lo siguiente a Main
para facilitar la depuración:
Console.WriteLine(IntPtr.Size); //verify bitness
Debugger.Break(); //attach debugger
Prueba 2:
No puedo reproducir completamente sus hallazgos en modo de 32 o 64 bits. En todos los casos, Test2
está Test2
en Test1
por lo que es una función muy simple:
Main
llama a Test1
en un bucle porque Test1
era demasiado grande para estar en línea (porque el tamaño no simplificado cuenta porque los métodos se procesan de forma aislada).
Cuando solo tiene una llamada Test2
en Test1
, ambas funciones son lo suficientemente pequeñas como para estar en línea. Esto permite al JIT for Main
descubrir que no se está haciendo nada en absoluto en ese código.
Respuesta final: Espero poder arrojar algo de luz sobre lo que está sucediendo. En el proceso descubrí algunas optimizaciones importantes. El JIT no es muy completo y completo. Si las mismas optimizaciones se realizaran en un segundo pase de identificación, se podrían simplificar muchas más aquí. Pero la mayoría de los programas solo necesitan pasar por todos los simplificadores. Estoy de acuerdo con la elección que hizo el equipo JIT aquí.
Entonces, ¿por qué es tan malo el JIT? Una parte es que debe ser rápido porque JITing es sensible a la latencia. Otra parte es que es solo un JIT primitivo y necesita más inversión.