c# - ¿Puedo forzar al compilador a optimizar un método específico?
optimization compiler-construction (3)
¿Hay alguna forma de que pueda generar el código de método original de forma dinámica utilizando Microsoft.CSharp.CSharpCodeProvider?
Si controla la compilación del método, puede establecer las opciones cuando llame al compilador utilizando CompilerOptions .
¿Hay algún atributo que pueda usar para decirle al compilador que siempre se debe optimizar un método, incluso si el interruptor del compilador global /o+
no está configurado?
La razón por la que pregunto es porque estoy jugando con la idea de crear dinámicamente un método basado en el código IL de un método existente; la manipulación que quiero hacer es razonablemente fácil cuando el código está optimizado, pero se vuelve significativamente más difícil en el código no optimizado, debido a las instrucciones adicionales generadas por el compilador.
EDITAR: más detalles sobre las no optimizaciones que me molestan ...
Consideremos la siguiente implementación de la función factorial:
static long FactorialRec(int n, long acc)
{
if (n == 0)
return acc;
return FactorialRec(n - 1, acc * n);
}
(Nota: sé que hay mejores formas de calcular el factorial, esto es solo un ejemplo)
El IL generado con las optimizaciones habilitadas es bastante sencillo:
IL_0000: ldarg.0
IL_0001: brtrue.s IL_0005
IL_0003: ldarg.1
IL_0004: ret
IL_0005: ldarg.0
IL_0006: ldc.i4.1
IL_0007: sub
IL_0008: ldarg.1
IL_0009: ldarg.0
IL_000A: conv.i8
IL_000B: mul
IL_000C: call UserQuery.FactorialRec
IL_0011: ret
Pero la versión no optimizada es bastante diferente
IL_0000: nop
IL_0001: ldarg.0
IL_0002: ldc.i4.0
IL_0003: ceq
IL_0005: ldc.i4.0
IL_0006: ceq
IL_0008: stloc.1
IL_0009: ldloc.1
IL_000A: brtrue.s IL_0010
IL_000C: ldarg.1
IL_000D: stloc.0
IL_000E: br.s IL_001F
IL_0010: ldarg.0
IL_0011: ldc.i4.1
IL_0012: sub
IL_0013: ldarg.1
IL_0014: ldarg.0
IL_0015: conv.i8
IL_0016: mul
IL_0017: call UserQuery.FactorialRec
IL_001C: stloc.0
IL_001D: br.s IL_001F
IL_001F: ldloc.0
IL_0020: ret
Está diseñado para tener solo un punto de salida, al final. El valor a devolver se almacena en una variable local.
¿Por qué es esto un problema? Quiero generar dinámicamente un método que incluya la optimización de la cola de llamadas. El método optimizado se puede modificar fácilmente agregando la tail.
prefijo antes de la llamada recursiva, ya que no hay nada después de la llamada excepto ret
. Pero con la versión no optimizada, no estoy tan seguro ... el resultado de la llamada recursiva se almacena en una variable local, luego hay una rama inútil que simplemente salta a la siguiente instrucción, la variable local se carga y se devuelve. Así que no tengo una manera fácil de verificar que la llamada recursiva realmente sea la última instrucción, por lo que no puedo estar seguro de que se pueda aplicar la optimización de la llamada final.
Nunca puede estar seguro de obtener la optimización de la cola, siempre que use C #.
En particular, incluso con call ... ret
JITter no garantiza una llamada final. Así que el código IMO C # que depende de la optimización de la cola de llamadas (para evitar el desbordamiento de la pila) simplemente se rompe. En C # tail, la optimización de llamadas es puramente una optimización del rendimiento.
Use un idioma en el que las llamadas de cola se emitan de manera confiable o reescriba su método para que no necesite llamadas de cola.
Si el método que utilizará como plantilla para el método dinámico es relativamente simple y sin dependencias con otros métodos. Luego, simplemente colóquelo en su propio ensamblaje y active la optimización solo para ese ensamblaje.
En cuanto al problema original, dado que MSIL es un lenguaje basado en pila. Y las especificaciones garantizan el estado de pila en la declaración de ret
. Puede estar 100% seguro de que puede agregar un prefijo de cola sin problema. Sin embargo, tampoco es probable que agregue ningún beneficio, ya que realmente no he visto que el JIT use el prefijo de la cola para optimizar realmente el código jitted finalmente.