final 关键字可以给编译器提供一个证明的功能,让编译器在优化虚函数的时候能够更加激进,从读虚指针然后跳转到虚函数。
mov rax, [rdi] // 读 vptr
jmp [rax] // 从 vtable slot 间接跳转
优化到直接调用/内联具体函数
int call_d(const D* p) {
return p->f();
}
- 如果 D 没有 final,在普通分离编译模型下,p 可能指向 D 的派生类对象,所以不能无条件去虚化
- 如果 D final,编译器不需要看到全程序,也能证明 p->f() 只能调用 D::f()。
样例
首先来看一下.cpp实现,唯一区别是是否加 final

然后编译得到汇编代码:
g++ -O2 -std=c++20 -S -masm=intel with_final.cpp -o with_final.s
g++ -O2 -std=c++20 -S -masm=intel not_final.cpp -o not_final.s

从这个图不难看出,with_final 的版本
- .set _Z6call_dPK1D,_ZNK1D1fEv:表示将call_d这个符号直接变成了D::f()的别名,调用call_d等价于调用D::f()

然后not_final 的版本会多出一段汇编,具体内容就是
- lea rdx, _ZNK1D1fEv[rip]: 将D::f() const 的函数地址放到 rdx
- mov rax, QWORD PTR [rax]: 读取vtable[0]的实际地址,这里的[rax] 是从 vptr 指向的 vtable address point 开始取第一个虚函数的slot
- cmp rax, rdx + jne .L13:如果vtable[0] != &D::f(),就跳转到.L13, 走保底的虚调用路径,如果相等,就走下面的已经inline的 D::f()快路径
- 然后后面的部分是将D::f() inline进来的汇编。
不难看出:
- 加final 的时候:D不可能有派生类型,因此 const D* p 的动态类型只能是D,编译器不需要保留 guard 和 fallback 虚调用路径。会少走使用虚指针+虚函数表来查找函数地址的代码部分。
- 不加final的时候:GCC做的是 guarded drvirtualization,先检查vtable slot 里面是否是 D::f(),如果是就直接走快路径,如果不是就跳到.L13 继续按照虚调用语义跳转到真实目标函数
发表回复