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

C# 为什么Int32.ToString()发出调用指令而不是callvirt?,c#,.net,clr,cil,boxing,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();

对于以下代码段:

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

由于值类型
Test
Int32
都重写了
ToString()
方法,因此我认为
a.ToString()
b.ToString()
中都不会出现装箱。因此,我想知道为什么编译器为
Test
发出
constranned
+
callvirt
,为
Int32
发出
call

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

对于primitve类型,编译器知道调用
ToString
的哪个实现,但当我们创建一个自定义值类型时,它是一个以前从未存在过的新类型,所以编译器不知道它,它需要弄清楚调用哪个实现以及它驻留在哪里,由于默认情况下它继承自
对象
,因此编译器必须执行
callvirt
来定位为自定义类型提供的
ToString()
实现,如果不重写,它将调用显而易见的对象类型

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


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

但是,即使对于自定义结构,
callvirt
实际上也会在运行时作为
call
执行,因为
受约束。
opcode-在方法被重写的情况下。它允许编译器在两种情况下发出相同的指令,并让运行时处理

发件人:

如果
thisType
是一个值类型,并且
thisType
实现
方法
,则
ptr
将作为
this
指向
调用
方法指令
的指针进行传递,而不作任何修改,以便
thisType
实现方法

以及:

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

我不知道有任何关于优化的官方文档,但是你可以在Roslyn回购协议中看到关于优化的评论


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

但是所有的值类型都是隐式密封的,所以我认为从这个角度来看,
Int32
Test
的行为应该是相同的。如果我错了,请纠正我。是的,这是框架提供的密封类型,这是特殊情况抱歉,我不知道你的“特殊情况”在这里是什么意思。你能告诉我更多的细节吗?正如@ali所说,编译器对代码执行的性能进行优化,就像对不能扩展的基本类型进行优化一样“编译器不知道”编译器当然有所有的信息来确定覆盖是否存在,并且可以使用
调用
。它只是决定不这样做,原因在@EliArbel的答案末尾解释了。非常感谢,我也猜是编译器优化起作用了。但我找不到任何支持我猜测的材料。因此,如果您能提供关于这个特殊优化的原始类型的文档,我将不胜感激。再次感谢。@LifuHuang更新了我的答案。谢谢@Eli Arbel,你真的解决了我的问题:)