use*_*768 3 c++ g++ vtable thunk
这个问题是关于虚函数调用的(可能的)实现(我相信它被使用gcc)。
考虑以下场景:
f()D 重写B 中声明的虚方法;实例化 F 类型的对象f()D 重写B 中声明的虚方法;实例化 F 类型的对象(这两种场景唯一的区别是B类的继承方式)
在场景 1 中,在对象 B 的 vtable 中,在目标位置处f()现在有一个(非虚拟)thunk 表示:
如果你想调用
f(),首先this用offset
(实际上是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
我想我在这里找到了答案:
“...给定上述信息,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的存在只是出于性能原因。如果我错了,请纠正我。
| 归档时间: |
|
| 查看次数: |
950 次 |
| 最近记录: |