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

问题描述:

出于好奇:CLR如何调用虚拟方法调用接口成员到正确的实现?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#第3版阅读了CLR中的接口一章,但它没有谈论这个。因此,this other question的答案不回答我的问题。

+0

问题中的链接不再指向特定文章。请你能告诉我们该文章出现在(月份和年份)的哪个问题? – buffjape 2015-12-14 14:57:48

+1

@buffjape这是[MSDN magazine](https://msdn.microsoft.com/en-us/magazine/ee310108.aspx)2005年5月刊。我更新了链接以指向互联网存档。 – Virtlink 2015-12-15 10:05:49

+0

请不要奖励一个巨大的赏金,即使在发布的时候,也不相关的严重过时的复制粘贴答案。这对任何人都没有帮助。 – 2016-10-30 12:09:29

.NET Stack

如果你看看图,这是链接的网站上,它可能更容易理解。

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

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

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

这些类都是用偏移量来布局的,所以每个类都有一个相对设置的区域。这使得查找方法更容易。它将搜索IVMap表并从界面中找到该方法。从那里,它进入MethodSlotTable并使用该类的接口实现。类的接口映射包含元数据,但是,实现与其他方法一样被处理。

再次从该网站您链接:

每个接口的实现将在IVMap的条目。如果MyInterface1由两个类实现,则IVMap表中将有两个条目。该项目将重新指向嵌入MyClass的方法表中的子表的开始

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


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

再次,从在OP的链接:

前4个字节的第一InterfaceInfo入口点MyInterface1的类型句柄(参见图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个字节长,我们需要做的是这样的:(伪代码)

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

虽然它仍然是一个线性搜索,在现实中,它是不会采取这种只要正在迭代的数据大小并不是很大。我希望有所帮助。


EDIT2:

所以看图表,不知道为什么会出现在IVMap为图中的类没有插槽1我重新阅读部分,发现这个后:

IVMap基于嵌入在方法表中的接口映射信息创建。接口映射是在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熟悉它,但我试图复制它们链接到的文章中的内容。从那时起

+0

我不会跟随偏移量的偏移量,所以每个东西都有一个相对设置的区域,它将会在哪里。但是,是的,我可以想象,CLR通过IVMap进行线性搜索,正如我的帖子所述。但我想知道它在实践中如何运作。当然,对于像经常发生的界面方法调用这样的事情来说,做一个线性搜索非常幼稚。 – Virtlink 2012-03-21 19:54:57

+0

@Virtlink如果你看看上面的图片,你会注意到有一个基于方法表的堆的数字。所以GCInfo在-12,基本实例大小是在4等等。使用这些设置,标准化的偏移量,你可以通过像'dword ptr [eax + 0Ch]'' – Jetti 2012-03-21 19:59:22

+0

那样找到字段。好吧,是的,你是正确的。但我从来没有在我的问题的任何地方提到过_fields_这个词:)。我想知道IVMap是什么样子的,以及CLR如何使用它从已知的接口+方法槽索引以及对象类型的VTable中包含接口方法槽的子表的对象引用。 – Virtlink 2012-03-21 20:04:09

从你链接的第一篇文章:

如果MyInterface1由两个类来实现,也将在IVMap表中的两个 条目。该条目将指向回到开始嵌入MyClass的方法表中的子表的 ,如图 图9

的类加载器遍历当前类的元数据, 父类和接口,并创建方法表。在 布局过程中,它将替换任何重写的虚拟方法,替换 隐藏的任何父类方法,创建新的插槽,并根据需要复制插槽。插槽的重复是必要的 创建一个错觉,每个接口都有自己的小虚拟表。 但是,重复的插槽指向相同的物理实现。

这表明,我认为该接口的IVMap具有指向类的虚函数表,基本上都有各自的实现该接口的类的方法重复实现的款类名(或同等学历)键条目,并指向与课程自己的vtable条目相同的物理实现。

虽然可能是完全错误的。

+0

如果您完全阅读我的文章,您会看到我已经引用了您所做的同样的引语,并且我知道VTable如何为每个接口指定方法实现的子节。我的问题具体是CLR如何知道使用哪个小节。 – Virtlink 2012-03-21 19:47:17

这篇文章是超过10岁,很多已经改变。

IVMaps现已被Virtual Stub Dispatch代替

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

去阅读那篇文章,它有更多的细节你永远需要知道。它来自Book of the Runtime,这是文件最初写的CLR开发者对开发者CLR,但现在已经被公布给大家。它基本上描述了运行时的胆量。

没有意义,对我来说,这里复制的文章,但我只是陈述要点和它们意味着什么:

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

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

当调度存根无法足够频繁,调用点被认为是多态和决心存根将支持修补通话网站直接指向解决方案存根以避免始终发生失败的调度存根的开销。在同步点(目前是GC的结尾),假设呼叫站点的多态属性通常是暂时的,则多态站点将被随机地提升回单形呼叫站点。如果这个假设对于任何特定的呼叫站点都是不正确的,那么它会很快触发一次派遣将其降级为多态。

运行时是真的看好约单态调用点,这让很多的意义上真正的代码,它会尽量避免决心存根尽可能。

+0

@布鲁诺谢谢! :) – 2016-11-03 08:45:00