关于c#:CLR实现对接口成员的虚方法调用

CLR implementation of virtual method calls to interface members

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

我知道clr为每种类型维护的vtable(每个方法都有方法槽),以及每个接口都有一个指向相关接口方法实现的方法槽的额外列表。但我不理解以下内容:clr如何有效地确定要从类型的vtable中选择哪个接口方法slot列表?

本文深入到.NET框架内部,了解clr如何从2005年5月的msdn杂志上创建运行时对象,讨论了一个按接口ID索引的进程级映射表ivmap。这是否意味着同一进程中的所有类型都有指向同一个ivmap的相同指针?

它还指出:

If MyInterface1 is implemented by two classes, there will be two
entries in the IVMap table. The entry will point back to the beginning
of the sub-table embedded within the MyClass method table.

CLR如何知道要选择哪个条目?它是否执行线性搜索以查找与当前类型匹配的条目?或者二进制搜索?或者某种直接索引,并在其中包含可能多个空条目的映射?

我还阅读了通过C第三版的clr中有关接口的章节,但它没有讨论这个问题。因此,另一个问题的答案不能回答我的问题。


.NET Stack

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

Does this mean that all types in the same process have the same pointer to the same IVMap?

是的,因为它是域级别的,所以它意味着AppDomain中的所有内容都具有相同的IVMAP。

How does the CLR know which entry to pick? Does it do a linear search to find the entry that matches the current type? Or a binary search? Or some kind of direct indexing and have a map with possibly many empty entries in it?

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

再次从您链接的网站:

Each interface implementation will have an entry in IVMap. If MyInterface1 is implemented by two classes, there will be two entries in the IVMap table. The entry will point back to the beginning of the sub-table embedded within the MyClass method table

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

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

同样,从操作中的链接:

The first 4 bytes of the first InterfaceInfo entry points to the TypeHandle of MyInterface1 (see Figure 9 and Figure 10). The next WORD (2 bytes) is taken up by Flags (where 0 is inherited from parent, and 1 is implemented in the current class). The WORD right after Flags is Start Slot, which is used by the class loader to lay out the interface implementation sub-table.

这里我们有一个表,数字是字节的偏移量。这只是IVMAP中的一个记录:

1
2
3
4
5
6
7
8
9
+----------------------------------+
| 0 - InterfaceInfo                |
+----------------------------------+
| 4 - Parent                       |
+----------------------------------+
| 5 - Current Class                |
+----------------------------------+
| 6 - Start Slot (2 Bytes)         |
+----------------------------------+

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

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

虽然它仍然是一个线性搜索,但实际上,只要正在迭代的数据的大小不太大,就不需要这么长时间。希望有帮助。

编辑2:

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

IVMap is created based on the Interface Map information embedded within the method table. Interface Map is created based on the metadata of the class during the MethodTable layout process. Once typeloading is complete, only IVMap is used in method dispatching.

所以类的ivmap只加载特定类继承的接口。它看起来像是从域ivmap复制的,但只保留指向的接口。这又引出了另一个问题,如何?很有可能,它等同于C++如何在每个条目都有偏移量的情况下进行VTABLE,并且接口映射提供了在IVMAP中包含的偏移量列表。

如果我们查看整个域的ivmap:

1
2
3
4
5
6
7
8
9
+-------------------------+
| Slot 1 - YourInterface  |
+-------------------------+
| Slot 2 - MyInterface    |
+-------------------------+
| Slot 3 - MyInterface2   |
+-------------------------+
| Slot 4 - YourInterface2 |
+-------------------------+

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

假设每个插槽为8个字节,插槽1从0开始,因此如果我们想要获得插槽2和3,我们将执行如下操作:

1
2
3
4
5
6
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,因为我不太熟悉它,但我试图复制他们在文章中的链接。


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

现在,虚拟存根调度已取代了ivmaps。

Virtual stub dispatching (VSD) is the technique of using stubs for virtual method invocations instead of the traditional virtual method table. In the past, interface dispatch required that interfaces had process-unique identifiers, and that every loaded interface was added to a global interface virtual table map.

去读那篇文章吧,它有更多你需要知道的细节。它来自运行时手册,最初是由clr devs为clr devs编写的文档,但现在已经为每个人发布了。它基本上描述了运行时的内部结构。

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

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

这里有一个重要的考虑,直接从文章开始:

When a dispatch stub fails frequently enough, the call site is deemed to be polymorphic and the resolve stub will back patch the call site to point directly to the resolve stub to avoid the overhead of a consistently failing dispatch stub. At sync points (currently the end of a GC), polymorphic sites will be randomly promoted back to monomorphic call sites under the assumption that the polymorphic attribute of a call site is usually temporary. If this assumption is incorrect for any particular call site, it will quickly trigger a backpatch to demote it to polymorphic again.

运行时对单态调用站点非常乐观,这在真正的代码中很有意义,它将尽可能避免解析存根。


从您链接的第一篇文章开始:

If MyInterface1 is implemented by two classes, there will be two
entries in the IVMap table. The entry will point back to the beginning
of the sub-table embedded within the MyClass method table, as shown in
Figure 9

The ClassLoader walks through the metadata of the current class,
parent class, and interfaces, and creates the method table. In the
layout process, it replaces any overridden virtual methods, replaces
any parent class methods being hidden, creates new slots, and
duplicates slots as necessary. The duplication of slots is necessary
to create an illusion that each interface has its own mini vtable.
However, the duplicated slots point to the same physical
implementation.

这向我表明,接口的ivmap中有一些条目是通过类名(或某些等效项)键控的,指向类的vtable的一个子部分,而vtable基本上具有实现该接口的每个类方法的重复实现,并由指向与类自己的vtable entrie相同物理实现的指针支持。S.

但可能是完全错误的。