为什么需要虚拟重击?

use*_*768 3 c++ g++ vtable thunk

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

考虑以下场景:

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

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

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

如果你想调用f(),首先thisoffset

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

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

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

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

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

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

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

场景一:

g++ -fdump-class-hierarchy

F 的 V 表

...

48 (int (*)(...))D:: _ZThn8_N1D1fEv(去破坏:非虚拟重击到 D::f())

g++ -S

_ZThn8_N1D1fEv :

.LFB16:

.cfi_startproc

子q $8,%rdi #,

jmp .LTHUNK0 #

.cfi_endproc

场景2:

g++ -fdump-class-hierarchy

F 的 V 表

...

64 (int (*)(...))D:: _ZTv0_n24_N1D1fEv(去重整:虚拟 thunk 到 D::f())

g++ -S

_ZTv0_n24_N1D1fEv :

.LFB16:

.cfi_startproc

movq (%rdi), %r10 #,

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

jmp .LTHUNK0 #

.cfi_endproc

use*_*768 5

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

“...给定上述信息,thunk 有几种可能的实现。请注意,下面我们假设在调用任何 vtable 条目之前,this 指针已被调整为指向与 vtable 对应的子对象,其中vptr 已获取。

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

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

看来虚拟thunk的存在只是出于性能原因。如果我错了,请纠正我。