《Efficient C++ : Performance Programming Techniques》是一本关于提高C++性能的编程书,书中的第八章到第十章探究了内联的技巧、利弊和使用场景,以下是对该内容的读书笔记。
内联的利与弊
内联的优势,可以从函数调用的开销去分析。
函数调用需要开辟栈空间,以及修改各种寄存器值以“保存现场”和保证函数的执行。例如指令寄存器保存旧值覆盖新值,栈指针寄存器记录当前栈位置,等等。所以函数即便执行的代码只有几行,也会产生固定的开销。
方法调用需要跳转。频繁的跳转意味着cpu的大量时间都用在重定向指令上。
编译器无法跨越函数边界去做优化。例如
int x = 10; x = 20;
这样的代码,编译器通常会将两行合并为一行int x = 20;
。但是如果 x 作为函数的引用类型参数在函数体内被赋值,编译器是无法做优化的。
内联的弊端,则是在于代码膨胀以及由此造成的缓存命中问题和缺页中断。如果是函数调用,不同的调用执行的都是同一地址的程序,如果缓存中有该段地址就可以快速执行,而内联则是不同地址,会增大缓存空间,使得缓存命中率下降。而页面错误也与此类似,试想如果内联函数代码膨胀严重,就会频繁发生缺页中断,使得性能下降。
还有一种情况不应该内联,那就是工程中可能会频繁变动的函数,因为内联会在实现层面和接口层面产生编译依赖,一旦内联函数有所改动,所有调用该内联函数的模块都要重新编译。
有两条内联规则,用来判断什么情况下函数应该内联:
- singleton 唯一化,即函数在程序中的调用点唯一;
- trival 精简化,即函数较短小,通常5行以内的源码,编译后10条以内的汇编指令。
虚函数内联
内联是编译时的优化,虚函数是运行时的机制,虚函数如何能够内联呢?对于一个优秀的编译器,如果对象指针在初始化后赋值之后没有再被赋值过,那么该指针的虚函数调用其实在编译期就可以确定对象类型的,所以没有必要用到多态。
条件内联 与 选择性内联
C++的内联机制缺少机动性,我们用条件内联和选择性内联,来决定在不同场景下使用不同的内联策略。
条件内联将内联函数定义在.inl文件中,并通过预处理机制来控制.h文件是否include .inl文件。在include代码前用一个宏INLINE来判断是否include,在编译选项时使用-D定义宏INLINE就可以打开内联开关。
条件内联仍然不够灵活,要么全部内联,要不都不。选择性内联维护函数的两个版本,内联版本和外联版本,可以在某些场合使用内联版本而在其它场合使用外联版本。为了避免代码重复,在外联版本的函数中调用内联版本的函数。