【问题标题】:Why Int32.ToString() emit call instruction instead of callvirt?为什么 Int32.ToString() 发出调用指令而不是 callvirt?
【发布时间】:2016-07-30 16:37:00
【问题描述】:

对于以下代码sn-p:

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

【问题讨论】:

    标签: c# .net clr cil boxing


    【解决方案1】:

    这是编译器对原始类型所做的优化。

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

    来自MSDN

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

    还有:

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

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

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

    【讨论】:

    • 非常感谢,我也猜这是编译器优化工作。但我找不到任何支持我猜测的材料。因此,如果您能提供任何有关这种针对原始类型的特殊优化的文档,我将不胜感激。再次感谢。
    • @LifuHuang 更新了我的答案。
    • 谢谢@Eli Arbel,你真的解决了我的问题:)
    【解决方案2】:

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

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

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

    Call and Callvirt

    【讨论】:

    • 但是所有值类型都是隐式密封的,所以我认为从这个角度来看,Int32Test 的行为方式应该相同。如果我错了,请纠正我。
    • 是的,它是框架提供的密封类型,即特殊情况
    • 对不起,我不知道你的“特殊情况”在这里是什么意思。请给我更多细节好​​吗?
    • 正如@ali 所说,编译器对代码执行性能进行优化,因为它可以针对无法扩展的原始类型进行优化
    • "compiler don't know about it" 编译器当然有所有信息来确定覆盖存在并且它可以使用call。它只是决定不这样做,原因在@EliArbel 的答案末尾解释。
    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 2020-09-27
    • 2012-10-28
    • 1970-01-01
    • 2014-09-17
    • 1970-01-01
    • 2010-11-24
    • 2011-10-19
    相关资源
    最近更新 更多