为什么Int32.ToString()发出调用指令而不是callvirt

本文关键字:指令 callvirt 调用 ToString Int32 为什么 | 更新日期: 2023-09-27 17:59:23

对于以下代码片段:

struct Test
{
    public override string ToString()
    {
        return "";
    }
}
public class Program
{
    public static void Main()
    {
        Test a = new Test();
        a.ToString();
        Int32 b = 5;
        b.ToString();
    }
}

编译器发出以下IL:

  .locals init ([0] valuetype ConsoleApplication2.Test a,
           [1] int32 b)
  IL_0000:  nop
  IL_0001:  ldloca.s   a
  IL_0003:  initobj    ConsoleApplication2.Test
  IL_0009:  ldloca.s   a
  IL_000b:  constrained. ConsoleApplication2.Test
  IL_0011:  callvirt   instance string [mscorlib]System.Object::ToString()
  IL_0016:  pop
  IL_0017:  ldc.i4.5
  IL_0018:  stloc.1
  IL_0019:  ldloca.s   b
  IL_001b:  call       instance string [mscorlib]System.Int32::ToString()
  IL_0020:  pop
  IL_0021:  ret

由于值类型TestInt32都覆盖了ToString()方法,我认为a.ToString()b.ToString()中都不会出现装箱。所以我想知道为什么编译器为Test发出constraned+callvirt,为Int32发出call

为什么Int32.ToString()发出调用指令而不是callvirt

这是编译器对基元类型进行的优化。

但是,即使对于自定义结构,由于constrained.操作码,在方法被重写的情况下,callvirt实际上也会在运行时作为call执行。它允许编译器在任何一种情况下发出相同的指令,并让运行时处理它

来自MSDN:

如果thisType是值类型,并且thisType实现method,则ptr作为指向call方法指令this指针未经修改地传递,以供thisType实现方法。

和:

constrained操作码允许IL编译器以统一的方式调用虚拟函数,而与ptr是值类型还是引用类型无关。尽管它适用于thisType是泛型类型变量的情况,但受约束前缀也适用于非泛型类型,并且可以降低在隐藏值类型和引用类型之间区别的语言中生成虚拟调用的复杂性。

我不知道任何关于优化的官方文档,但您可以在Roslyn repo中看到MayUseCallForStructMethod方法的注释。

至于为什么将这种优化推迟到非基元类型的运行时,我认为这是因为实现可能会发生变化。想象一下,引用一个最初具有ToString覆盖的库,然后将DLL(无需重新编译!)更改为删除覆盖的DLL。这会导致运行时异常。对于基元,他们可以确信这不会发生。

因为Int是一个框架提供的密封类型,其他类型永远不会覆盖int ToString方法,所以编译器知道它总是需要调用int类型中提供的ToString()方法实现,所以它不需要使用callvirt来确定要调用哪个实现。

对于原始类型,编译器知道要调用ToString的哪个实现,但当我们创建自定义值类型时,它是一个以前从未存在过的新类型,所以编译器不知道它,它需要弄清楚要调用哪个实现以及它驻留在哪里,因为它默认继承自Object,因此编译器必须执行CCD_ 33来定位为自定义类型提供的CCD_。

以下现有的SO帖子可以帮助你理解这一点:

Call和Callvirt