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

Why Int32.ToString() emit call instruction instead of callvirt?

对于以下代码片段:

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

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

对于原始类型,编译器知道要调用 ToString 的哪个实现,但是当我们创建一个自定义值类型时,它是一个新的,它以前不存在,所以编译器不知道它它需要弄清楚要调用哪个实现以及它所在的位置,因为它默认继承自 Object,因此编译器必须执行 callvirt 来定位提供的 ToString() 实现对于自定义类型,如果未覆盖,它将调用显而易见的对象类型。

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

Call and Callvirt

这是编译器针对基本类型所做的优化。

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

来自 MSDN:

If thisType is a value type and thisType implements method then ptr is passed unmodified as the this pointer to a call method instruction, for the implementation of method by thisType.

并且:

The constrained opcode allows IL compilers to make a call to a virtual function in a uniform way independent of whether ptr is a value type or a reference type. Although it is intended for the case where thisType is a generic type variable, the constrained prefix also works for nongeneric types and can reduce the complexity of generating virtual calls in languages that hide the distinction between value types and reference types.

我不知道有任何关于优化的官方文档,但您可以在 Roslyn 存储库中查看 MayUseCallForStructMethod method.

的备注

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