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

这样的函数通常是成员函数,但是如果把friend函数的实现也放在头文件内,那么该friend函数也会被隐式申请为inline。

例如:

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

From:http://www.cnblogs.com/xkfz007/archive/2012/03/27/2420166.html

不恰当地使用inline导致编译器拒绝进行inlining是会带来副作用的,这会带来代码膨胀(目标码膨胀)和可能极难察觉的bug。因为编译器对普通函数(没有声明为inline)的实现与对inlining失败的函数的实现是不同的。

普通函数在编译时被单独编译为一个对象,包含在相应的目标文件中。目标文件在链接时,对该函数的调用会被链接到该对象上。

若一个函数被声明为inline,那么编译器即使遇到该函数的声明也不会为该函数编译一个对象,因为inline函数是在调用的地方进行展开的。但是如果在调用的地方发现该inline函数不适合被展开怎么办?一种做法是在调用该内联函数的目标文件中为该内联函数编译一个对象。这么做的直接后果是:若在多个文件调用了内联失败的函数,其中每个文件对应的目标文件中都会包含一份该内联函数的目标代码。

如果编译器真的选择了上面的做法对待内联失败的函数,那么目标代码的体积膨胀得与成功内联的目标代码一样,但目标代码的效率确和没内联一样。

更糟的是由于存在多份函数目标代码带来一些程序bug。最明显的例子是:内联失败的函数内的静态变量实际上就不在只有一份,而是有若干份。这显然是个错误,但是如果不了解内幕就很难找到原因。

[Effective C++系列]-透彻了解inlining的里里外外的更多相关文章

  1. [Effective C++ --030]透彻了解inlining的里里外外

    引言  inline函数 在函数声明或定义中函数返回类型前加上关键字inline即把min()指定为内联. inline函数对编译器而言必须是可见的,以便它能够在调用点内展开该函数.与非inline函 ...

  2. Effective C++ -----条款30:透彻了解inlining的里里外外

    将大多数inlining限制在小型.被频繁调用的函数身上.这可使日后的调试过程和二进制升级(binary upgradability)更容易,也可使潜在的代码膨胀问题最小化,使程序的速度提升机会最大化 ...

  3. 【30】透彻了解inlining 的里里外外

    1.inline方法相当于文本替换,不需要承担方法调用的额外开销,同时还有潜在的优势,文本替换后,编译器会进行代码优化.而对于方法调用,编译器没有能力进行代码优化. 2.显而易见,inline方法往往 ...

  4. 条款30:透彻了解inline的里里外外(understand the ins and outs of inlining)

    NOTE: 1.将大多数inline限制在小型 被频繁调用的函数身上.这可使日后的调试过程和二进制升级(binary upgradability)更容易,也可使潜在的代码膨胀问题最小化, 使程序的速度 ...

  5. Effective java 系列之避免过度同步和不要使用原生态类型,优先考虑泛型

    避免过度同步(67):在一个被同步的方法或代码块中,不要调用哪些被设计成被覆盖的方法或者是由客户端以函数对象的形式提供的方法(21). 有点拗口,书上提供的创建者与观察者模式,add方法太多,看得眼花 ...

  6. [Effective C++系列]-为多态基类声明Virtual析构函数

    Declare destructors virtual in polymorphic base classes.   [原理] C++指出,当derived class对象经由一个由base clas ...

  7. Effective java 系列之异常转译

    异常转译:当位于最上层的子系统不需要关心底层的异常细节时,常见的作法时捕获原始异常,把它转换一个新的不同类型的异常,在将新异常抛出. 通常方法捕获底层异常,然后抛高层异常. public static ...

  8. Effective java 系列之更优雅的关闭资源-try-with-resources

    背景: 在Java编程过程中,如果打开了外部资源(文件.数据库连接.网络连接等),我们必须在这些外部资源使用完毕后,手动关闭它们.因为外部资源不由JVM管理,无法享用JVM的垃圾回收机制,如果我们不在 ...

  9. 条款30:透彻了解inline的里里外外。

    inline可以带来各种好处: 首先其可以使得消除函数调用带来的开销,再者编译器对这种非函数的代码可以做出更多的优化策略.   但是inline函数首先肯定是会导致程序代码的大小更加的庞大,这样会带来 ...

随机推荐

  1. ASP.NET在实际开发中验证码的用法

    在网上有看到很多关于验证码的代码,很多都只是生成一张验证码图片,然而在实际登陆验证模块,验证码要怎么添加进去或者说怎么运用.和实际项目开发中要怎么使用验证码,我自己总结了几点. 一.在实际开发登陆模块 ...

  2. Oozie — What Why and How

    Oozie是什么? Oozie最初是Yahoo!为Hadoop开发的一个工作流调度器,一个工作流有多个Job组成.它允许用户提交由多个Job组成的工作流配置文件,这些Job既可以顺序执行,也可以并行执 ...

  3. JS--Div中数据滚动到最后一条重新从头开始滚动

    在做东西时要求让数据滚动,且滚动到最后一条时,从头再开始滚动,样图如下:

  4. 设置Proxy Server和SQL Server实现互联网上的数据库安全

    ◆首先,我们需要了解一下SQL Server在WinSock上定义协议的步骤: 1. 在”启动”菜单上,指向”程序/Microsoft Proxy Server”,然后点击”Microsoft Man ...

  5. MVC 全局异常过滤器HandleErrorAttribute

    using System; using System.Collections.Generic; using System.Linq; using System.Web; using System.We ...

  6. iOS FMDB

    FMDB FMDB概述 什么是FMDB * FMDB是iOS平台的SQLite数据库框架 * FMDB以OC的方式封装了SQLite的C语言API FMDB的优点 * 使用起来更加面向对象,省去了很多 ...

  7. NSArray数组的学习总结

    1.不可变数组NSArray NSArray是不可变的,而且只能储存Object-c对象.另外,数组的最后一个元素一定是nil,表示结束. 注:这些集合类只能收集cocoa对象(NSOjbect对象) ...

  8. 实现一个宽和高都是100像素的div可以用鼠标拖拽移动的效果

    html,body{ width:100%;height:100%;margin:0px;padding:0px; } #box{ width:100px;height:100px;backgroun ...

  9. PDO扩展使用方法

    pdo扩展为php访问数据库提供了一个轻量级的一致接口,pdo提供了一个数据访问抽象层,这意味着不管使用哪种数据库,都可以使用相同的函数来查询和获取数据. $dbms='mysql'; //数据库类型 ...

  10. HDU 5794 - A Simple Chess

    HDU 5794 - A Simple Chess题意: 马(象棋)初始位置在(1,1), 现在要走到(n,m), 问有几种走法 棋盘上有r个障碍物, 该位置不能走, 并规定只能走右下方 数据范围: ...