出于好奇: CLR如何调度虚拟方法调用接口成员到正确的实现?
我知道CLR为每种类型维护的VTable以及每种方法的方法槽,以及每个接口都有一个额外的方法槽列表,指向相关的接口方法实现。但我不明白以下内容:CLR如何有效地确定从类型的VTable中选择哪个接口方法槽列表?
2005年5月号MSDN杂志上的文章Drill Into .NET Framework Internals to See How the CLR Creates Runtime Objects讨论了由接口ID索引的进程级映射表IVMap。这是否意味着同一进程中的所有类型都具有指向同一IVMap的相同指针?
它还声明:
如果
MyInterface1
由两个类实现,则会有两个类 IVMap表中的条目。该条目将指回到开头 嵌入MyClass
方法表中的子表。
CLR如何知道要选择哪个条目?它是否进行线性搜索以查找与当前类型匹配的条目?还是二元搜索?或者某种直接索引并且有一个可能有很多空条目的地图?
我还通过C#第3版阅读了CLR中的接口章节,但它没有谈到这一点。因此,this other question的答案不回答我的问题。
答案 0 :(得分:22)
如果您查看链接网站上的图表,可能会更容易理解。
这是否意味着同一进程中的所有类型都具有指向同一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从父级继承,1在当前类中实现)。 Flags之后的WORD是Start Slot,类加载器使用它来布局接口实现子表。
所以这里我们有一个表,其中数字是字节的偏移量。这只是IVMap中的一条记录:
+----------------------------------+
| 0 - InterfaceInfo |
+----------------------------------+
| 4 - Parent |
+----------------------------------+
| 5 - Current Class |
+----------------------------------+
| 6 - Start Slot (2 Bytes) |
+----------------------------------+
假设此AppDomain中有100个接口记录,我们需要找到每个接口记录的实现。我们只是比较第5个字节以查看它是否与我们当前的类匹配,如果匹配,我们跳转到第6个字节的代码。因为,每个记录长度为8个字节,我们需要做这样的事情:( Psuedocode)
findclass :
if (!position == class)
findclass adjust offset by 8 and try again
虽然它仍然是线性搜索,但实际上,由于迭代的数据大小不是很大,所以不会花费很长时间。我希望有所帮助。
EDIT2:
因此,在查看图表并想知道为什么图表中的类的IVMap中没有Slot 1之后,我重新阅读了该部分并找到了:
IVMap是基于方法表中嵌入的接口映射信息创建的。在MethodTable布局过程中,基于类的元数据创建接口映射。一旦完成了类型加载,在方法分派中只使用IVMap。
因此,类的IVMap仅加载特定类继承的接口。它看起来像是从域IVMap复制,但只保留指向的接口。这带来了另一个问题,怎么样?有可能它相当于C ++如何处理vtable,其中每个条目都有一个偏移量,接口映射提供了一个包含在IVMap中的偏移量列表。
如果我们查看可能适用于整个域的IVMap:
+-------------------------+
| Slot 1 - YourInterface |
+-------------------------+
| Slot 2 - MyInterface |
+-------------------------+
| Slot 3 - MyInterface2 |
+-------------------------+
| Slot 4 - YourInterface2 |
+-------------------------+
假设此域中只有4个Interface Map实现。每个插槽都有一个偏移量(类似于我之前发布的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,因为我不熟悉它,但我试图复制它们在链接到的文章中的内容。
答案 1 :(得分:18)
那篇文章已经超过10年了,很多从那以后发生了变化。
IVMaps现已被Virtual Stub Dispatch 取代。
虚拟存根调度(VSD)是使用存根进行虚拟方法调用而不是传统虚方法表的技术。过去,接口调度要求接口具有进程唯一标识符,并且每个已加载的接口都已添加到全局接口虚拟表映射中。
去阅读那篇文章,它有更多你需要知道的细节。它来自Book of the Runtime,这是最初由CLR开发人员为CLR开发人员编写的文档,但现在已经为所有人发布。它基本上描述了运行时的内容。
我没有必要在这里复制这篇文章,但我只是说明要点及其含义:
这是一个重要的考虑因素,直接来自文章:
当调度存根经常失败时,调用站点被认为是多态的,解析存根将返回补丁调用站点以直接指向解析存根以避免一致失败的调度存根的开销。在同步点(当前是GC的结尾),在假设呼叫站点的多态属性通常是临时的情况下,多态站点将被随机提升回单态呼叫站点。如果这个假设对于任何特定的呼叫站点都是不正确的,它将很快触发一个后端补丁,将其再次降级为多态。
关于单态调用站点的运行时非常乐观,这在实际代码中很有意义,并且会尽可能地避免解析存根
答案 2 :(得分:0)
从您关联的第一篇文章:
如果MyInterface1由两个类实现,则会有两个类 IVMap表中的条目。该条目将指回到开头 嵌入在MyClass方法表中的子表,如图所示 图9
和
ClassLoader遍历当前类的元数据, 父类和接口,并创建方法表。在里面 布局过程中,它取代了任何被覆盖的虚拟方法,取而代之 任何隐藏的父类方法,创建新的插槽,和 必要时复制插槽。插槽的重复是必要的 创造一个错觉,每个界面都有自己的迷你vtable。 但是,重复的插槽指向相同的物理 实施
这告诉我,接口的IVMap具有由类名(或某些等价物)键入的条目,这些条目指向类的vtable的子部分,其实质上具有实现该接口的每个类的方法的重复实现,由指向与类自己的vtable条目相同的物理实现。
虽然可能完全错误。