Item 30: 了解 inlining(内联化)的来龙去脉
作者:Scott Meyers
译者:fatalerror99 (iTePub's Nirvana)
发布:http://blog.csdn.net/fatalerror99/
inline functions ——多么棒的主意啊!它们看起来像 functions,它们产生的效果也像 functions,它们在各方面都比 macros(宏)好得太多太多(参见 Item 2),而你却可以在调用它们时不招致函数调用的成本。你还有什么更多的要求呢?
实际上你得到的可能比你想到的更多,因为避免函数调用的成本只是故事的一部分。编译器的优化一般说来是为了一段连续的没有函数调用的代码设计的,所以当你 inline 一个函数的时候,你就使得编译器对函数体实行上下文相关的专有优化成为可能。大多数编译器都不会对 "outlined" 函数调用实行这样的优化。
然而,在编程中,就像在生活中,没有免费午餐,而 inline functions 也不例外。一个 inline function 背后的思想是用函数的代码本体代替每一处对这个函数的调用,而且不必取得统计学博士学位就可以看出这样很可能会增加你的目标代码的大小。在有限内存的机器上,过分热衷于 inlining(内联化)会使得程序对于可用空间来说过于庞大。即使使用了虚拟内存,inline 引起的代码膨胀也会导致额外的分页调度,减少指令缓存命中率,以及随之而来的性能损失。
在另一方面,如果一个 inline function 本体很短,为函数本体生成的代码可能比为一个函数调用生成的代码还要小。如果是这种情况,inlining(内联化)这个函数可以实际上导致更小的目标代码和更高的指令缓存命中率!
记住,inline 是向编译器发出的一个请求,而不是一个命令。这个请求能够以隐式的或显式的方式提出。隐式的方法就是在一个 class definition(类定义)的内部定义一个函数:
class Person {
public:
...
int age() const { return theAge; } // an implicit inline request: age is
... // defined in a class definition
private:
int theAge;
};
这样的函数通常是 member functions(成员函数),但是 Item 46 阐释了 friend functions(友元函数)也能被定义在 classes 的内部,如果是这样,它们也被隐式地声明为 inline。
声明一个 inline function 的显式方法是在它的声明之前加上 inline 关键字。例如,以下就是标准 max template(来自 <algorithm>)的通常的实现:
template<typename T> // an explicit inline
inline const T& std::max(const T& a, const T& b) // request: std::max is
{ return a < b ? b : a; } // preceded by "inline"
max 是一个 template 的事实引出一个观察结论:inline functions 和 templates 一般都是定义在头文件中的。这就使得一些程序员得出结论断定 function templates(函数模板)必须是 inline 的。这个结论是无效的而且有潜在的危害,所以它值得我们考察一下。
inline functions 一般必须在头文件内,因为大多数构建环境在编译期间进行 inlining(内联化)。为了用被调用函数的函数本体替换一个函数调用,编译器必须知道函数看起来像什么。(有一些构建环境可以在连接期间 inline,还有少数几个——比如,基于 .NET Common Language Infrastructure (CLI) 的托管环境——居然能在运行时 inline。然而,这些环境都是例外,并非规定。inlining(内联化)在大多数 C++ 程序中是一个 compile-time activity(编译期行为)。)
templates 一般在头文件内,因为编译器需要知道一个 template 看起来像什么以便需要时对它进行实例化。(同样,也不是全部如此。一些构建环境可以在连接期间进行 template instantiation(模板实例化)。然而,compile-time instantiation(编译期实例化)更为普遍。)
template instantiation(模板实例化)与 inlining(内联化)无关。如果你写了一个 template,而且你认为所有从这个 template 实例化出来的函数都应该被内联,那么就声明这个模板为 inline,这就是上面的 std::max 的实现所做的事情。但是如果你为没有理由被内联的函数写一个 template,就要避免声明这个 template 为 inline(无论显式的还是隐式的)。inlining(内联化)是有成本的,而且你不希望在毫无预见的情况下遭遇它们。我们已经说到 inlining(内联化)是如何引起代码膨胀的(这对于 template 作者来说是极为重要的一个考虑事项——参见 Item 44),但是,还有其它的成本,过一会儿我们再讨论。
在做这件事之前,我们先来完成对这个结论的考察:inline 是一个编译器可能忽略的请求。大多数编译器拒绝它们认为太复杂的 inline functions(例如,那些包含循环或者递归的),而且,除了最琐碎的以外的全部的对 virtual functions(虚拟函数)的调用都抗拒被 inlining(内联化)。不应该对这后一个结论感到惊讶。virtual 意味着“等待,直到运行时再断定哪一个函数被调用”,而 inline 意味着“执行之前,用被调用的函数取代调用的位置”。如果编译器不知道哪一个函数将被调用,你很难责备它们拒绝内联这个函数本体。
所有这些加在一起,就会得出:一个特定的 inline function 是否能真的被内联,取决于你正在使用的构建环境——主要是编译器。幸运的是,大多数编译器都有一个诊断层次,在它们不能 inline 一个你提出请求的函数时,会导致一个警告(参见 Item 53)。