C++ final指示符带来的性能提升

C++ final指示符带来的性能提升

作者:BlogUpdater |  时间:2020-03-05 |  浏览:745 |  评论已关闭 条评论

final指示符简介
C++中的final指示符可以将一个类或者虚函数标记为不可继承或重写。让我们先看看如下的代码:

如果我们尝试创建一个新类并继承自[derived],则我们会得到一个编译错误,如下图所示:

final指示符向类的客户明确传递了这样的信息:这个类不应该用来被继承,并且使用编译器来保证这一点。同时,使用这个标识符可以使用去虚拟化(Devirtualization)来优化代码的性能。

去虚拟化(Devirtualization)
虚函数需要通过虚函数表(vtable)进行间接调用,相对于直接调用来说,间接调用更加昂贵,因为它会受到分支预测和指令缓存的影响,同时,间接调用还阻止了编译期对代码的更进一步的优化,例如,例如,通过将代码内联来提升运行时性能。

去虚拟化是一项编译期优化措施,它可以在编译期解析出虚函数调用,而不是在运行时进行决断。这项优化解决了上面谈到的问题,因此它可以显著的提升代码的性能,尤其是代码里含有大量的虚函数调用的时候。

下面是一个去虚拟化的例子代码:

在上面的代码中,即使[dog::speak]是一个虚函数,[main]函数的唯一输出只可能是”woof”,如果你仔细的查看编译器输出,你会发现,MSVC, GCC和Clang这些编译器都会识别到这一点并将[dog::speak]的定义内联到[main]函数中,这样就避免了运行时的间接调用,从而提升了代码性能。

final指示符带来的性能提升
通过在代码中使用final指示符,编译器可以获得更多的机会执行去虚拟化。因为如果代码中使用到了final,就表示类的虚函数可以在编译期进行解析,从而使得编译器可以进行去虚拟化。
我们还是看之前的例子代码:

考察一下代码:

因为[derived]被标识为final,所以编译器知道它不可被继承。这意味着对虚函数[f]的调用只会调用到[derived::f]上,所以这个调用可以在编译期进行解析。当[derived]或者[derived::f]被标识为final时,我们可以在MSVC的编译输出中看到如下的证据:

你可以看到,[derived::f]被内联到了[call_f]的定义中。如果我们将final标识符从代码中去掉,则我们会观察到如下的汇编代码:

以上代码将会加载对象[d]的虚函数表,然后通过保存在虚表相对位置的函数指针对[derived::f]发起一次间接调用。

[load]和[jump]指令的成本可能看起来不是很多,因为它就是两条指令而已。但是这会带来错误的分支预测或者指令缓存未命中,从而带来一次[pipeline stall]。
而且,如果在[call_f]中包含更多的代码或者更多的函数调用它的话,编译器将会看到代码调用树的全景并进行额外的分析,从而有可能对代码进行更为激进的优化。

总结
通过将你的类或者虚函数声明为final,我们可以提供给编译器更多的优化可能性,使得编译器可以在编译期就对虚函数调用进行解析,从而避免了运行时的间接调用成本。

那么,今天在你的工程中试试吧,看看代码是不是跑的更加欢快了?

标签:

评论已关闭。