这个问题是关于虚拟函数调用的(可能的)实现(我认为它由gcc
使用)。
请考虑以下情况:
f()
; F类型的对象被实例化f()
; 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的虚拟表中放置非虚拟 thunk(在f()
的位置)?因为当需要实例化F对象时,编译器知道在B中声明了f()
,但它在D中被覆盖了。它还知道对象B(-in-F)和对象B之间的确切偏移量。对象D(-in-F)(我认为首先是虚拟 thunk的原因)。
编辑(添加了g++ -fdump-class-hierarchy
和g++ -S
的输出)
情景1:
g++ -fdump-class-hierarchy
:
F表格
...
48(int(*)(...))D :: _ZThn8_N1D1fEv (去除错误:非虚拟thunk到D :: f())
g++ -S
:
_ZThn8_N1D1fEv :
.LFB16:
.cfi_startproc
subq $ 8,%rdi#,
jmp .LTHUNK0#
.cfi_endproc
情景2:
g++ -fdump-class-hierarchy
:
F表格
...
64(int(*)(...))D :: _ZTv0_n24_N1D1fEv (de-mangled:虚拟thunk到D :: f())
g++ -S
:
_ZTv0_n24_N1D1fEv :
.LFB16:
.cfi_startproc
movq(%rdi),%r10#,
addq -24(%r10),%rdi#,
jmp .LTHUNK0#
.cfi_endproc
答案 0 :(得分:3)
我想我找到了答案here:
" ...鉴于上述信息,有几种可能的实施方式。请注意,在下面我们假设在调用任何vtable条目之前,已经调整了this指针以指向与从中提取vptr的vtable对应的子对象。
一个。由于偏移总是在编译时已知,即使对于虚拟基础,每个thunk也可能是不同的,将已知偏移添加到此并分支到目标函数。 这将导致每个覆盖者在不同的偏移处产生一个thunk。因此,每次在代码中的任何给定点为参考更改实际类型时,都会发生分支错误预测和可能的指令高速缓存未命中。
B中。在虚拟继承的情况下,虽然在声明覆盖器时已知,但是偏移量可能根据来自覆盖器类的派生而不同。上面的H和I是最简单的例子。 H是I的主要基础,但I的int成员意味着A在I中与H的偏移量与从独立H中的H不同。因此,ABI指定虚拟基础A的辅助vtable包含到H的vcall偏移量,以便共享thunk可以加载vcall偏移量,将其添加到此,并分支到目标函数H :: f。 这将导致更少的thunk,因为对于继承层次结构,其中A是H的虚拟基础,并且H :: f重写A :: f,更大层次结构中的H的所有实例都可以使用相同的thunk。结果,这些thunk将导致更少的分支错误预测和指令缓存未命中。权衡是他们必须在偏移添加之前进行加载。由于偏移量小于thunk的代码,因此负载应该不经常在缓存中丢失,因此尽管vcall偏移负载需要2个或更多个周期,但更好的缓存未命中行为应该会产生更好的结果....&# 34;
似乎虚拟thunk仅出于性能原因而存在。如果我错了,请纠正我。