了解Inline

  
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
 
  • 0
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值