valid - ¿Hay una manera de ver el código nativo producido por theJITter para C#/ CIL dado?
remarks/> (3)
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.
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 programa 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 compilador 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 declaración
Debugger.Break()
algún lugar - Construir el montaje
- Ejecute el archivo .exe y, cuando se rompa, realice una depuración utilizando la instancia de VS existente
- Ahora la ventana de Desmontaje 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 desmontaje. Los diversos métodos de cambio se 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 pila 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 sumar el compilador de C #, el JITter y el CLR hacen que sea mucho más inteligente que cualquier pequeño truco que podamos probar como humildes programadores 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 desmontaje se ve así:
00000000 ret
Tendrás 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 compilación. Funciona bastante bien, ¿no es así?
Sí. Visual Studio tiene un desensamblador incorporado para hacer eso. Sin embargo, debes agregar el comando a la barra de menú. Vaya a Extras / Personalizar / Comandos (aunque no sé si en realidad se llaman así en la versión en inglés) y agregue el comando Desensamblar, que es un poco de Depuración, en algún lugar de su barra de menús.
Luego, establezca un punto de interrupción en su programa y cuando se rompa, haga clic en este comando de Desmontaje. 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
}