34

出于好奇:CLR 如何将接口成员的虚拟方法调用分派给正确的实现?

我知道 CLR 为每种类型维护的 VTable 以及每个方法的方法槽,并且对于每个接口它都有一个额外的方法槽列表,这些方法槽指向相关的接口方法实现。但我不明白以下内容:CLR 如何有效地确定从类型的 VTable 中选择哪个接口方法槽列表?

MSDN 杂志 2005 年 5 月号的文章Drill Into .NET Framework Internals to See How the CLR Creates Runtime Objects讨论了由接口 ID 索引的进程级映射表 IVMap。这是否意味着同一进程中的所有类型都具有指向同一 IVMap 的相同指针?

它还指出:

如果MyInterface1由两个类实现,IVMap 表中将有两个条目。该条目将指向嵌入在MyClass方法表中的子表的开头。

CLR 如何知道选择哪个条目?它是否进行线性搜索以找到与当前类型匹配的条目?还是二分查找?还是某种直接索引并有一个可能包含许多空条目的地图?

我还通过 C# 3rd edition 阅读了关于 CLR 中的接口的章节,但它没有谈到这一点。因此,这个其他问题的答案不能回答我的问题。

4

3 回答 3

27

那篇文章已有 10 多年的历史,从那时起发生了很多变化。

IVMaps 现在已被Virtual Stub Dispatch取代。

虚拟存根分派 (VSD) 是使用存根进行虚拟方法调用而不是传统的虚拟方法表的技术。过去,接口调度要求接口具有进程唯一标识符,并且每个加载的接口都被添加到全局接口虚拟表映射中。

去阅读那篇文章,它有更多你需要知道的细节。它来自运行时之书,它是最初由 CLR 开发人员为 CLR 开发人员编写的文档,但现在已经发布给所有人。它基本上描述了运行时的内容。

我没有必要在这里重复这篇文章,但我只会陈述要点和它们的含义:

  • 当 JIT 看到对接口成员的调用时,它会将其编译为查找存根。这是一段将调用通用解析器的代码。
  • 通用解析器是一个函数,它将找出要调用的方法。这是调用这种方法的最通用的方法,因此也是最慢的方法。当第一次从查找存根调用时,它将将该存根(在运行时重写其代码)修补到调度存根中。它还生成一个解析存根供以后使用。此时查找存根消失。
  • 调度存根是调用接口成员的最快方法,但有一个问题:它对调用是单态的持乐观态度,这意味着它针对接口调用始终解析为相同具体类型的情况进行了优化。它将对象的方法表(即具体类型)与先前看到的方法表(硬编码到存根中)进行比较,如果比较成功,则调用缓存的方法(其地址也是硬编码的)。如果失败,它会退回到resolve stub
  • 解析存根处理多态调用(一般情况)。它使用缓存来查找要调用的方法。如果该方法不在缓存中,它会调用通用解析器(它也写入此缓存)。

这是一个重要的考虑因素,直接来自文章:

当调度存根经常失败时,调用站点被认为是多态的,并且解析存根将回补调用站点以直接指向解析存根,以避免持续失败的调度存根的开销。在同步点(当前是 GC 的末尾),假设调用站点的多态属性通常是临时的,多态站点将被随机提升回单态调用站点。如果这个假设对于任何特定的调用站点都不正确,它会很快触发一个 backpatch 再次将其降级为多态。

运行时非常看好单态调用站点,这在实际代码中很有意义,并且会尽量避免解析存根

于 2016-10-30T11:33:26.913 回答
24

.NET 堆栈

如果您查看链接站点上的图表,它可能会更容易理解。

这是否意味着同一进程中的所有类型都具有指向同一 IVMap 的相同指针?

是的,因为它在域级别,这意味着该 AppDomain 中的所有内容都具有相同的 IVMap。

CLR 如何知道选择哪个条目?它是否进行线性搜索以找到与当前类型匹配的条目?还是二分查找?还是某种直接索引并有一个可能包含许多空条目的地图?

这些类是用偏移量布局的,所以所有东西都有一个相对固定的区域。在寻找方法时,这使事情变得更容易。它将搜索 IVMap 表并从界面中找到该方法。从那里,它进入 MethodSlotTable 并使用该类的接口实现。类的接口映射包含元数据,但是,实现的处理方式与任何其他方法一样。

再次从您链接的网站:

每个接口实现都会在 IVMap 中有一个条目。如果 MyInterface1 由两个类实现,IVMap 表中将有两个条目。该条目将指向嵌入在 MyClass 方法表中的子表的开头

这意味着每次实现接口时,它在 IVMap 中都有一个唯一记录,该记录指向 MethodSlotTable,而 MethodSlotTable 又指向实现。因此,它知道根据调用它的类选择哪个实现,因为 IVMap 记录指向调用该方法的类中的 MethodSlotTable。所以我想这只是通过 IVMap 进行线性搜索以找到正确的实例,然后它们就会关闭并运行。


编辑:提供有关 IVMap 的更多信息。

同样,来自 OP 中的链接:

第一个 InterfaceInfo 条目的前 4 个字节指向 MyInterface1 的 TypeHandle(参见图 9 和图 10)。下一个 WORD(2 个字节)由 Flags 占用(其中 0 是从 parent 继承的,1 在当前类中实现)。Flags 后面的 WORD 是 Start Slot,类加载器使用它来布局接口实现子表。

所以这里我们有一个表格,其中数字是字节的偏移量。这只是 IVMap 中的一条记录:

+----------------------------------+
| 0 - InterfaceInfo                |
+----------------------------------+
| 4 - Parent                       |
+----------------------------------+
| 5 - Current Class                |
+----------------------------------+
| 6 - Start Slot (2 Bytes)         |
+----------------------------------+

假设这个 AppDomain 中有 100 条接口记录,我们需要找到每一条的实现。我们只是比较第 5 个字节,看看它是否与我们当前的类匹配,如果匹配,我们跳转到第 6 个字节中的代码。因为,每条记录是 8 字节长,我们需要做这样的事情:(伪代码)

findclass :
   if (!position == class) 
      findclass adjust offset by 8 and try again

虽然它仍然是线性搜索,但实际上,只要迭代的数据量不是很大,它不会花费那么长时间。我希望这会有所帮助。


编辑2:

因此,在查看图表并想知道为什么图表中的类的 IVMap 中没有 Slot 1 之后,我重新阅读了该部分并发现了这一点:

IVMap 是根据嵌入在方法表中的接口映射信息创建的。Interface Map 是在 MethodTable 布局过程中基于类的元数据创建的。一旦类型加载完成,在方法分派中只使用 IVMap。

因此,类的 IVMap 仅加载特定类继承的接口。它看起来像是从域 IVMap 复制的,但只保留指向的接口。这就引出了另一个问题,怎么做?很可能它相当于 C++ 执行 vtables 的方式,其中每个条目都有一个偏移量,并且接口映射提供了要包含在 IVMap 中的偏移量列表。

如果我们查看可能适用于整个域的 IVMap:

+-------------------------+
| Slot 1 - YourInterface  |
+-------------------------+
| Slot 2 - MyInterface    |
+-------------------------+
| Slot 3 - MyInterface2   |
+-------------------------+
| Slot 4 - YourInterface2 |
+-------------------------+

假设该域中只有 4 个接口映射的实现。每个槽都有一个偏移量(类似于我之前发布的 IVMap 记录),并且此类的 IVMap 将使用这些偏移量来访问 IVMap 中的记录。

假设每个插槽是 8 个字节,插槽 1 从 0 开始,所以如果我们想要获取插槽 2 和 3,我们可以这样做:

mov ecx,edi
mov eax, dword ptr [ecx]
mov eax, dword ptr [ecx+08h] ; slot 2
; do stuff with slot 2
mov eax, dword ptr [ecx+10h] ; slot 3
; do stuff with slot 3

请原谅我的 x86,因为我不太熟悉它,但我试图复制他们在链接到的文章中的内容。

于 2012-03-21T19:09:16.577 回答
0

从您链接的第一篇文章中:

如果 MyInterface1 由两个类实现,IVMap 表中将有两个条目。该条目将指向嵌入在 MyClass 方法表中的子表的开头,如图 9 所示

ClassLoader 遍历当前类、父类和接口的元数据,并创建方法表。在布局过程中,它会替换任何被覆盖的虚方法,替换任何被隐藏的父类方法,创建新的插槽,并根据需要复制插槽。插槽的重复对于创建每个接口都有自己的迷你 vtable 的错觉是必要的。但是,重复的插槽指向相同的物理实现。

这向我表明,接口的 IVMap 具有由类名(或某些等价物)键入的条目,这些条目指向类的 vtable 的一个子部分,该子部分本质上具有实现该接口的每个类方法的重复实现,并由指向与类自己的 vtable 条目相同的物理实现。

虽然可能是完全错误的。

于 2012-03-21T18:54:10.887 回答