【问题标题】:Why are virtual thunks necessary?为什么需要虚拟 thunk?
【发布时间】:2017-06-06 18:43:44
【问题描述】:

这个问题是关于虚函数调用的(可能的)实现(我相信它被 gcc 使用)。

考虑以下场景:

  1. F 类继承自 D 类(可能还有其他),而 D 类继承自 B 类(不是虚拟的)。 D覆盖了B中声明的虚方法f();实例化一个 F 类型的对象
  2. F 类继承自 D 类(可能还有其他),而 D 类继承自 B 类(实际上)。 D覆盖了B中声明的虚方法f();实例化一个 F 类型的对象

(这两种情况的唯一区别是B类的继承方式)

在场景 1 中,在对象 B 的 vtable 中,在指向 f() 的位置现在有一个 (non-virtual) thunk 表示:

如果要调用f(),先将this指针改为offset

(实际上是 D 把这个 thunk 放在那里)

在场景 2 中,在对象 B 的 vtable 中,在指向 f() 的位置现在有一个 (virtual) thunk 表示:

如果要调用f(),首先将this指针更改为存储在addr的值

(D无法准确告诉B需要调整多少this指针,因为它不知道B对象在F对象最终内存布局中的位置)

这些假设是通过结合g++ -S 查看g++ -fdump-class-hierarchy 的输出得出的。他们是对的吗?

现在我的问题是:为什么需要虚拟 thunk?为什么 F 不能在 B 的虚拟表(在f() 的位置)中放置一个非虚拟 thunk?因为当需要实例化一个 F 对象时,编译器知道 f() 在 B 中声明,但它在 D 中被覆盖。并且它还知道对象 B(-in-F)和对象 D 之间的确切偏移量(-in-F) (我认为这首先是 virtual thunk 的原因)。

编辑(添加g++ -fdump-class-hierarchyg++ -S 的输出)

场景 1:

g++ -fdump-class-hierarchy:

F的Vtable

...

48 (int (*)(...))D::_ZThn8_N1D1fEv(de-mangled:非虚拟 thunk 到 D::f())

g++ -S:

_ZThn8_N1D1fEv

.LFB16:

.cfi_startproc

subq $8, %rdi #,

jmp .LTHUNK0 #

.cfi_endproc

场景 2:

g++ -fdump-class-hierarchy:

F的Vtable

...

64 (int (*)(...))D::_ZTv0_n24_N1D1fEv (de-mangled: virtual thunk to D::f())

g++ -S:

_ZTv0_n24_N1D1fEv

.LFB16:

.cfi_startproc

movq (%rdi), %r10 #,

addq -24(%r10), %rdi #,

jmp .LTHUNK0 #

.cfi_endproc

【问题讨论】:

  • 您能否用一个简洁的代码示例以及g++ -fdump-class-hierarchyg++ -S 的输出来说明您的问题。很难从你的散文中理解你的意思。另请注意,vtable 不是 C++ 标准概念,而是编译器特定的实现细节。
  • 我现在会尝试快速重新创建它。
  • 现代编译器现在非常擅长去虚拟化函数。但有时,如果有超出他们控制范围的外部功能依赖于虚拟性,他们就无法做到。
  • @πάνταῥεῖ 当问题包括三个不同的实现特定标签(g++、vtable 和 thunk 不是 C++ 概念)。
  • @curiousguy 我不知道:-P

标签: c++ g++ vtable thunk


【解决方案1】:

我想我找到了答案here

"...根据上述信息,thunk 有几种可能的实现方式。注意以下我们假设在调用任何 vtable 条目之前,已将 this 指针调整为指向与 vtable 对应的子对象从中获取 vptr。

A.由于偏移量在编译时总是已知的,即使对于虚拟基础,每个 thunk 都可能是不同的,将已知偏移量添加到此并分支到目标函数。 这将导致每个覆盖器在不同的偏移量处产生一个 thunk。因此,每次在代码中的任何给定点更改引用的实际类型时,都会发生分支错误预测并可能发生指令缓存未命中。

B.在虚拟继承的情况下,尽管在声明覆盖器时知道偏移量,但可能会根据覆盖器类的派生而有所不同。上面的 H 和 I 是最简单的例子。 H 是 I 的主要基础,但 I 的 int 成员意味着 A 与 I 中的 H 的偏移量与独立 H 的偏移量不同。因此,ABI 指定虚拟基础 A 的辅助 vtable包含到 H 的 vcall 偏移量,以便共享 thunk 可以加载 vcall 偏移量,将其添加到 this 中,然后分支到目标函数 H::f。 这将导致更少的 thunk,因为对于 A 是 H 的虚拟基并且 H::f 覆盖 A::f 的继承层次结构,较大层次结构中的所有 H 实例都可以使用相同的 thunk。因此,这些 thunk 将导致更少的分支错误预测和指令缓存未命中。权衡是他们必须在添加偏移量之前进行加载。由于偏移量小于 thunk 的代码,因此缓存中的加载丢失频率应该更低,因此更好的缓存未命中行为应该会产生更好的结果,尽管 vcall 偏移加载需要 2 个或更多周期......"

似乎虚拟 thunk 仅出于性能原因而存在。如果我错了,请纠正我。

【讨论】:

    猜你喜欢
    • 2021-04-28
    • 1970-01-01
    • 2013-01-10
    • 2013-10-21
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2011-03-01
    相关资源
    最近更新 更多