38

向某人解释虚拟调度很容易:每个对象都有一个指向表的指针作为其数据的一部分。类上有 N 个虚方法。对特定方法的每次调用都会在对象到达时索引对象并调用表中的第 i 个方法。每个实现方法 X() 的类都将在相同的第 i 个索引中包含方法 X() 的代码。

但随后我们得到了接口。接口需要某种扭曲,因为两个实现相同接口的非继承类将在表的不同索引中具有虚函数。

我在网上搜索了很多关于如何实现接口调度的讨论。有两大类:a)某种哈希表在对象上查找以找到正确的调度表 b)当对象被强制转换到接口时,会创建一个指向相同数据但指向不同数据的新指针虚表。

但是,尽管有很多关于它如何工作的信息但我找不到任何关于 .NET 运行时引擎如何实际实现它的信息。

有谁知道描述当对象类型是接口时在 callvirt 指令中发生的实际指针算术的文档?

4

2 回答 2

37

CLR 中的接口调度是黑魔法。

正如您正确指出的那样,虚拟方法分派在概念上很容易解释。事实上,我在本系列文章中这样做了,我在其中描述了如何用一种缺乏虚拟方法的类 C# 语言实现虚拟方法:

http://blogs.msdn.com/b/ericlippert/archive/2011/03/17/implementing-the-virtual-method-pattern-in-c-part-one.aspx

我描述的机制与实际使用的机制非常相似。

接口调度更难描述,CLR 实现它的方式一点也不明显。接口调度的 CLR 机制已经过仔细调整,以便为最常见的情况提供高性能,因此,随着 CLR 团队对实际使用模式的了解越来越多,这些机制的细节可能会发生变化。

基本上它在幕后工作的方式是每个调用站点——即代码中调用接口方法的每个点——都有一个小缓存,上面写着“我认为与这个接口槽关联的方法是。 .. 这里”。绝大多数情况下,缓存是正确的;您很少使用一百万种不同的实现调用相同的接口方法一百万次。它通常是一遍又一遍地重复相同的实现,连续多次。

如果缓存结果是未命中,则它会退回到维护的哈希表,以进行稍慢的查找。

如果结果是未命中,则分析对象元数据以确定与接口槽对应的方法。

最终效果是,在给定的调用站点,如果您总是调用映射到特定类方法的接口方法,它会非常快。如果您总是为给定的接口方法调用少数类方法之一,那么性能非常好。最糟糕的做法是永远不要在同一个站点用同一个接口方法调用同一个类方法两次;每次都采用最慢的路径。

如果您想知道如何在内存中维护慢速查找的表,请参阅 Matthew Watson 的答案中的链接。

于 2013-09-25T20:16:39.687 回答
14

因为编译器总是必须有一个实际的对象来调用方法(在运行时),所以它总是在运行时知道它正在处理的具体类型。

调用虚方法的代码首先确定了所使用对象的类型。然后它会查询类型的方法表以查找被调用的方法。然后代码简单地调用该方法,将对象的引用作为“this”与任何其他参数一起传递。

我怀疑您感兴趣的关键部分是代码如何在类型的方法表中查找方法的地址。

有关方法表的更多详细信息,请参见 MSDN 杂志 2005 年 5 月版中的“JIT and Run”文章(在撰写本文时,可以从该页面以“.chm”的形式下载该文章- 但您将不得不去由于安全限制,在文件正确显示之前将其解锁。)

关于查找是如何完成的仍然有点手忙脚乱,但它确实提供了很多其他细节。

于 2013-09-25T20:14:58.977 回答