秒杀面试题:深入final,掌握C++性能优化

2024-01-11 13:41:27 浏览数 (1)

秒杀面试题:深入final,掌握C 性能优

C 11之后有了final,它用来指定不能在派生类中重写虚函数,或者不能从中派生类。

例如下面这个例子,当添加final之后:

  • foo函数被禁止重写
  • bar函数被禁止重写
  • B类被禁止继承

那么,除了这些还有什么?如果面试官问你final,你只答这些能过?接下来本文来拓展一下final的妙用!

代码语言:javascript复制
struct Base
{
    virtual void foo();
};
 
struct A : Base
{
    void foo() final; // Base::foo is overridden and A::foo is the final override
    void bar() final; // Error: bar cannot be final as it is non-virtual
};
 
struct B final : A // struct B is final
{
    void foo() override; // Error: foo cannot be overridden as it is final in A
};
struct C : B {}; // Error: B is final

虚函数需要在运行时通过vtable进行间接调用,其中会发生分支预测和指令缓存,还会影响内联。因此相比于函数的直接调用,其性能较差。

通过final可以做到去虚拟化,它是一种编译器优化手段,尝试在编译时而非运行时解决函数调用问题。由于在编译时可以确定调用哪个函数,因此便不会存在前面的vtable问题了,从而极大的提升虚函数调用的性能。

1.去虚拟化与原生对比

以下面为例:A有两个接口,f1、f2,子类B继承A重写了这两个接口,f1后面添加了final,f2不变,f1与f2函数各自调用b的f1与f2。

代码语言:javascript复制
class A {
public:
    virtual void f1() = 0;
    virtual void f2() = 0;
};

class B : public A {
public:
    void f1() final override {}
    void f2() override {}
};

void f1(B& b) {
    b.f1();
}

void f2(B &b) {
    b.f2();
}

其汇编代码如下:

https://gcc.godbolt.org/z/GT78G7T9z

代码语言:javascript复制
B::f2():
        rep ret
f1(B&):
        rep ret
f2(B&):
        mov     rax, QWORD PTR [rdi]
        mov     rax, QWORD PTR [rax 8]
        cmp     rax, OFFSET FLAT:B::f2()
        jne     .L6
        rep ret
.L6:
        jmp     rax

从上面可以看到,对于f1(b)来说由于添加了final,我们可以看到直接调用了B的f1,而并没有加载B的vtable。对于f2(b)来说加载了B的vtable、比较虚函数地址,判断是否jmp。

上面这个例子比较简单,指针加载和跳转的成本可能看起来并不多,因为它只是几条指令,但是这可能涉及分支预测错误和缓存未命中问题,这可能会引发性能下降。

2.总结一下final

final在日常面试/工作中还是比较有用,如果面试官问你final,你可以解答如下。

  • 禁止继承
  • 禁止虚函数重写
  • 去虚拟化提升性能
  • 设计约束,禁止子类继承,放置子类重写

总的来说,final 关键字在 C 中用于阻止继承和虚函数重写,提高代码的性能、可维护性和安全性。使用 final 能够更加明确地表达代码的意图,并防止一些潜在的错误。


0 人点赞