【问题标题】:How are compiled method calls that return a value?如何编译返回值的方法调用?
【发布时间】:2012-02-25 18:46:49
【问题描述】:

如果我有一个返回值的方法(例如Dictionary 类的Remove 方法返回一个bool),如果我不将返回值分配给变量会怎样?换句话说,如果我写dictionary.Remove("plugin-01"); 而不将结果分配给bool 变量,那么编译相对于bool b = dictionary.Remove("plugin-01"); 有什么区别?

【问题讨论】:

    标签: c# .net compiler-construction


    【解决方案1】:

    让我们看一个简单的示例及其生成的 IL 代码(由 LinqPad 提供)

    void Main()
    {
        Bar();
        Baz();
    }
    
    bool Bar()
    {
      return true;
    }
    
    void Baz()
    {
      Console.WriteLine("placeholder");
    }
    

    这会产生以下 IL:

    IL_0000:  ldarg.0     
    IL_0001:  call        UserQuery.Bar
    IL_0006:  pop          //remove current value from evaluation stack
    IL_0007:  ldarg.0     
    IL_0008:  call        UserQuery.Baz
    
    
    Bar:
    IL_0000:  ldc.i4.1    
    IL_0001:  ret    
    
    Baz:
    IL_0000:  ldstr       "placeholder"
    IL_0005:  call        System.Console.WriteLine
    IL_000A:  ret   
    

    您可以看到Bar 被调用,然后弹出以从评估堆栈中删除布尔返回值 - 它无处可去。我必须更新示例以包含对 Baz() 的另一个方法调用,否则将不会发出 pop,因为程序结束。

    现在我们来看一个实际使用返回值的案例:

    void Main()
    {
        bool foo = Bar();
        Console.WriteLine(foo);
    }
    
    bool Bar()
    {
      return true;
    }
    

    这会产生以下 IL:

    IL_0000:  ldarg.0     
    IL_0001:  call        UserQuery.Bar
    IL_0006:  stloc.0    //pops current value from evaluation stack, stores in local var
    IL_0007:  ldloc.0     
    IL_0008:  call        System.Console.WriteLine
    
    Bar:
    IL_0000:  ldc.i4.1    
    IL_0001:  ret 
    

    忽略System.Console.WriteLine 部分,它是IL_007 之后的所有内容,包括IL_007 - 只需添加它,这样编译器就不会优化变量的使用。您会看到Bar 方法调用的结果从评估堆栈中弹出并存储在局部变量foo 中。这就是区别——pop 抓取并丢弃返回值,stloc.0 将结果分配给变量。

    因此,如果您不需要方法调用的结果,您应该忽略结果。即使您将结果分配给变量并且该变量从未使用过,编译器也可能完全优化掉变量和分配 - 至少在发布模式下(在调试模式下,大多数优化被禁用以改善您的调试体验)。

    【讨论】:

    • 如果去掉Console.WriteLine(foo),IL应该和第一个例子一样吗?
    • 是的——它被优化掉了(大概)因为变量foo从未被使用过。
    • 在您的第一个“这会产生以下 IL”中,我认为您应该在调用之后包含 POP 指令。这澄清了方法结果在 MSIL 堆栈上,调用者必须以某种方式处理它(即使“处理”它意味着显式丢弃它)
    • @CoreyKosak:我展示了由 LinqPad 生成的完整 IL 输出 - 但你是对的 - 应该有一个 pop,在这种情况下,它只是没有发出,因为程序在进行任何操作之前就结束了其他电话。
    • @BrokenGlass:感谢您的精彩解释!不过有一个问题,在方法调用之前,通常会在 IL 中看到 Ldoc、Ldc 等。我将它们读作将参数值推送到评估堆栈。但是,例如被调用方方法将此参数读取为 Ldarg.0。根据 Ldarg 操作码的行为 - 它将参数加载到堆栈上。相同的论点不是两次推送到评估堆栈吗?我确定这里缺少一些基本的东西,但不确定是什么:(
    【解决方案2】:

    如果你不使用它,返回值将被忽略。

    你可以做类似的事情

    if(!dictionary.Remove("plugin-01")) {
        MessageBox.Show("Error: plugin-01 does not exist!");
    }
    

    不介意的话可以放心写

    dictionary.Remove("plugin-01");
    

    【讨论】:

      【解决方案3】:

      调用部分相同,但在IL中跳过了赋值部分。看看 - 这是一个简单程序的反汇编:

      var d = new Dictionary<int,int>();
      bool a = d.Remove(5);
      d.Remove(6);
      

      反汇编如下:

          bool a = d.Remove(5);
      00000057  mov         ecx,dword ptr [ebp-40h] 
      0000005a  mov         edx,5 
      0000005f  cmp         dword ptr [ecx],ecx 
      00000061  call        69106A00
      // Here is the assignment part 
      00000066  mov         dword ptr [ebp-4Ch],eax 
      00000069  movzx       eax,byte ptr [ebp-4Ch] 
      0000006d  mov         dword ptr [ebp-44h],eax 
          d.Remove(6);
      00000070  mov         ecx,dword ptr [ebp-40h] 
      00000073  mov         edx,6 
      00000078  cmp         dword ptr [ecx],ecx 
      0000007a  call        69106A00
      

      前四行很常见;第一次调用的最后三行处理分配;他们在第二次调用的反汇编中丢失了。

      【讨论】:

        猜你喜欢
        • 1970-01-01
        • 1970-01-01
        • 2019-10-28
        • 2018-05-23
        • 1970-01-01
        • 2013-02-14
        • 2015-03-22
        • 1970-01-01
        • 2017-02-18
        相关资源
        最近更新 更多