final关键字_C++中final关键字对性能的影响

07d48d6db4c417209d93edb90cf91876.png

C++11引入了关键字final,按官方的标准是该关键字是用来标识虚函数不能在子类中被覆盖(override),或一个类不能被继承。用法如下:

struct Base
{
    virtual void foo();
};

struct A : Base
{
    void foo() final; // Base::foo 被覆盖而 A::foo 是最终覆盖函数
    void bar() final; // 错误:非虚函数不能被覆盖或是 final
};

struct B final : A // struct B 为 final
{
    void foo() override; // 错误:foo 不能被覆盖,因为它在 A 中是 final
};

struct C : B // 错误:B 为 final
{
};

然而在除了上述的标准化作用之外,或许可以从该关键字本身来挖掘更多可能性,如性能提升。

在正式开始本话题之前,我们来回顾一下C++中的动态绑定(dynamic binding),动态绑定依赖于虚表(vtable)来在运行时决定需要被调用的方法/函数,即通过对象指向的vtable来查找调用的函数。

struct A : Base
{
    virtual void foo() override;
};

A * obj = new A();
a->foo();

其中a->foo()先首先根据a指向的虚表找到foo的函数地址,然后再执行foo函数的调用,因此这里就有一定性能开销,即从vtable中找到foo的地址。

为什么要从vtable中找foo的地址,这里因为A有可能被其它类继承,在子类中覆盖了foo的实现,也即为了实现动态绑定。

思考一个问题,如果明确知道A不会被其它类继承的情况下,是不是可以省掉从vtable中找到foo地址的开销呢?答案是肯定的,如果明确知道该信息,完全可以将A类型上的调用全部退化成对A上非virtual函数的调用。因此如果一个类或函数声明成final,在编译器层面可以优化掉一步vtable的查找开销,这在C++的标准中并没有规定,完全依赖于编译器的实现。

即然依赖于编译器的实现,那是否有编译器实现了这种优化呢?下面来看一下Clang的编译结果。

class Base
{
public:
    __attribute__((noinline)) virtual float GetA( int i ) {
        return  i * float(i);
    }
    __attribute__((noinline)) virtual float Get( int i ) {
        return  i * float(i);
    }
};

class Derived_A : public Base
{
    public:
__attribute__((noinline))virtual float Get( int i ) {
        return  i * float(i) * 2.1f;
    }
};

class Derived_B final : public Base
{
    public:
    __attribute__((noinline)) virtual float Get( int i ) {
        return  i * float(i) * 3.4f;
    }
};

__attribute__((noinline))float Test_A(Derived_A* a, int i)
{
    return a->Get(i) + 1.0f;
}

__attribute__((noinline))float Test_B(Derived_B* a, int i)
{
    return a->Get(i) + 2.0f;
}

int main(int argc)
{
    Derived_A* a = new Derived_A();
    auto b = new Derived_B();

    auto v1 = Test_A(a, argc);
    auto v2 = Test_B(b, argc);
    return v1 + v2;
}

由于测试代码非常简单,因此在clang编译时使用O2级别的优化,会导致一些代码被inline,因此在编译时强制不内联函数。Test_A和Test_B的声明也是为了增加一下间接性。

编译后的结果片断如下:

Test_A(Derived_A*, int):                  # @Test_A(Derived_A*, int)
# %bb.0:
        push    rax
        mov     rax, qword ptr [rdi]
        call    qword ptr [rax + 8]
        addss   xmm0, dword ptr [rip + .LCPI0_0]
        pop     rax
        ret
                                        # -- End function
.LCPI1_0:
        .long   1073741824              # float 2
Test_B(Derived_B*, int):                  # @Test_B(Derived_B*, int)
# %bb.0:
        push    rax
        call    Derived_B::Get(int)
        addss   xmm0, dword ptr [rip + .LCPI1_0]
        pop     rax
        ret

从编译后的结果可以看出对Derived_A类型的调用Get函数会去查找vtable,而对Derived_B的Get函数调用是在编译时直接使用函数地址。测试显示,直接将Get声明为final可能达到相同的效果。

  • 0
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值