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

《Effective C++》:条款30:透彻了解inlining的里里外外

时间:2015-02-22 23:07:42      阅读:348      评论:0      收藏:0      [点我收藏+]

标签:inline   c++   头文件   函数调用   

inline函数是特殊的函数,它有宏的优点,却克服了宏的缺点(**条款**2)。inline函数可以免除函数调用所招致的额外开销,但你实际获得的好处可能比你想象的还多,编译器会对inline函数本体执行语境相关最优化。

但使用inline函数会导致目标码(object code)变大,因为对inline函数的调用都会以函数本体替换。在内存比较小的机器上,不宜过多使用inline函数。即使使用虚拟内存,也会导致额外的换页行为(paging),降低指令高速缓存装置的击中率(instruction cache hit rate),以及伴随而来的效率损失。如果inline函数本体很小,编译器对函数本体产出的码可能比函数调用所产出的码更小;如果这样,那么将函数inlining确实会减小目标码和提高高速指令高速缓存装置的击中率。

在函数前面加上inline关键字不是强制这个函数变为inline函数,这只是向编译器提一个申请。这个申请有时是隐喻的,例如将函数定义在class内。如果把friend函数定义在class内,那么它们也将隐喻声明为inline。

明确声明为inline函数的的做法是在其定义式前加上关键字inline。例如标准的max template(来自)是这样的:

template<typename T>
inline const T& std::max(const T& a, const T& b)
{   return a < b ? b : a; }

inline函数为什么放到头文件

inline函数和templates通常都被定义于头文件。这不是巧合。大多数建置环境(build environment)在编译过程中进行inlining,将一个“函数调用”替换为“被调用函数的本体”,在替换时需要知道这个函数长什么样子。.NET CLI(Common Language Infrastructure;公共计基础设置)的托管环境(managed environments)可以在运行期间完成inlining,但这是个例外。Inlining在大多数C++中是编译期行为。

Templates通常也放置在头文件,因为它一旦被使用,编译器为了将它具体化,也需要知道它长什么样子。(有些编译环境可以在链接期间才执行template具体化,但是编译期间完成的更常见)。

需要说明的是上述templates是inline,但实际上templates和inlining无关。对于template,inlining除了引起代码膨胀,还有其他成本(**条款**44),稍后再做介绍。

大部分编译器拒绝太过复杂的inlining函数(例如有循环或递归)。virtual函数也不能是inline函数,因为virtual函数是直到运行时才确定调用哪个函数,而inline是执行前将调用动作替换为函数本体。所以表面上是inline函数,实际上未必是,很大程度上取决于编译器。大多数编译器提供了一个诊断级别:如果无法将你要求的函数inline化,会给你一个警告信息(**条款**53)。

编译器将inline函数调用替换为inline函数本体的同时,还是可能会为该函数生成一个函数本体。如果程序要取某个inline函数的地址,编译器通常必须为此函数生成赢outlined函数本体。如果inline函数本体不存在,自然就不会有这个函数的地址。但是,通过函数指针调用inline函数,这时inline函数一般不会被inlined。

inline void f(){……}//假设编译器会inline函数f
void ( *pf)()=f;//指向函数的指针
f();//这个调用会被inlined,因为是正常调用
pf();//这个调用可能不会被inlined,因为它是通过函数指针达成

就算你未使用函数指针,程序有时也会使用。例如,编译器会生成构造函数和析构函数的outline副本,这样就可以获得这些函数的指针,在array内部元素的构造和析构过程中使用。

实际上,把构造函数和析构函数做为inline函数未必合适,表面上看并不可以看出原因。考虑下面Derived class构造函数

class Base{
public:
    ……
private:
    std::string bm1, bm2;
};

class Derived: public Base {
public:
    Derived(){}//Derived构造函数
    ……
private:
    std::string dm1, dm2, dm3;
};

class Derived的构造函数不含任何代码,应该是inlining函数的绝佳候选。但实际上未必。C++对于“对象被创建和销毁时都发了什么事”做了各种保证。例如,当你创建一个对象,base class和derived class的每一个成员变量都会被自动构造;当你销毁一个对象,会有反向的析构过程。这是正常运行时的情况,但是如果对象在构造期间有异常被抛出,那么该对象已经构造好的那一部分应该自动销毁。当然,这是编译器负责的事情,但是编译器是怎么实现的呢?那就是编译器在你的程序中插入了某些代码,通常就在构造函数和析构函数内。我们可以想象一下,那个空的构造函数到底应该是怎么样的:

Derived::Derived()//概念实现
{
    Base::Base();
    try{ dm1.std::string::string();}//构造dm1
    catch(……){
        Base::~Base();//销毁base class部分
        throw;//抛出异常
    }

    try{ dm2.std::string::string(); }//构造dm2
    catch(……){
        dm1.str::string::~string();//销毁dm1
        Base::~Base();
        throw;
    }

    try{ dm3.std::string::string();}
    catch(……){
        dm2.std::string::~string();
        dm1.std::string::~string();
        Base::~Base();
    }
}

上面代码并不是编译器生成的,编译器生成的应该会更加精致复杂,处理异常也没这么简单。但这也足够反应空白的Derived构造函数提供的行为。不论编译器怎么优化,Derived构造函数都会调用base class的构造函数和其成员变量的构造函数,而那些调用(它们自身也可能被inlined)会影响编译器是否对此空白函数inlining。这个道理同样适用于Base构造函数,也同样适用于析构函数。

程序员还要考虑将函数声明为inline带了的其他影响:inline函数无法随着程序库的升级而升级。例如,fun是个inline函数,客户讲fun编进其程序中,一旦程序库设计者升级程序库,所有用到函数fun的客户端程序多必须重新编译。但是如果fun是non-inline的,客户端只需重新连接即可;如果是动态链接库,客户端甚至感觉不到程序库升级。还有一个影响就是调试。大部分调试器无法调试inline函数,因为你不能再一个不存在的函数内设立断点。

这就使得我们在使用inline时更加慎重,这样一来要求手工优化代码。有一个80-20经验法则:平均而言,一个程序往往将80%的执行时间花费在20%的代码上。作为一个开发者,你的目标是有效增强20%代码的效率,用inline或其他方法将它瘦身。

总结

  • 1.将大多数inlining限制在小型、被频繁调用的函数身上。这可使日后的调试过程和二进制升级(binary upgradability)更容易,使代码膨胀问题最小化,使程序提升效率机会最大化。
  • 2.function tempates出现在头文件,但是不一定必须是inline。

《Effective C++》:条款30:透彻了解inlining的里里外外

标签:inline   c++   头文件   函数调用   

原文地址:http://blog.csdn.net/kangroger/article/details/43909975

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