c# c++ .net performance optimization

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.