【问题标题】:Why is the C# compiler emitting a callvirt instruction for a GetType() method call?为什么 C# 编译器会为 GetType() 方法调用发出 callvirt 指令?
【发布时间】:2009-05-10 16:56:33
【问题描述】:

我很想知道为什么会这样。请阅读下面的代码示例以及每个部分下方的 cmets 中发出的相应 IL:

using System;

class Program
{
    static void Main()
    {
        Object o = new Object();
        o.GetType();

        // L_0001: newobj instance void [mscorlib]System.Object::.ctor()
        // L_0006: stloc.0 
        // L_0007: ldloc.0 
        // L_0008: callvirt instance class [mscorlib]System.Type [mscorlib]System.Object::GetType()

        new Object().GetType();

        // L_000e: newobj instance void [mscorlib]System.Object::.ctor()
        // L_0013: call instance class [mscorlib]System.Type [mscorlib]System.Object::GetType()
    }
}

为什么编译器为第一部分生成 callvirt 而为第二部分生成 call?编译器是否有任何理由会为非虚拟方法发出 callvirt 指令?如果在某些情况下编译器会为非虚拟方法发出callvirt,这是否会给类型安全带来问题?

【问题讨论】:

  • 非常好的问题......让我伸出手去拿我的书。
  • 总结:案例(1)调用虚方法:生成callvirt。案例(2)在可空接收器上调用实例方法:生成 callvirt 以获取廉价的空检查——是的,这是类型安全的。案例(3)在已知的不可空接收器上调用实例方法:生成对避免空检查的调用。您的第一个示例属于 (2) 类,您的第二个示例属于 (3) 类。 (编译器知道 new 永远不会返回 null,因此不需要再次检查。)
  • 感谢 Eric,空值检查很有意义。您的评论加上 Gishu 的回答让事情变得更加清晰! :)

标签: c# compiler-construction il type-safety


【解决方案1】:

参见 this Eric Gunnerson 的旧博文。

这是帖子的正文:

为什么 C# 总是使用 callvirt?

这个问题出现在一个内部 C# 别名上,我认为答案会引起普遍关注。这是假设答案是正确的 - 已经有一段时间了。

.NET IL 语言同时提供 call 和 callvirt 指令,其中 callvirt 用于调用虚函数。但是,如果您查看 C# 生成的代码,您会发现即使在不涉及虚函数的情况下,它也会生成“callvirt”。为什么会这样?

我回顾了我的语言设计说明,它们非常清楚地表明我们决定在 1999 年 12 月 13 日使用 callvirt。不幸的是,他们没有抓住我们这样做的理由,所以我将不得不从我的记忆中消失。

我们从某人(可能是使用 C# 的 .NET 小组之一(当时认为它还没有命名为 C#))收到一份报告,该人编写了在空指针上调用方法的代码,但他们没有'不会因为方法没有访问任何字段而引发异常(即“this”为空,但方法中没有使用它)。然后那个方法调用了另一个方法,它确实使用了这个点并抛出了一个异常,随之而来的是一些令人头疼的问题。在他们弄明白之后,他们给我们发了一个便条。

我们认为能够在空实例上调用方法有点奇怪。 Peter Golde 做了一些测试,看看总是使用 callvirt 对性能的影响是什么,它足够小,我们决定做出改变。

【讨论】:

  • 太糟糕了,非虚拟方法无法指定在没有 callvirt 的情况下调用它,因为允许像 string.IsNullOrEmpty 这样的方法可用于空字符串会很有帮助。
  • @supercat 一种解决方法是使用扩展方法,即使对象为空,它仍然会在扩展的第一个参数中传递。
  • 不幸的是,您不能在泛型函数中使用具有泛型类型的扩展方法来(有效地)在静态方法上创建运行时决策。
【解决方案2】:

只是谨慎行事。

从技术上讲,C# 编译器并不总是使用callvirt

对于在值类型上定义的静态方法和方法,它使用call。大多数是通过callvirt IL 指令提供的。

两者之间的不同之处在于call 假定“用于进行调用的对象”不为空。另一方面,callvirt 检查 not null 并在需要时抛出 NullReferenceException。

  • 对于静态方法,对象是类型对象,不能为空。值类型同上。因此,call 用于它们 - 更好的性能。
  • 对于其他语言,语言设计者决定使用callvirt,因此 JIT 编译器验证用于进行调用的对象不为空。即使对于非虚拟实例方法.. 他们更看重安全而不是性能。

另请参阅:Jeff Richter 在这方面做得更好 - 在他的 CLR 中通过 C# 2nd Ed 的“设计类型”一章中

【讨论】:

【解决方案3】:

除了(也许-)有趣之外...GetType() 的不同之处在于它不是 virtual - 这会导致一些very, very odd things

(标记为 wiki,因为它与实际问题有些偏离主题)

【讨论】:

    【解决方案4】:

    编译器不知道第一个表达式中o 的真实类型,但它知道第二个表达式中的真实类型。看起来它一次只查看一个语句。

    这很好,因为 C# 严重依赖 JIT 进行优化。在这种简单的情况下,很可能两个调用都会在运行时变成实例调用。

    我不相信 callvirt 会为非虚拟方法发出,但即使是,也没有问题,因为该方法永远不会被覆盖(出于显而易见的原因)。

    【讨论】:

    • 这仅解释了虚拟方法的 callvirt。但是 GetType 不是虚拟的。这是一个在 CLR 内部深处实现的外部函数(可能返回一个存储在对象的 vtable 或其他东西中的字段)。每个对象都使用相同的方法。
    • 很公平——我认为 GetType 是虚拟的。我的错。我喜欢达斯汀的回答。
    【解决方案5】:

    我会冒险猜测这是因为第一个分配给一个变量,该变量可能包含另一个类型的向下转换实例,该实例可能已覆盖 GetType(尽管我们可以看到它没有);第二个永远不会是Object以外的任何东西。

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 1970-01-01
      • 2012-10-02
      • 2018-06-09
      • 1970-01-01
      • 1970-01-01
      • 2019-11-18
      • 1970-01-01
      • 1970-01-01
      相关资源
      最近更新 更多