final 关键字

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 继续按照虚调用语义跳转到真实目标函数

评论

一条对“final 关键字”的回复

  1. Alpaca00 的头像
    Alpaca00

    学到了

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注