在C#和Java实现中,对象通常只有一个指向其类的指针。这是可能的,因为它们是单继承语言。然后,类结构包含单继承层次结构的vtable。但是调用接口方法也具有多重继承的所有问题。通常可以通过将所有已实现接口的附加vtable放入类结构中来解决此问题。与C ++中的典型虚拟继承实现相比,这节省了空间,但使接口方法的分派更加复杂-可以通过缓存部分地进行补偿。
例如,在OpenJDK JVM中,每个类都包含用于所有已实现接口的vtable数组(接口vtable称为itable)。调用接口方法时,将在该数组中线性搜索该接口的itable,然后可以通过该itable调度该方法。使用缓存是为了使每个调用站点都能记住方法分配的结果,因此仅当具体对象类型发生更改时才需要重复此搜索。用于方法分派的伪代码:
// Dispatch SomeInterface.method
Method const* resolve_method(
Object const* instance, Klass const* interface, uint itable_slot) {
Klass const* klass = instance->klass;
for (Itable const* itable : klass->itables()) {
if (itable->klass() == interface)
return itable[itable_slot];
}
throw ...; // class does not implement required interface
}
(比较OpenJDK HotSpot 解释器或x86编译器中的真实代码。)
C#(或更准确地说,CLR)使用一种相关的方法。但是,这里的itables不包含指向方法的指针,而是插槽映射:它们指向类的主vtable中的条目。与Java一样,必须搜索正确的itable只是最坏的情况,并且可以预期,在调用站点进行缓存可以几乎始终避免这种搜索。CLR使用一种称为虚拟存根调度的技术,以使用不同的缓存策略来修补JIT编译的机器代码。伪代码:
Method const* resolve_method(
Object const* instance, Klass const* interface, uint interface_slot) {
Klass const* klass = instance->klass;
// Walk all base classes to find slot map
for (Klass const* base = klass; base != nullptr; base = base->base()) {
// I think the CLR actually uses hash tables instead of a linear search
for (SlotMap const* slot_map : base->slot_maps()) {
if (slot_map->klass() == interface) {
uint vtable_slot = slot_map[interface_slot];
return klass->vtable[vtable_slot];
}
}
}
throw ...; // class does not implement required interface
}
与OpenJDK伪代码的主要区别在于,在OpenJDK中,每个类都有一个数组,这些数组包含所有直接或间接实现的接口,而CLR仅保留该类中直接实现的接口的插槽映射数组。因此,我们需要将继承层次结构向上移动,直到找到插槽映射。对于深度继承层次结构,这可以节省空间。由于泛型的实现方式,这些在CLR中特别相关:对于泛型专门化,将复制类结构,并且主vtable中的方法可能会被专门化代替。插槽映射继续指向正确的vtable条目,因此可以在一个类的所有通用专长之间共享。
最后,实现接口调度还有更多的可能性。无需将vtable / itable指针放在对象或类结构中,我们可以使用指向对象的胖指针,它们基本上是(Object*, VTable*)
一对。缺点是指针大小增加了一倍,并且上载(从具体类型到接口类型)不是免费的。但是它更灵活,间接性更少,也意味着可以从类的外部实现接口。Go接口,Rust特征和Haskell类型类使用了相关的方法。
参考资料和进一步阅读:
- Wikipedia:内联缓存。讨论可用于避免昂贵的方法查找的缓存方法。基于vtable的分派通常不需要,但是对于像上述接口分派策略这样的更昂贵的分派机制非常理想。
- OpenJDK Wiki(2013):接口调用。讨论实用性。
- Pobar,Neward(2009):SSCLI 2.0 Internals。本书的第5章详细讨论了插槽图。从未出版过,但是由作者在他们的博客上提供。此后,PDF链接已移动。这本书可能不再反映CLR的当前状态。
- CoreCLR(2006):虚拟存根调度。在:运行时手册。讨论插槽映射和缓存以避免昂贵的查找。
- Kennedy,Syme(2001):. NET公共语言运行时的泛型设计和实现。(PDF链接)。讨论实现泛型的各种方法。泛型与方法分派交互,因为方法可能是专门的,所以可能必须重写vtables。