替代虚拟机制的实现?

C ++通过虚拟机制支持动态绑定。 但是据我了解,虚拟机制是编译器的实现细节,而该标准仅指定了在特定情况下应发生的行为。 大多数编译器通过虚拟表和虚拟指针来实现虚拟机制。 是的,我知道这是如何工作的,所以我的问题不是关于虚拟指针和表的实现细节。 我的问题是:

  1. 除了虚拟指针和虚拟表机制以外,是否有任何编译器以其他方式实现虚拟机制? 据我所见,大多数(阅读g ++,Microsoft Visual Studio)都是通过虚拟表,指针机制来实现的。 那么实际上还有其他编译器实现吗?
  2. 任何仅具有虚拟功能的类的sizeof将是该编译器上的指针大小(this中的vptr),因此,鉴于虚拟ptr和tbl机制本身就是编译器实现,我在上面所作的声明是否总是正确的?
Alok Save asked 2020-02-19T10:55:42Z
11个解决方案
21 votes

对象中的vtable指针始终是最有效的,这不是真的。 我的另一种语言的编译器出于类似的原因曾经使用对象内指针,但不再使用:而是使用单独的数据结构将对象地址映射到所需的元数据:在我的系统中,这恰好是形状信息以供使用 由垃圾收集器。

对于单个简单对象,此实现会花费更多的存储空间;对于具有多个基数的复杂对象,此实现会更有效;对于数组,由于对映射表中的数组中的所有对象都只需要一个条目,因此对数组的效率将大大提高。 我的特定实现还可以找到给定指向对象内部任何点的指针的元数据。

实际查找速度非常快,并且存储要求非常适中,因为我使用的是地球上最好的数据结构:Judy数组。

我也知道没有C ++编译器使用除vtable指针以外的任何东西,但这不是唯一的方法。 实际上,具有基类的初始化语义会使任何实现变得混乱。 这是因为在构造对象时,必须完整地查看类型。 这些语义的结果是,复杂的混合对象导致生成大量vtable,大型对象和缓慢的对象初始化。 这可能不是vtable技术的结果,而是需要严格遵循子对象的运行时类型始终正确的要求。 实际上,在构造过程中没有充分的理由,因为构造函数不是方法,并且不能明智地使用虚拟调度:因为析构函数是真实的方法,因此对于销毁我来说,这不是很清楚。

Yttrill answered 2020-02-19T11:00:29Z
7 votes

据我所知,所有C ++实现都使用vtable指针,尽管在对象中保留较小的类型索引(1-2 B)非常容易(并且可能不会像您认为的给定缓存那样糟糕)。 随后通过小的表查找获得vtable和类型信息。

另一种有趣的方法可能是BIBOP([http://foldoc.org/BIBOP)]-大量的页面-尽管C ++可能会遇到问题。 想法:将相同类型的对象放在页面上。 通过简单地去掉对象指针的低有效位,就可以得到指向页面顶部类型描述符/ vtable的指针。 (当然,对于堆栈上的对象来说效果不佳!)

另一种方法是在对象指针本身中编码某些类型的标签/索引。 例如,如果通过构造将所有对象对齐16字节,则可以使用4个LSB在其中放置4位类型标签。 (还不够。)或者(特别是对于嵌入式系统而言),如果您保证地址中未使用的更重要的位,则可以在其中放置更多的标记位,并使用移位和掩码对其进行恢复。

尽管这两种方案对于其他语言实现都很有趣(有时会用到),但它们对于C ++来说却是有问题的。 某些C ++语义(例如在(基类)对象构造和销毁过程中调用哪些基类虚函数重写)将您带入一个模型,在该模型中,您输入基类ctor / dtor时会修改对象的某些状态。

您可能会发现有关Microsoft C ++对象模型实现的旧教程很有趣。[http://www.openrce.org/articles/files/jangrayhood.pdf]

骇客骇客!

Jan Gray answered 2020-02-19T11:01:15Z
5 votes

除了虚拟指针和虚拟表机制以外,是否有任何编译器以其他方式实现虚拟机制? 据我所见,大多数(阅读g ++,Microsoft Visual Studio)都是通过虚拟表,指针机制来实现的。 那么实际上还有其他编译器实现吗?

我知道的所有当前编译器都使用vtable机制。

这是可能的优化,因为C ++是静态类型检查的。

相反,在一些更动态的语言中,动态搜索基类链,从对象的最大派生类开始搜索虚拟调用的成员函数的实现。 例如,这就是原始Smalltalk中的工作方式。 C ++标准描述了虚拟调用的效果,就像使用了这种搜索一样。

在1990年代的Borland / Turbo Pascal中,这种动态搜索被用于查找Windows API“窗口消息”的处理程序。 我认为Borland C ++可能也一样。 它是仅用于消息处理程序的常规vtable机制的补充。

如果我不记得它是在Borland / Turbo C ++中使用的,那么它是对语言扩展的支持,该扩展使您可以将消息ID与消息处理程序功能相关联。

任何仅具有虚拟函数的类的sizeof都将是该编译器上的指针(在this内的vptr)的大小,因此,鉴于虚拟ptr和tbl机制本身就是编译器实现,我在上面所作的声明是否总是正确的?

正式为否(即使假设使用vtable机制),也取决于编译器。 由于该标准不需要vtable机制,因此它对在每个对象中放置vtable指针一无所知。 其他规则允许编译器在末尾自由添加填充(未使用的字节)。

但是实际上。 ;-)

但是,这不是您应该依赖的东西,也不是您需要依赖的东西。 但是在另一个方向上,您可以要求这样做,例如,如果要定义ABI。 然后,任何不符合您要求的编译器都将完全不符合您的要求。

干杯,……

Cheers and hth. - Alf answered 2020-02-19T11:02:23Z
5 votes
  1. 我认为除了vptr / vtable之外,没有其他现代编译器具有其他方法。 确实,很难找出其他不仅效率低下的东西。

    但是,在这种方法中,仍然存在很大的设计折衷空间。 也许特别是关于如何处理虚拟继承。 因此,使此实现定义很有意义。

    如果您对这种东西感兴趣,我强烈建议您阅读C ++对象模型内部。

  2. sizeof class取决于编译器。 如果您想要可移植的代码,请不要做任何假设。

Johan Kotlinski answered 2020-02-19T11:02:58Z
4 votes

在尝试设想一个替代方案时,我根据Yttril的回答提出了以下方案。 据我所知,没有编译器使用它!

给定足够大的虚拟地址空间和灵活的OS内存分配例程,则sizeof可以在固定的,不重叠的地址范围内分配不同类型的对象。 然后可以使用右移操作从其地址快速推断出对象的类型,并将结果用于索引vtable表,从而为每个对象节省1个vtable指针。

乍一看,此方案可能会遇到堆栈分配对象的问题,但这可以干净地处理:

  1. 对于每个分配给堆栈的对象,编译器会添加代码,这些代码会在创建对象时将记录添加到sizeof对的全局数组中,并在销毁记录时将其删除。
  2. 包含堆栈的地址范围将映射到一个包含大量thunk的vtable,该thump读取sizeof指针,扫描数组以查找该地址处对象的对应类型(vptr),然后在vtable中调用对应的方法 指向。 (即第42个thunk将在vtable中调用第42个方法-如果任何类中使用的最虚拟函数是sizeof (vptr),则至少需要this thunk。)

对于基于堆栈的对象的虚拟方法调用,此方案显然会产生不小的开销(对于查找至少为O(log n))。 在没有基于堆栈的对象的数组或组成(包含在另一个对象中)的情况下,可以使用一种更简单,更快速的方法,其中将vptr放置在紧接对象之前的堆栈上(请注意,它不被视为对象的一部分)。 对象,并且对sizeof的尺寸没有贡献)。 在这种情况下,重击只需将this中的sizeof (vptr)减去即可找到要使用的正确vptr,然后像以前一样转发。

j_random_hacker answered 2020-02-19T11:03:43Z
4 votes

IIRC Eiffel使用不同的方法,并且该方法的所有替代最终都在一个地址中合并并编译,并带有在其中检查对象类型的序言(因此,每个对象都必须具有类型ID,但这不是指向VMT的指针)。 对于C ++,这当然需要在链接时创建最终函数。但是,我不知道任何使用这种方法的C ++编译器。

6502 answered 2020-02-19T11:04:03Z
3 votes
  1. 我从未听说过或看到过使用任何其他实现的编译器。 vtable之所以如此受欢迎,是因为它不仅是最有效的实现,而且还是最简单的设计和最明显的实现。

  2. 在几乎所有您想使用的编译器上,这几乎都是正确的。 但是,这并不能保证也并非总是如此-尽管几乎总是如此,但您不能依赖它。 您最喜欢的编译器还可以在不告诉您的情况下更改其对齐方式,增加其大小,以达到有趣的目的。 它也可以从内存中插入任何调试信息和喜欢的信息。

Puppy answered 2020-02-19T11:04:29Z
3 votes

除了虚拟指针和虚拟表机制以外,是否有任何编译器以其他方式实现虚拟机制? 据我所见,大多数(阅读g ++,Microsoft Visual Studio)都是通过虚拟表,指针机制来实现的。 那么实际上还有其他编译器实现吗?

我不知道C ++编译器在使用什么,尽管您可能会发现阅读二叉树调度很有趣。 如果您有兴趣以任何方式利用虚拟调度表的期望,则应注意,编译器可以-在编译时知道类型的地方-有时可以在编译时解析虚拟函数调用,因此可能不参考该表。

任何仅具有虚拟函数的类的sizeof都将是该编译器上的指针(在this内的vptr)的大小,因此,鉴于虚拟ptr和tbl机制本身就是编译器实现,我在上面所作的声明是否总是正确的?

假设没有具有自己的虚拟成员的基类,也没有虚拟基类,那么绝大多数情况是正确的。 可以设想替代方案-例如,整个程序分析仅显示类层次结构中的一个成员,以及切换到编译时分派。 如果需要运行时调度,很难想象为什么任何编译器都会引入进一步的间接寻址。 尽管如此,标准还是没有明确规定这些事情,因此实现可能会有所不同,或者将来会有所不同。

Tony Delroy answered 2020-02-19T11:05:05Z
3 votes

C ++ / CLI不同于这两个假设。 如果定义ref类,则根本不会将其编译为机器代码。 而是,编译器将其编译为.NET托管代码。 在中间语言中,类是内置功能,虚拟方法集是在元数据中定义的,而不是在方法表中定义的。

实现对象布局和分发的特定策略取决于VM。 在Mono中,仅包含一个虚拟方法的对象没有一个指针的大小,但是在MonoObject结构中需要两个指针。 第二个用于对象同步。 由于这是实现定义的,并且实际上也没有用,因此C ++ / CLI中的ref类不支持sizeof。

Martin v. Löwis answered 2020-02-19T11:05:32Z
0 votes

Tony D的答案正确地指出,允许编译器使用整个程序分析,以对唯一可能的函数实现的静态调用替换虚拟函数调用。 或将obj->method()编译为

if (auto frobj = dynamic_cast<FrequentlyOccurringType>(obj)) {
    frobj->FrequentlyOccurringType::method();  // static dispatch on hot path
} else {
    obj->method();  // vtable dispatch on cold path
}

Karel Driesen和UrsHölzle在1996年写了一篇非常有趣的论文,他们在典型的C ++应用程序上模拟了完美的整个程序优化的效果:“ C ++中虚拟函数调用的直接成本”。 (如果需要Google提供,则可免费获得PDF。)不幸的是,它们仅以vtable分发为基准,而以静态分发为基准。 他们没有将其与二叉树分发进行比较。

他们确实指出,当您谈论支持多重继承的语言(例如C ++)时,实际上有两种vtable。 通过多重继承,当您调用从第二个基类继承的虚拟方法时,您需要“修复”对象指针,使其指向第二个基类的实例。 该修正偏移量可以作为数据存储在vtable中,也可以作为代码存储在“ thunk”中。 (有关更多详细信息,请参见本文。)

我相信这些天所有不错的编译器都使用thunk,但是要达到100%的市场渗透率却需要10到20年。

Quuxplusone answered 2020-02-19T11:06:39Z
0 votes

首先,提到了Borland对C ++的专有扩展,即动态调度虚拟表(DDVT),您可以在名为DDISPATC.ZIP的文件中阅读有关它的内容。 Borland Pascal同时使用了虚拟方法和动态方法,Delphi引入了另一种“消息”语法,类似于动态,但用于消息。 在这一点上,我不确定Borland C ++是否具有相同的功能。 Pascal或Delphi中都没有多重继承,因此Borland C ++ DDVT可能不同于Pascal或Delphi。

其次,在1990年代或更早的时候,人们尝试了不同的对象模型,而Borland并不是最先进的对象模型。 我个人认为关闭IBM SOMobjects对我们所有人仍然遭受的世界造成了破坏。 在关闭SOM之前,已经进行了Direct-to-SOM C ++编译器的实验。 因此,不是使用C ++的方法调用方式,而是使用SOM。 它在许多方面类似于C ++ vtable,但有一些例外。 首先,为防止脆弱的基类问题,程序不使用vtable内部的偏移量,因为它们不知道此偏移量。 如果基类引入了新方法,则可以更改。 而是,调用者调用在运行时创建的thunk,该thunk在其汇编代码中具有此知识。 还有另外一个区别。 在C ++中,当使用多重继承时,一个对象可以包含多个VMT IIRC。 与C ++相比,每个SOM对象只有一个VMT,因此调度代码应不同于“ call dword ptr [VMT + offset]”。

有一个与SOM有关的文档,即SOM中的发布至发行版二进制兼容性。 您可以找到SOM与我不了解的其他项目的比较,例如Delta / C ++和Sun OBI。 他们解决了SOM解决的问题的子集,并且这样做了,他们还对发票代码进行了一些调整。

我最近发现用于Windows编译器的Visual Age C ++ v3.5片段足以使事情运行并真正实现。 大多数用户不太可能仅使用DTS C ++来获得OS / 2 VM,但是拥有Windows编译器则完全是另一回事。 VAC v3.5是第一个也是最后一个支持Direct-to-SOM C ++功能的版本。 VAC v3.6.5和v4.0不适用。

  1. 从IBM FTP下载VAC 3.5 fixpak 9。 这个fixpak包含许多文件,因此您甚至不需要完整的编译器(我有3.5.7发行版,但是fixpak 9足以进行一些测试)。
  2. 打开包装到e。 G。 C:\ home \ OCTAGRAM \ DTS
  3. 启动命令行并在此运行后续命令
  4. 运行:设置SOMBASE = C:\ home \ OCTAGRAM \ DTS \ ibmcppw
  5. 运行:C:\ home \ OCTAGRAM \ DTS \ ibmcppw \ bin \ SOMENV.BAT
  6. 运行:cd C:\ home \ OCTAGRAM \ DTS \ ibmcppw \ samples \ compiler \ dts
  7. 运行:nmake clean
  8. 运行:nmake
  9. hhmain.exe和它的dll位于不同的目录中,因此我们必须使它们以某种方式相互找到; 由于进行了几次实验,因此执行了一次“设置PATH =%PATH%; C:\ home \ OCTAGRAM \ DTS \ ibmcppw \ samples \ compiler \ dts \ xhmain \ dtsdll”,但是您可以将dll复制到hhmain附近。 可执行程序
  10. 运行:hhmain.exe

我这样输出:

Local anInfo->x = 5
Local anInfo->_get_x() = 5
Local anInfo->y = A
Local anInfo->_get_y() = B
{An instance of class info at address 0092E318

}
OCTAGRAM answered 2020-02-19T11:08:05Z
translate from https://stackoverflow.com:/questions/4352032/alternative-virtual-mechanism-implementations