2

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

考虑以下场景:

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

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

在场景 1 中,在对象 B 的 vtable 中,在目的地的位置f()现在有一个(非虚拟的)thunk 表示:

如果你想打电话f(),首先改变this指针offset

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

在场景 2 中,在对象 B 的 vtable 中,在目的地的位置f()现在有一个(虚拟)thunk,它说:

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

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

这些假设是通过查看与g++ -fdump-class-hierarchy结合的输出而做出的g++ -S。他们是正确的吗?

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

编辑(添加g++ -fdump-class-hierarchy和的输出g++ -S

场景一:

g++ -fdump-class-hierarchy

F 的 Vtable

...

48 (int (*)(...))D:: _ZThn8_N1D1fEv (de-mangled: non-virtual thunk to D::f())

g++ -S

_ZThn8_N1D1fEv

.LFB16:

.cfi_startproc

subq $8, %rdi #,

jmp .LTHUNK0 #

.cfi_endproc

场景二:

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

4

1 回答 1

3

我想我在这里找到了答案:

“...给定上述信息,thunk 有几种可能的实现。请注意,在下文中,我们假设在调用任何 vtable 条目之前,此指针已被调整为指向对应于 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 仅出于性能原因而存在。如果我错了,请纠正我。

于 2017-06-07T08:54:42.823 回答