CLR实现对接口成员的虚方法调用

Dan*_*ker 32 c# clr interface dispatch method-call

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

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

从2005年5月刊的MSDN杂志中了解CLR如何创建运行时对象的文章Dril Into .NET Framework Internals讨论了由接口ID索引的进程级映射表IVMap.这是否意味着同一进程中的所有类型都具有指向同一IVMap的相同指针?

它还指出:

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

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

我还通过C#第3版阅读了CLR中的接口章节,但它没有谈到这一点.因此,这个问题的答案没有回答我的问题.

Jet*_*tti 23

.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从父级继承,1在当前类中实现).Flags之后的WORD是Start Slot,类加载器使用它来布局接口实现子表.

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

+----------------------------------+
| 0 - InterfaceInfo                |
+----------------------------------+
| 4 - Parent                       |
+----------------------------------+
| 5 - Current Class                |
+----------------------------------+
| 6 - Start Slot (2 Bytes)         |
+----------------------------------+
Run Code Online (Sandbox Code Playgroud)

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

findclass :
   if (!position == class) 
      findclass adjust offset by 8 and try again
Run Code Online (Sandbox Code Playgroud)

虽然它仍然是线性搜索,但实际上,由于迭代的数据大小不是很大,所以不会花费很长时间.我希望有所帮助.


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 |
+-------------------------+
Run Code Online (Sandbox Code Playgroud)

假设此域中只有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
Run Code Online (Sandbox Code Playgroud)

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


Luc*_*ski 21

那篇文章已经超过10年了,从那以后发生了很多变化.

IVMaps现已被Virtual Stub Dispatch取代.

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

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

我没有必要在这里复制这篇文章,但我只是说明要点及其含义:

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

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

当调度存根经常失败时,调用站点被认为是多态的,并且解析存根将返回修补调用站点以直接指向解析存根以避免一致失败的调度存根的开销.在同步点(当前是GC的结尾),在假设呼叫站点的多态属性通常是临时的情况下,多态站点将被随机提升回单态呼叫站点.如果这个假设对于任何特定的呼叫站点是不正确的,它将快速触发回程以将其再次降级为多态.

运行时对单态调用站点非常乐观,这在实际代码中很有意义,并且会尽可能地避免使用解析存根.