c# - 再添加一个int变量时生成的不同IL

标签 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)

最佳答案

一些注意事项。

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

.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 0x004的编码,是相关本地的索引。这意味着“弹出栈顶的值,并将其存储在本地的第5个(索引4)中”。

现在,这里有一些缩写。其中之一是几个指令的.s“短”形式(等效于_S值的名称中的System.Reflection.Emit.OpCode)。这些是其他指令的变体,它们采用一个字节的值(带符号或无符号,取决于指令),而另一种形式则采用一个2字节或4字节的值,通常是跳转的索引或相对距离。因此,我们可以使用stloc V_4代替stloc.s V_4,而该0x13 0x4仅为stloc V_0,因此更小。

然后,有些变体在指令中包含特定值。因此,我们可以使用stloc.s V_0而不是stloc.00x0A,它只是单字节stloc.s

当您考虑一次只使用少数几个本地人是很普遍的,因此使用stloc.0或(更好)使用stloc.1stloc.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。 (顺带一提,您在使用什么?我大部分使用ILSpy,如果您调试与该文件关联的信息,它会相应地称为stloc)。

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

.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.
}


行为将与您的代码差不多,但会更大一些。因此,我们将stloc.0替换为stloc.3,在我们不能使用的地方使用stloc.s,但仍然可以使用stloc.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
}


让我们以非常直观的逐步方式将其写回到C#:

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是因为CIL实际上没有像forwhile这样的东西,甚至没有我们可以放在ifelse之后的块,它仅具有跳转和条件跳转。

但是,为什么不去存储值(我在C#重写中称为shouldJump)而不是仅仅对它起作用呢?

如果您要调试的话,只是为了更轻松地检查每个点正在发生的事情。特别是,要使调试器能够在制定出a > b但尚未起作用的地方停止,则需要存储a > b或相反的(a <= b)。

因此,调试版本倾向于编写CIL,而该CIL花费大量时间来记录其所做的工作。通过发布版本,我们将获得更多类似的东西:

.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
}


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

public static void Maybe(int a, int b)
{
  if (a <= b) goto IL_000e;
  Console.WriteLine("Greater");
IL_000e:
  Console.WriteLine("Done");
}


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

关于c# - 再添加一个int变量时生成的不同IL,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/34091444/

相关文章:

c# - 在 C# 中通过 COM Interop 编码字符串时编码失败(双 UTF8 编码?)

c# - 使用枚举类型的 JSON 反序列化

.net - 为什么 MethodBody.GetILAsByteArray 在不同的平台上返回不同的数组?

c# - 在新的 c# 6 "?"空检查的情况下调用而不是 callvirt

c# - 如何使用 CodeDOM 定位特定语言版本?

c# - 在 .Net 中构建 Web 服务的首选方法是什么?

c# - 使用 Xamarin 自动登录 Azure 移动服务

.net - 每个 .NET 类的最大方法数是多少

c# - 公众号是做什么的?

MSBUILD/csc : Cleanest way of handling x64 mscorlib warning 1607