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

时间:2016-07-30 16:37:01

标签: c# .net clr cil boxing

对于以下代码段:

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()都不会发生装箱。因此,我想知道为什么编译器为constraned发出callvirt + Test,为call发出Int32

2 个答案:

答案 0 :(得分:6)

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

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

来自MSDN

  

如果thisType是值类型且thisType实现method,那么ptr将作为this指针未经修改地传递给 {{1}方法指令,用于call的方法实现。

  

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

我不知道有关优化的任何官方文档,但您可以在Roslyn仓库中看到MayUseCallForStructMethod method的评论。

至于为什么这种优化被推迟到非原始类型的运行时,我相信这是因为实现可以改变。想象一下,引用一个最初覆盖thisType的库,然后将DLL(不重新编译!)更改为删除覆盖的库。这会导致运行时异常。对于原语,他们可以肯定它不会发生。

答案 1 :(得分:0)

这是因为serialize是一个提供密封类型的框架,并且永远不会发生某些其他类型覆盖int serialize方法,因此编译器知道它总是需要调用Int ToString类型提供的方法实现,因此不需要使用ToString()来确定要调用的实现。

对于primitve类型编译器知道要调用int的哪个实现,但是当我们创建自定义值类型时,它是一个以前从未存在的新类型,因此编译器不知道它和它需要弄清楚要调用哪个实现以及它所在的位置,因为它默认从callvirt继承,因此编译器必须ToString找到为自定义提供的Object实现如果不重写,它将调用显式的Object类型。

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

Call and Callvirt