为什么编译器即使对于非虚方法也会发出callvirt?

时间:2013-01-11 17:21:40

标签: c# virtual-method

  

可能重复:
  Why is the C# compiler emitting a callvirt instruction for a GetType() method call?

我看到当我调用类的实例方法时,C#编译器会发出  调用该方法的callvirt指令,为什么会这样?

这是否意味着编译器将所有实例方法都视为virtual methods,这是什么谜?

3 个答案:

答案 0 :(得分:12)

实现C#语言规范中的承诺。其中说通过null引用调用类的实例方法是不合法的。这可能听起来像一个明显的特征,但它实际上并不常见于OOP语言。特别是C ++ / CLI编译器没有它。 CLI规范没有它。像C ++这样的非托管语言没有它。

有时候,当实例方法不使用任何非静态类成员时,它甚至会达到一个好的结果。这种方法当然应该是静态的,但不是必需的或强制执行的。

C#要求非常好,它使NullReferenceException更容易诊断。由于它们是在调用站点而不是在实例方法内生成的,因此它阐明了对象引用为空。确定方法中 this 引用为null是有点困难的,特别是因为你看不到它。进一步复杂的地址实际上不是null,访问类的字段将生成一个偏离0的地址。如果对象是巨大的,超过64千字节则反过来 unsafe 。在这样一个大对象的末尾访问一个字段不一定会产生处理器异常,你只需要阅读随机垃圾。如果你写它就会损坏内存。

所以C#团队寻找一种廉价的方法来实现null测试。并在callvirt IL指令中找到了一个。与call不同, 承诺在CLI规范中发生异常。这是一个非常便宜的测试,它只需要一个机器代码指令。并且不需要分支,如果处理器的分支预测逻辑猜错了那就非常昂贵。

你现在也知道为什么String.Equals()包含了一些神秘的代码:

public override bool Equals(Object obj) {
    if (this == null)
        throw new NullReferenceException();
    // etc...
}

答案 1 :(得分:12)

汉斯和迈克的答案是正确的。只是添加一些额外的信息:

  • callvirt指令在非虚拟方法上明确记录为合法,以便您获得空检查行为。

  • 在极少数情况下,C#编译器证明非虚拟调用不可能有一个空接收器,它会回退到call并保存进行空检查所需的纳秒。例如,如果您有(new C()).InstanceMethod(),那么应该生成call,而不是callvirt,因为编译器知道接收表达式永远不会为空。 (如果分配失败,则抛出异常,因此永远不会执行调用。)

答案 2 :(得分:2)

简短回答:这样更安全。

callvirt将首先检查对象是否为null,如果是,则抛出异常。

您会注意到调用静态方法仍将使用call,因为该对象不能为空。

Here's有点历史。