Inline functions
看起来像
functions
,产生的效果也像
functions
,它们在各方面都比
macros
(宏)好得多,而我们却可以在调用它们时不招致函数调用的成本。
实际上我们得到的可能比想到的更多,因为避免函数调用的成本只是故事的一部分。编译器的优化一般说是为了一段连续的没有函数调用的代码设计的,所以当我们
inline
一个函数时,我们就使得编译器对函数体实行上下文相关的专有优化成为可能。大多数编译器都不会对
"outlined"
函数调用实行这样的优化。
然而,编程中,就像在生活中没有免费午餐,而
inline functions
也不例外。一个
inline function
背后的思想是用函数的代码本体代替每一处对这个函数的调用,而且不必拿着统计表中的
Ph.D.
就可以看出这样很可能会增加我们的目标代码的大小。在有限内存的机器上,过分热衷于
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
(成员函数),但是
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
(内联化)是如何引起代码膨胀的,但是,还有其它的成本。
inline
是一个编译器可能忽略的请求。大多数编译器拒绝它们认为太复杂的
inline functions
(例如,那些包含循环或者递归的),而且,除了最琐碎的以外的全部的对
virtual functions
(虚拟函数)的调用都抗拒被
inlining
(内联化)。
virtual
意味着“等待,直到运行时再断定哪一个函数被调用”,而
inline
意味着“执行之前,用被调用的函数取代调用的位置”。如果编译器不知道哪一个函数将被调用,我们很难责备它们拒绝内联这个函数本体。
所有这些加在一起,就会得出:一个特定的
inline function
是否能真的被内联,取决于我们正在使用的构建环境——主要是编译器。幸运的是,大多数编译器都有一个诊断层次,在它们不能
inline
一个我们提出请求的函数时,会导致一个警告。
有时,即使编译器完全心甘情愿地
inline
一个函数,它们还是会为这个
inline function
生成函数本体。例如,如果我们的程序要持有一个
inline function
的地址,编译器通常必然为它生成一个
outlined
函数本体。它们怎么能拿得出一个指向根本不存在的函数的指针呢?再加上编译器一般不会对通过函数指针的调用进行
inlining
(内联化)的事实,这就意味着,对一个
inline function
的调用可能被也可能不被内联,这要依赖于这个调用是如何做成的:
inline void f() {...}
// assume compilers are willing to inline calls to f
void (*pf)() = f; // pf points to f
f();
// this call will be inlined, because it's a "normal" call
pf();
// this call probably won't be, because it's through
// a function pointer
甚至在我们从来没有使用函数指针时,
un-inlined inline functions
(未被内联的内联函数)的幽灵也会神秘地拜访,因为程序员并不一定是函数指针的唯一需求者。有时候编译器会生成
constructors
(构造函数)和
destructors
(析构函数)的
out-of-line
拷贝,以便它们能得到指向这些函数的指针,在对数组中的
objects
进行构造和析构的过程中使用。
事实上,对于
inlining
(内联化)来说,
constructors
(构造函数)和
destructors
(析构函数)经常是一个比在不经意的检查中所能显示出来的更加糟糕的候选者。例如,考虑下面这个
class Derived
的
constructor
(构造函数):
class Base {
public:
...
private:
std::string bm1, bm2; // base members 1 and 2
};
class Derived: public Base {
public:
Derived() {}
// Derived's ctor is empty — or is it?
...
private:
std::string dm1, dm2, dm3; // derived members 1–3
};
这个
constructor
(构造函数)看上去像一个
inlining
(内联化)的极好的候选者,因为它不包含代码。但是视觉会被欺骗。
C++
为
objects
被创建和被销毁时所发生的事情做出了各种保证。例如,当我们使用
new
时,我们的被动态创建的
objects
会被它们的
constructors
(构造函数)自动初始化,而当我们使用
delete
时,则相应的
destructors
(析构函数)被调用。当我们创建一个
object
时,这个
object
的每一个
base class
(基类)和每一个
data member
(数据成员)都会被自动构造,而当一个
object
被销毁时,则自动发生关于析构的反向过程。如果在一个
object
的构造期间有一个异常被抛出,这个
object
已经完全构造好的任何构件都被自动销毁。所有这些环节,
C++
只说什么必须发生,但没有说如何发生。那是编译器实现者的事,但显然这些事情不会自己发生。在我们的程序中必须有一些代码使这些事发生,而这些代码——由编译器写出的代码和在编译期间插入我们的程序的代码——必须位于某处。有时它们最终就位于
constructors
(构造函数)和
destructors
(析构函数)中,所以我们可以设想实现为上面那个声称为空的
Derived
constructor
生成相当于下面这样的代码:
Derived::Derived() // conceptual implementation of
{ // "empty" Derived ctor
Base::Base();
// initialize Base part
try { dm1.std::string::string(); }
// try to construct dm1
catch (...) {
// if it throws,
Base::~Base();
// destroy base class part and
throw;
// propagate the exception
}
try { dm2.std::string::string(); }
// try to construct dm2
catch(...) {
// if it throws,
dm1.std::string::~string();
// destroy dm1,
Base::~Base();
// destroy base class part, and
throw;
// propagate the exception
}
try { dm3.std::string::string(); }
// construct dm3
catch(...) {
// if it throws,
dm2.std::string::~string();
// destroy dm2,
dm1.std::string::~string();
// destroy dm1,
Base::~Base();
// destroy base class part, and
throw;
// propagate the exception
}
}
这些代码相对于真正的编译器所生成的代码不具有代表性,因为真正的编译器会用更复杂的方法处理异常。尽管如此,它还是准确地反映了
Derived
的
"empty" constructor
(“空”构造函数)必须提供的行为。不论一个编译器的异常实现多么复杂,
Derived
的
constructor
(构造函数)至少必须调用它的
data members
(数据成员)和
base class
(基类)的
constructors
(构造函数),而这些调用(它们自己也可能是被内联的)会影响它对于
inlining
(内联化)的吸引力。
同样的原因也适用于
Base
constructor
,所以如果它是被内联的,插入它的全部代码也要插入
Derived
constructor
(通过
Derived
constructor
对
Base
constructor
的调用)。而且如果
string
的构造函数碰巧也是被内联的,
Derived
constructor
中将增加那个函数的代码的五个拷贝,分别对应于一个
Derived
object
中的五个
string
s
(两个继承的加上三个它自己声明的)。也许在现在,为什么是否内联
Derived
的
constructor
不是一个不经大脑的决定就很清楚了。类似的考虑也适用于
Derived
的
destructor
,用同样的或者不同的方法,必须保证所有被
Derived
的
constructor
初始化的
objects
被完全销毁。
库设计者必须评估声明函数为
inline
的影响,因为为一个库中的可见的
inline functions
提供二进制升级是不可能的。换句话说,如果
f
是一个库中的一个
inline functions
,库的客户将函数
f
的本体编译到他们的应用程序中。如果一个库的实现者后来决定修改
f
,所有使用了
f
的客户都必须重新编译。这常常会令人厌烦。在另一方面,如果
f
是一个
non-inline function
,对
f
的改变只需要客户重新链接。这与重新编译相比显然减轻了很大的负担,而且,如果库包含的函数是被动态链接的,这可能就是一种对于用户来说完全透明的方法。
为了程序开发的目标,在头脑中牢记这些需要考虑的事项是很重要的,但是从编码期间的实践观点来看,占有支配地位的事实是:大多数调试器会与
inline functions
发生冲突。这应该不是什么重大的新发现。我们怎么能在一个不在那里的函数中设置一个断点呢?虽然一些构建环境设法支持
inline functions
的调试,多数环境还是简单地为调试构建取消了
inlining
(内联化)。
这就导出了一个用于决定哪些函数应该被声明为
inline
,哪些不应该的合乎逻辑的策略。最初,不要
inline
任何东西,或者至少要将我们的
inlining
(内联化)的范围限制在那些必须
inline
的)和那些实在微不足道的函数上。通过慎重地使用
inlines
,我们可以使调试器的使用变得容易,但是我们也将
inlining
(内联化)放在了它应在的地位:作为一种手动的优化。不要忘记由经验确定的
80-20
规则,它宣称一个典型的程序用
80%
的时间执行
20%
的代码。这是一个重要的规则,因为它提醒我们作为一个软件开发者的目标是识别出能提升我们的程序的整体性能的
20%
的代码。我们可以
inline
或者用其他方式无限期地调节自己的函数,但除非将精力集中在正确的函数上,否则就是白白浪费精力。
Things to Remember
将大部分
inlining
(内联化)限制在小的,频繁调用的函数上。这使得程序调试和二进制升级更加容易,最小化潜在的代码膨胀,并最大化提高程序速度的可能性。
不要仅仅因为
function templates
(函数模板)出现在头文件中,就将它声明为
inline
。