¿Hay una manera de ver el código nativo producido por theJITter para C # / CIL dado?

En un comentario sobre esta respuesta (que sugiere el uso de operadores de cambio de bits en la multiplicación / división de enteros, para el rendimiento), pregunté si esto sería realmente más rápido. En el fondo de mi mente hay una idea de que, en algún nivel, algo será lo suficientemente inteligente como para descubrir que >> 1 y / 2 son la misma operación. Sin embargo, ahora me pregunto si esto es verdad, y si lo es, a qué nivel ocurre.

Un progtwig de prueba produce el siguiente CIL comparativo (con optimize ) para dos métodos que respectivamente dividen y cambian su argumento:

  IL_0000: ldarg.0 IL_0001: ldc.i4.2 IL_0002: div IL_0003: ret } // end of method Program::Divider 

versus

  IL_0000: ldarg.0 IL_0001: ldc.i4.1 IL_0002: shr IL_0003: ret } // end of method Program::Shifter 

Así que el comstackdor de C # está emitiendo instrucciones div o shr , sin ser inteligente. Ahora me gustaría ver el ensamblador x86 real que produce el JITter, pero no tengo idea de cómo hacerlo. ¿Es incluso posible?

editar para agregar

Recomendaciones

Gracias por las respuestas, he aceptado la de nobugz porque contenía la información clave sobre esa opción del depurador. Lo que eventualmente funcionó para mí es:

  • Cambiar a la configuración de liberación
  • En Tools | Options | Debugger Tools | Options | Debugger Tools | Options | Debugger , desactive ‘Suprimir optimización JIT en la carga del módulo’ (es decir, queremos permitir la optimización JIT)
  • En el mismo lugar, desactive “Habilitar solo mi código” (es decir, queremos depurar todo el código)
  • Ponga una statement Debugger.Break() algún lugar
  • Construir el assembly
  • Ejecute el archivo .exe y, cuando se rompa, realice una depuración utilizando la instancia de VS existente
  • Ahora la ventana de Desassembly muestra el x86 real que se va a ejecutar

Los resultados fueron esclarecedores, por decir lo menos: ¡resulta que el JITter puede hacer aritmética! Aquí hay ejemplos editados de la ventana de Desassembly. Los diversos métodos de -Shifter dividen por potencias de dos usando >> ; los diversos métodos -Divider dividen por enteros usando /

  Console.WriteLine(string.Format(" {0} shift-divided by 2: {1} divide-divided by 2: {2}", 60, TwoShifter(60), TwoDivider(60))); 00000026 mov dword ptr [edx+4],3Ch ... 0000003b mov dword ptr [edx+4],1Eh ... 00000057 mov dword ptr [esi+4],1Eh 

Ambos métodos de dividir estáticamente por 2 no solo se han incorporado, sino que los cálculos reales se han realizado por el JITter

 Console.WriteLine(string.Format(" {0} divide-divided by 3: {1}", 60, ThreeDivider(60))); 00000085 mov dword ptr [esi+4],3Ch ... 000000a0 mov dword ptr [esi+4],14h 

Lo mismo con dividir estáticamente por 3.

 Console.WriteLine(string.Format(" {0} shift-divided by 4: {1} divide-divided by 4 {2}", 60, FourShifter(60), FourDivider(60))); 000000ce mov dword ptr [esi+4],3Ch ... 000000e3 mov dword ptr [edx+4],0Fh ... 000000ff mov dword ptr [esi+4],0Fh 

Y estáticamente dividir por 4.

El mejor:

 Console.WriteLine(string.Format(" {0} n-divided by 2: {1} n-divided by 3: {2} n-divided by 4: {3}", 60, Divider(60, 2), Divider(60, 3), Divider(60, 4))); 0000013e mov dword ptr [esi+4],3Ch ... 0000015b mov dword ptr [esi+4],1Eh ... 0000017b mov dword ptr [esi+4],14h ... 0000019b mov dword ptr [edi+4],0Fh 

Está en línea y luego calcula todas estas divisiones estáticas!

Pero ¿y si el resultado no es estático? Agregué al código para leer un entero desde la consola. Esto es lo que produce para las divisiones en eso:

 Console.WriteLine(string.Format(" {0} shift-divided by 2: {1} divide-divided by 2: {2}", i, TwoShifter(i), TwoDivider(i))); 00000211 sar eax,1 ... 00000230 sar eax,1 

Entonces, a pesar de que el CIL es diferente, el JITter sabe que dividir por 2 es cambiar a la derecha por 1.

 Console.WriteLine(string.Format(" {0} divide-divided by 3: {1}", i, ThreeDivider(i))); 

00000283 idiv eax, ecx

Y sabe que hay que dividir para dividir por 3.

 Console.WriteLine(string.Format(" {0} shift-divided by 4: {1} divide-divided by 4 {2}", i, FourShifter(i), FourDivider(i))); 000002c5 sar eax,2 ... 000002ec sar eax,2 

Y sabe que dividir por 4 es desplazar a la derecha por 2.

Finalmente (lo mejor otra vez!)

 Console.WriteLine(string.Format(" {0} n-divided by 2: {1} n-divided by 3: {2} n-divided by 4: {3}", i, Divider(i, 2), Divider(i, 3), Divider(i, 4))); 00000345 sar eax,1 ... 00000370 idiv eax,ecx ... 00000395 sar esi,2 

Ha incorporado el método y ha ideado la mejor manera de hacer las cosas, basándose en los argumentos disponibles estáticamente. Bonito.


Así que sí, en algún lugar de la stack entre C # y x86, algo es lo suficientemente inteligente como para descubrir que >> 1 y / 2 son lo mismo. Y todo esto me ha dado aún más peso a mi opinión de que sumr el comstackdor de C #, el JITter y el CLR hacen mucho más ingenioso que cualquier pequeño truco que podamos probar como humildes progtwigdores de aplicaciones 🙂

No obtendrá resultados significativos hasta que configure el depurador. Herramientas + Opciones, Depuración, General, desactive “Suprimir optimización JIT en la carga del módulo”. Cambia a la configuración del modo Release. Un fragmento de muestra:

 static void Main(string[] args) { int value = 4; int result = divideby2(value); } 

Lo estás haciendo bien si el desassembly se ve así:

 00000000 ret 

Tendrá que engañar al optimizador JIT para forzar la expresión a evaluar. Usar Console.WriteLine (variable) puede ayudar. Entonces deberías ver algo como esto:

 0000000a mov edx,2 0000000f mov eax,dword ptr [ecx] 00000011 call dword ptr [eax+000000BCh] 

Sí, evaluó el resultado en tiempo de comstackción. Funciona bastante bien, ¿no es así?

Sí. Visual Studio tiene un desensamblador incorporado para hacer eso. Sin embargo, debe agregar el comando a la barra de menú. Vaya a Extras / Personalizar / Comandos (aunque no sé si realmente se los llama así en la versión en inglés) y agregue el comando Desensamblar, que es el apartado Depuración, en algún lugar de la barra de menús.

Luego, establezca un punto de interrupción en su progtwig y cuando se rompa, haga clic en este comando de Desassembly. VS le mostrará el código de la máquina desmontado.

Ejemplo de salida para un método divisor:

 public static int Divider(int intArg) { 00000000 push ebp 00000001 mov ebp,esp 00000003 push edi 00000004 push esi 00000005 push ebx 00000006 sub esp,34h 00000009 mov esi,ecx 0000000b lea edi,[ebp-38h] 0000000e mov ecx,0Bh 00000013 xor eax,eax 00000015 rep stos dword ptr es:[edi] 00000017 mov ecx,esi 00000019 xor eax,eax 0000001b mov dword ptr [ebp-1Ch],eax 0000001e mov dword ptr [ebp-3Ch],ecx 00000021 cmp dword ptr ds:[00469240h],0 00000028 je 0000002F 0000002a call 6BA09D91 0000002f xor edx,edx 00000031 mov dword ptr [ebp-40h],edx 00000034 nop return intArg / 2; 00000035 mov eax,dword ptr [ebp-3Ch] 00000038 sar eax,1 0000003a jns 0000003F 0000003c adc eax,0 0000003f mov dword ptr [ebp-40h],eax 00000042 nop 00000043 jmp 00000045 } 

Mientras realiza la depuración (y solo mientras realiza la depuración), haga clic en Depurar – Windows – Desensamblaje o presione el atajo de teclado correspondiente Ctrl + Alt + D.