码迷,mamicode.com
首页 > 编程语言 > 详细

[Effective C++系列]-透彻了解inlining的里里外外

时间:2015-03-28 18:37:41      阅读:208      评论:0      收藏:0      [点我收藏+]

标签:

Understand the ins and outs of inlining.
 
  • [原理]
Inline函数背后的做法是将“对函数的每一个调用”都用函数本体替换之。其好处是:
  1. 可以消除函数调用所带来的开销。
  2. 编译器最优化机制通常被设计用来浓缩那些“不含函数调用”的代码,因此当你inline某个函数,或许编译器有能力对它(函数本体)执行语境相关最优化。大部分编译器不会为一个“outlined函数调用”执行这种最优化动作。
然而inline函数这些美好的一面也伴随着代价:inline函数可能增加程序的目标码(object code)大小。在一台内存有限的机器上,过度inlining会造成加载至内存中的程序体积太大,即使拥有虚拟内存,inline造成的代码膨胀会导致额外的换页行为(paging),降低高速缓存装置的击中率,导致效率损失。(如果inline函数的本体很小,编译器针对“函数本体”产生的目标码可能比“函数调用”所产生的目标码更小,那么inlining确实可能导致更小的程序目标码和较高的指令高速缓存装置击中率。)
 
但这不是inline的全貌,inline只是对编译器的一个申请,不是强制命令。这项申请可以隐式声明,也可以显式声明。这意味着两方面:
  1. 当你申请(隐式或者显式)对一个函数进行inlining时,编译器未必真的这么做了。
  2. 有些你没注意到的写法可能导致一个函数被隐式inlining。

 

  • [详解]
1. 隐式的inline申请:在头文件中声明class成员函数时同时实现该成员函数。
class person
{
public:
     ...
     int age() {return m_age;} // 该函数会被隐式申请为inline函数
     ...
private:
     int m_age;
};

这样的函数通常是成员函数,但是friend函数也可以被定义于class内,那么这些函数也会被隐式申请为inline。

 
2. 显式申请inline函数的做法是在其定义前加上inline关键字。
template <typename T>
inline const T&  max(const T& a, const T& b)
{return a >b ? a : b;}
inline函数与template函数通常都被定义于头文件内,这会造成误解:function templates 一定必须是inline。这个结论不仅无效且可能有害。因为:
 
inline函数之所以一般被置于头文件内,那是因为大多数编译环境(building enviroment)是在编译过程中进行inlining,为了讲一个“函数调用”替换为“被调用函数的本体”,编译器必须知道该函数的实现内容。虽然有些编译环境可以在链接期完成inlining,甚至.NET CLI的托管环境可以在运行期完成inlining,但大多数C++编译环境是在编译期完成inlining行为的。
 
同样,template函数的实现一般被放在头文件中也是因为,模版函数的实例化一般也是在编译期完成的,因而编译器需要知道函数的实现内容(某些编译环境可以在链接期完成模板实例化,但这不常见)。
 
因此,template与inline无必然联系,如果你想让根据template函数实例化出的所有函数都应该是inlined,那么你就需要将此template函数声明为inline。否则,你应该避免这么做,因为inlining需要成本(如引发代码膨胀等等)。
 
3. 编译器拒绝将过于复杂(带有循环或者递归)的函数inlining,并且所有的virtual函数的调用都会使inlining落空。因为virtual函数意味在运行期才能动态地决定哪一个函数被调用,但是inline意味着在编译器就需要将被函数的调用替换成函数的本体。
 
4. 另外,如果程序中要取得某个inline函数的地址,编译器通常会为此函数生成一个outlined函数本体。因为编译器没有能力产生一个指针指向并不存在的函数。同时,编译器通常不对“通过函数指针而进行的调用”实施inlining动作,即对inline函数的调用有可能被inlined,也有可能不被inlined,这取决于该调用是如何实施的:
inline void fn() {…} // 假设编译器愿意inline“对fn的调用”
void (*pf) () = fn; // pf指向fn
...
fn(); // 该调用会被inlined,因为这是一个正常调用。
pf(); // 该调用不会被inlined, 因为它是通过函数指针实施。
 
所以,一个inline函数是否真的被inlining,取决于编译器的判断。
 
5. 即使程序员自己从未使用函数指针,编译器有时候还是会生成构造函数和析构函数的outline副本,这样一来它们就可以获得指针指向那些函数,在array内部元素的构造和析构中使用。
 
6. 实际上构造函数和析构函数往往是不适合被inlined的。
因为C++指出,当创建一个对象时,每一个base class及其每一个成员变量都会被构造;当销毁一个对象时,反向的析构动作也会发生。如果有异常在对象构造期间被抛出,该对象已构造好的那一部分会被自动销毁。
 
这意味着虽然定义了一个空的构造函数,但并不意味着编译后,该构造函数的实现一定是空的,因为编译器会在编译器间产生并安插代码到构造函数或者析构函数中。
 
7. 将函数声明为inline还会为程序开发过程带来冲击。
 
inline函数无法随着程序库的升级而升级,如果fn是程序库中的一个inline函数,所有调用了fn的“客户程序”都会将fn函数本体编译到其程序中,一旦程序库设计者改变了fn,所有用到fn的“客户程序”都需要被重新编译。
如果fn不是inline函数,一旦它有任何修改,“客户程序”只需要重新链接就好(如果是静态链接),远比重新编译负担少。如果程序库使用静态链接,fn的改动甚至不会被“客户程序”察觉。
 
另外,inline函数会给调试带来麻烦,因为无法在一个并不存在的函数中设立断点,从而导致许多编译环境会选择在调试版程序(DEBUG)中禁止发生linlining。
 
  • [总结]
  1. 一个合乎逻辑的策略是:一开始不要讲任何函数声明为inline,或至少将inlining局限于小型的、被频繁调用的函数身上。这会使得日后的调试和二进制升级更容易,也可使代码膨胀的问题最小化,使程序的速度提升机会最大化。
  2. 不要只因为function template出现在头文件中,就将它们声明为inline。

 

[Effective C++系列]-透彻了解inlining的里里外外

标签:

原文地址:http://www.cnblogs.com/dearcarlos/p/4374460.html

(0)
(0)
   
举报
评论 一句话评论(0
登录后才能评论!
© 2014 mamicode.com 版权所有  联系我们:gaon5@hotmail.com
迷上了代码!