see remarks para name cref comment c# .net cil ildasm ilasm

remarks - see cref c#



¿Por qué este método tan simple de C#produce un código CIL tan ilógico? (3)

He estado investigando en IL recientemente, y noté un comportamiento extraño en el compilador de C #. El siguiente método es una aplicación muy simple y verificable, se cerrará inmediatamente con el código de salida 1:

static int Main(string[] args) { return 1; }

Cuando compilo esto con Visual Studio Community 2015, se genera el siguiente código de IL (comentarios agregados):

.method private hidebysig static int32 Main(string[] args) cil managed { .entrypoint .maxstack 1 .locals init ([0] int32 V_0) // Local variable init IL_0000: nop // Do nothing IL_0001: ldc.i4.1 // Push ''1'' to stack IL_0002: stloc.0 // Pop stack to local variable 0 IL_0003: br.s IL_0005 // Jump to next instruction IL_0005: ldloc.0 // Load local variable 0 onto stack IL_0006: ret // Return }

Si tuviera que escribir este método a mano, aparentemente se podría lograr el mismo resultado con la siguiente IL:

.method static int32 Main() { .entrypoint ldc.i4.1 // Push ''1'' to stack ret // Return }

¿Hay razones subyacentes de las que no estoy al tanto de que esto haga que este sea el comportamiento esperado?

¿O simplemente es que el código objeto IL ensamblado se optimiza aún más en la línea, para que el compilador de C # no tenga que preocuparse por la optimización?


La respuesta de Jon es, por supuesto, correcta; Esta respuesta es para dar seguimiento a este comentario:

@EricLippert el local tiene perfecto sentido, pero ¿hay alguna razón para esa instrucción de br.s o está ahí por conveniencia en el código del emisor? Supongo que si el compilador quisiera insertar un marcador de posición de punto de interrupción allí, podría simplemente emitir un nop ...

La razón de la rama aparentemente sin sentido se vuelve más sensata si nos fijamos en un fragmento de programa más complicado:

public int M(bool b) { if (b) return 1; else return 2; }

La IL no optimizada es

IL_0000: nop IL_0001: ldarg.1 IL_0002: stloc.0 IL_0003: ldloc.0 IL_0004: brfalse.s IL_000a IL_0006: ldc.i4.1 IL_0007: stloc.1 IL_0008: br.s IL_000e IL_000a: ldc.i4.2 IL_000b: stloc.1 IL_000c: br.s IL_000e IL_000e: ldloc.1 IL_000f: ret

Tenga en cuenta que hay dos instrucciones de return pero solo una instrucción ret . En IL no optimizada, el patrón para el código de una declaración de retorno simple es:

  • Rellena el valor que vas a volver a una ranura de pila
  • Rama / dejar hasta el final del método.
  • al final del método, lea el valor de la ranura y devuelva

Es decir, el código no optimizado utiliza un formulario de punto de retorno único.

Tanto en este caso como en el caso simple mostrado por el póster original, ese patrón hace que se genere una situación de "ramificación a la siguiente". El optimizador de "eliminar cualquier rama a la siguiente" no se ejecuta cuando se genera un código no optimizado, por lo que permanece.


La salida que has mostrado es para una compilación de depuración. Con una versión de compilación (o básicamente con las optimizaciones activadas), el compilador de C # genera la misma IL que habrías escrito a mano.

Sospecho firmemente que esto es todo para hacer que el trabajo del depurador sea más sencillo, básicamente, para que sea más fácil romperlo y también ver el valor de retorno antes de que se devuelva.

Moraleja: cuando desee ejecutar un código optimizado, asegúrese de que no está pidiendo al compilador que genere un código destinado a la depuración :)


Lo que estoy a punto de escribir no es realmente específico de .NET sino general, y no conozco las optimizaciones que .NET reconoce y utiliza al generar CIL. El árbol de sintaxis (y por él mismo el analizador de gramática) reconoce la declaración de retorno con los siguientes lexemas:

returnStatement ::= RETURN expr ;

donde returnStatement y expr no son terminales y RETURN es el terminal (token de retorno ), de modo que cuando se visita el nodo en busca de la constante 1 el analizador se comporta como si fuera parte de una expresión. Para ilustrar mejor lo que quiero decir, el código para:

return 1 + 1;

se vería algo así para una máquina (virtual) usando la pila de expresiones:

push const_1 // Pushes numerical value ''1'' to expression stack push const_1 // Pushes numerical value ''1'' to expression stack add // result = pop() + pop(); push(result) return // pops the value on the top of the stack and returns it as the function result exit