添加一个int变量时生成不同的IL

时间:2015-12-04 15:12:55

标签: c# il csc ildasm

我在c#中有这个程序:

using System;

class Program
{
    public static void Main()
    {
    int i = 4;
    double d = 12.34;
    double PI = Math.PI;
    string name = "Ehsan";


    }
}

当我编译它时,以下是编译器为Main生成的IL:

.method public hidebysig static void  Main() cil managed
{
  .entrypoint
  // Code size       30 (0x1e)
  .maxstack  1
  .locals init (int32 V_0,
           float64 V_1,
           float64 V_2,
           string V_3)
  IL_0000:  nop
  IL_0001:  ldc.i4.4
  IL_0002:  stloc.0
  IL_0003:  ldc.r8     12.34
  IL_000c:  stloc.1
  IL_000d:  ldc.r8     3.1415926535897931
  IL_0016:  stloc.2
  IL_0017:  ldstr      "Ehsan"
  IL_001c:  stloc.3
  IL_001d:  ret
} // end of method Program::Main

这很好,我明白了,现在如果我添加另一个整数变量然后生成不同的东西,这里是修改后的c#代码:

using System;

class Program
{
    public static void Main()
    {
    int unassigned;
    int i = 4;
    unassigned = i;
    double d = 12.34;
        double PI = Math.PI;
    string name = "Ehsan";


    }
}

以下是针对上述c#代码生成的IL:

.method public hidebysig static void  Main() cil managed
{
  .entrypoint
  // Code size       33 (0x21)
  .maxstack  1
  .locals init (int32 V_0,
           int32 V_1,
           float64 V_2,
           float64 V_3,
           string V_4)
  IL_0000:  nop
  IL_0001:  ldc.i4.4
  IL_0002:  stloc.1
  IL_0003:  ldloc.1
  IL_0004:  stloc.0
  IL_0005:  ldc.r8     12.34
  IL_000e:  stloc.2
  IL_000f:  ldc.r8     3.1415926535897931
  IL_0018:  stloc.3
  IL_0019:  ldstr      "Ehsan"
  IL_001e:  stloc.s    V_4  // what is happening here in this case
  IL_0020:  ret
} // end of method Program::Main

如果您现在注意stloc.s语句是使用本地V_4生成的,但我不清楚这一点,我也不知道这些本地人的目的是什么,我的意思是这些:

 .locals init (int32 V_0,
               float64 V_1,
               float64 V_2,
               string V_3)

2 个答案:

答案 0 :(得分:5)

有些事情要注意。

首先,这可能是一个调试版本,或者至少在编译中关闭了某些优化。我期望在这里看到的是:

.method public hidebysig static void Main () cil managed 
{
  .entrypoint

  IL_0000: ret
}

也就是说,由于那些本地人没有被使用,我希望编译器完全跳过它们。它不会在调试版本中获得,但这是一个很好的例子,说明C#所说的内容和IL所说的内容之间会有很大差异。

接下来要注意的是如何构建IL方法。您有一组本地值,这些值由.locals块定义,具有各种类型。这些通常与C#的内容非常接近,但通常会做出捷径和重新安排。

最后,我们有一组指令,这些指令都对那些本地人,任何参数和它可以推送的堆栈起作用,从中可以弹出,以及各种指令将在哪些方面进行交互。

接下来要注意的是,你在这里看到的IL是一种用于字节码的汇编:这里的每条指令都有一对一映射到一个或两个字节,每个值也消耗一定数量的字节数。例如,stloc V_4(实际上并未出现在您的示例中,但我们会这样做)会映射到0xFE 0x0E 0x04 0x00,其中0xFE 0x0Estloc的编码0x04 0x00 4的{​​{1}}是当地的索引。它意味着"弹出堆栈顶部的值,并将其存储在第5个(索引4)本地"。

现在,这里有一些缩写。其中之一是.s"短"几个指令的形式(等效_S值的名称中的System.Reflection.Emit.OpCode)。这些是采用单字节值(有符号或无符号取决于指令)的其他指令的变体,其中另一种形式采用两个或四个字节的值,通常是索引或跳转的相对距离。因此,stloc V_4代替stloc.s V_4,而0x13 0x4只有stloc V_0,因此更小。

然后有一些变体在指令中包含特定值。因此,我们可以使用stloc.s V_0而不是stloc.00x0A来代替stloc.s

当您考虑到一次只使用少数本地人时,这很有意义,因此使用stloc.0或(更好)stloc.1之类的1}},stloc.252等)可以带来很少的节省。

但只有这么多。如果我们有例如stloc.253stloc等,则会有很多这样的指令,并且每条指令所需的字节数必须更多,而且总体而言亏损。与本地相关的超级简短形式(ldlocldarg)和与参数相关的(3)只能达到starg。 (有starg.sstarg.0但没有ldc.i4等,因为存储参数相对较少。 ldc.i4.s / ldc.i4.0(将持续的32位带符号值推送到堆栈上)具有从ldc.i4.8lcd.i4.m1以及-1的超短版本V_4

值得注意的是,name根本不存在于您的代码中。无论你检查了什么,IL都不知道你使用了变量名V_4所以它只使用了name。 (你在使用什么,BTW?我大部分都使用ILSpy,如果你调试与文件相关的信息,它会相应地称它为.method public hidebysig static void Main() cil managed { .entrypoint .maxstack 1 .locals init (int32 unassigned, int32 i, float64 d, float64 PI, string name) nop // Do Nothing (helps debugger to have some of these around). ldc.i4 4 // Push number 4 on stack stloc i // Pop value from stack, put in i (i = 4) ldloc i // Push value in i on stack stloc unassigned // Pop value from stack, put in unassigned (unassigned = i) ldc.r8 12.34 // Push the 64-bit floating value 12.34 onto the stack stloc d // Push the value on stack in d (d = 12.34) ldc.r8 3.1415926535897931 // Push the 64-bit floating value 3.1415926535897931 onto the stack. stloc PI // Pop the value from stack, put in PI (PI = 3.1415… which is the constant Math.PI) ldstr "Ehsan" // Push the string "Ehsan" on stack stloc name // Pop the value from stack, put in name ret // return. }

因此,要生成具有更多可比名称的方法的注释非短路版本,我们可以编写以下CIL:

stloc

这会像你的代码那样表现,但要大一些。因此,我们尽可能将stloc.0替换为stloc.3 ... stloc.sstloc.s我们无法使用ldc.i4 4,但仍然可以使用ldc.i4.4.method public hidebysig static void Main() cil managed { .entrypoint .maxstack 1 .locals init (int32 unassigned, int32 i, float64 d, float64 PI, string name) nop // Do Nothing (helps debugger to have some of these around). ldc.i4.4 // Push number 4 on stack stloc.1 // Pop value from stack, put in i (i = 4) ldloc.1 // Push value in i on stack stloc.0 // Pop value from stack, put in unassigned (unassigned = i) ldc.r8 12.34 // Push the 64-bit floating value 12.34 onto the stack stloc.2 // Push the value on stack in d (d = 12.34) ldc.r8 3.1415926535897931 // Push the 64-bit floating value 3.1415926535897931 onto the stack. stloc.3 // Pop the value from stack, put in PI (PI = 3.1415… which is the constant Math.PI) ldstr "Ehsan" // Push the string "Ehsan" on stack stloc.s name // Pop the value from stack, put in name ret // return. } public static void Maybe(int a, int b) { if (a > b) Console.WriteLine("Greater"); Console.WriteLine("Done"); } .method public hidebysig static void Maybe ( int32 a, int32 b ) cil managed { .maxstack 2 .locals init ( [0] bool CS$4$0000 ) IL_0000: nop IL_0001: ldarg.0 IL_0002: ldarg.1 IL_0003: cgt IL_0005: ldc.i4.0 IL_0006: ceq IL_0008: stloc.0 IL_0009: ldloc.0 IL_000a: brtrue.s IL_0017 IL_000c: ldstr "Greater" IL_0011: call void [mscorlib]System.Console::WriteLine(string) IL_0016: nop IL_0017: ldstr "Done" IL_001c: call void [mscorlib]System.Console::WriteLine(string) IL_0021: nop IL_0022: ret } ,我们会使用更短的字节码执行相同的操作:

IL_0017

现在我们与您的反汇编完全相同的代码,除了我们有更好的名字。请记住,名称不会出现在字节代码中,因此反汇编程序无法做到尽可能好。

评论中的问题应该是另一个问题,但它提供了一个添加重要内容的机会,我只是在上面简要提到过。我们来考虑一下:

.method public hidebysig static 
  void Maybe (
    int32 a,
    int32 b
  ) cil managed 
{
  .maxstack 2
  .locals init (
    [0] bool CS$4$0000
  )

  nop
  ldarg.0
  ldarg.1
  cgt
  ldc.i4.0
  ceq
  stloc.0
  ldloc.0
  brtrue.s IL_0017

  ldstr "Greater"
  call void [mscorlib]System.Console::WriteLine(string)
  nop

  IL_0017: ldstr "Done"
  call void [mscorlib]System.Console::WriteLine(string)
  nop
  ret
}

在调试中编译,最终得到类似的内容:

.method public hidebysig static 
  void Maybe (
    int32 a,
    int32 b
  ) cil managed 
{
  .maxstack 2
  .locals init (
    [0] bool CS$4$0000
  )

  nop                   // Do nothing
  ldarg.0               // Load first argument (index 0) onto stack.
  ldarg.1               // Load second argument (index 1) onto stack.
  cgt                   // Pop two values from stack, push 1 (true) if the first is greater
                        // than the second, 0 (false) otherwise.
  ldc.i4.0              // Push 0 onto stack.
  ceq                   // Pop two values from stack, push 1 (true) if the two are equal,
                        // 0 (false) otherwise.
  stloc.0               // Pop value from stack, store in first local (index 0)
  ldloc.0               // Load first local onto stack.
  brtrue.s IL_0017      // Pop value from stack. If it's non-zero (true) jump to IL_0017

  ldstr "Greater"       // Load string "Greater" onto stack.

                        // Call Console.WriteLine(string)
  call void [mscorlib]System.Console::WriteLine(string)
  nop                   // Do nothing

  IL_0017: ldstr "Done" // Load string "Done" onto stack.
                        // Call Console.WriteLine(string)
  call void [mscorlib]System.Console::WriteLine(string)
  nop                   // Do nothing
  ret                   // return
}

现在需要注意的是,所有标签如public static void Maybe(int a, int b) { bool shouldJump = (a > b) == false; if (shouldJump) goto IL_0017; Console.WriteLine("Greater"); IL_0017: Console.WriteLine("Done"); } 等都会根据指令的索引添加到每一行。这使得反汇编程序的生活更加轻松,但除非跳转到标签,否则不是必需的。让我们删除所有未跳转到的标签:

goto

现在,让我们考虑每一行的作用:

for

让我们以一种非常直接的方式将它写回C#:

while

尝试一下,你会发现它做同样的事情。 if的使用是因为CIL确实没有像elseshouldJump这样的内容,甚至我们可以在a > ba > b后添加的内容,它只是跳跃和条件跳跃。

但是为什么要把这个值(我在C#重写中称为a <= b而不仅仅是对它进行操作而烦恼?

如果您正在调试,只是为了更容易检查每个点上发生了什么。特别是,为了使调试器能够在.method public hidebysig static void Maybe ( int32 a, int32 b ) cil managed { ldarg.0 // Load first argument onto stack ldarg.1 // Load second argument onto stack ble.s IL_000e // Pop two values from stack. If the first is // less than or equal to the second, goto IL_000e: ldstr "Greater" // Load string "Greater" onto stack. // Call Console.WriteLine(string) call void [mscorlib]System.Console::WriteLine(string) // Load string "Done" onto stack. IL_000e: ldstr "Done" // Call Console.WriteLine(string) call void [mscorlib]System.Console::WriteLine(string) ret } 处理但尚未执行的位置停止,则需要存储public static void Maybe(int a, int b) { if (a <= b) goto IL_000e; Console.WriteLine("Greater"); IL_000e: Console.WriteLine("Done"); } 或其相对({{1}})。

由于这个原因,调试版本倾向于编写CIL,花费大量时间来编写它刚才做的记录。通过发布版本,我们可以获得更多类似的内容:

{{1}}

或者做一个类似的逐行写回C#:

{{1}}

因此,您可以看到发布版本如何更简洁地执行相同的操作。

答案 1 :(得分:5)

MSIL经过大量微优化,使存储尽可能小。转到Opcodes class并记下列出的<div class="fluid-wrapper hidden-phone" align="center"> <iframe style="border: 0;" src="https://www.google.com/maps/embed?pb=!1m14!1m8!1m3!1d105844.34381467322!2d-84.5165869!3d34.0018888!3m2!1i1024!2i768!4f13.1!3m3!1m2!1s0x88f56b7fceeae357%3A0x621d85f56e1d8376!2sCherokee+Collision+Center!5e0!3m2!1sen!2sus!4v1423686645744" width="850" height="500" frameborder="0"></iframe> 说明。它有6个版本,它们都完全相同。

StlocStloc_0Stloc_1Stloc_2是最小的,它们只占用一个字节。他们使用的变量号是隐式的,0到3.当然非常常用。

然后是Stloc_3,它是一个双字节操作码,第二个字节用于编码变量号。当方法有超过4个变量时,需要使用此方法。

最后有Stloc_S,它是一个三字节操作码,使用两个字节来编码变量号。当方法具有超过256个变量时必须使用。希望你永远不会那样做。当你编写一个超过65536个变量的怪物时,你运气不好,这是不受支持的。已经完成了btw,自动生成的代码可以超越这个限制。

很容易看到第二个片段中发生了什么,你添加了Stloc变量并将局部变量的数量从4增加到5.由于没有unassigned,编译器必须使用Stloc_4分配第5个变量。