条款 30:透彻了解 inlining 的里里外外

《Effective C++ 中文版 第三版》读书笔记

** 条款 30:透彻了解 inlining 的里里外外 **

inline 函数,可以调用它们而又不需蒙受函数调用所招致的额外开销

当你 inline 某个函数,或许编译器就因此又能力对它(函数本体)执行语境相关最优化。

然而,inline 函数背后的整体观念是,将 “对此函数的每一个调用” 都已函数本体替换之,这样做可能增加你的目标码(object code)大小。在内存有限的机器上,过度 inline 会造成程序体积太大,导致换页行为,降低缓存的命中率等一些带来效率损失的行为。

如果 inline 函数的本体很小,编译器针对 “函数本体” 所产生的码可能比针对 “函数调用” 所产出的码更小。将函数 inline 可以导致更小的目标码,从而提高效率。

inline 只是对编译器的一个申请,不是强制命令。这种申请可以隐喻提出也可以明确提出。

隐喻方式是将函数定义于 class 定义式内:

class Person{ 
public: 
    ... 
    int age() const {return theAge;}//一个隐喻的inline申请 
    ... 

private: 
    int theAge; 
};

明确申请 inline 函数的做法是在其定义式前加上关键字 inline。

template 

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

inline 函数通常放置在头文件内,因为大多数建置环境(build environments)在编译过程中进行 inlining,为了将 “函数调用” 替换为 “被调用函数的本体”,编译器必须知道那个函数长什么样子。

templates 通常也被置于头文件内,因为他一旦被引用,编译器(在编译期)为了将它具现化,需要知道它长什么样子。如果 template 没有理由要求它所具现的每个函数都是 inlined,就应该避免将这个 template 声明为 inline(不论显式还是隐式)。inline 需要成本。

大部分编译器拒绝将太过复杂(带有循环或递归)的函数 inlining,而所有对 virtual 函数的调用也都会使 inlining 落空。

有时,虽然编译器有意愿 inlining 某个函数,还是可能为该函数生成一个函数本体。例如,如果程序要取某个 inline 函数的地址,编译器通常必须为此函数生成一个 outlined 函数本体。编译器通常不对 “通过函数指针而进行的调用” 实施 inlining,这意味着对 inline 函数的调用有可能 inlined,也可能不被 inlined:

inline void f(){…}  // 假设编译器有意愿 inline “对 f 的调用”
void (*pf)() = f;
f(); // 这个调用将被 inlined,因为是一个正常调用
pf(); // 这个调用或许不被 inlined,因为通过指针达成有时候编译器生成构造函数和析构函数的 outline 副本,这样他们就可以获得指针指向那些函数,在array内部元素的构造和析构过程中使用。

class base { 
public: 
    ... 
private: 
    std::string bm1, bm2; 
};

class Derived : public Base { 
public: 
    Derived(){}  //Derived 构造函数是空的 是吗? 
    ... 
private: 
    std::string dm1, dm2, dm3; 
};

这个构造函数看起来是 inlining 的绝佳候选人,因为他根本不含任何代码,但是:

C++ 对于 “对象被创建和被销毁时发生什么事” 做了各式各样的保证。编译器为稍早说的那个表面上看起来是空的 Derived 构造函数所产生的代码,相当于以下所列:

Derived::Derived() 
{ 

   Base::Base(); 

    try{dm1.std::string::string();} 

    catch(...){ 

        Base::~Base(); 

        throw; 

    } 

    try{dm2.std::string::string();} 

    catch(...){ 

        dm1.std::string::~string(); 

        Base::~Base(); 

        throw; 

    } 

    try{dm3.std::string::string();} 

    catch(...){ 

        dm2.std::string::~string(); 

        dm1.std::string::~string(); 

        Base::~Base(); 

        throw; 

    } 

}

这段代码并不能代表编译器真正制造出来的代码,但是不论编译器在其内所做的异常处理多么精致复杂,Derived 构造函数至少一定会陆续调用其成员变量和 base class 两者的构造函数,而那些调用(它们自身也可能被 inlined)会影响编译器是否对此空白函数 inlining。

程序库设计者必须评估 “将函数声明为 inline” 的冲击:inline 函数无法随着程序库的升级而升级。f 是程序库内的一个 inline 函数,

客户将 “f 函数本体” 编进其程序中,一旦程序库设计者决定改变 f,所有用到f的客户端程序都必须重新编译。然而若 f 是 non-inline 函数,客户端只要重新连接就好了,如果是程序库采用动态链接,升级后的函数甚至可以不知不觉的被应用程序吸纳。

从实用观点出发,大部分调试器对 inline 函数都束手无策。

请记住:

  1. 将大多数 inline 限制在小型、被频繁调用的函数身上。这可使日后的调试过程和二进制升级更容易,也可以使潜在的代码膨胀问题最小化,使程序的速度提升机会最大化。
  2. 不要只因为 function template 出现在头文件中,就将它们声明为 inline。

你可能感兴趣的:(条款 30:透彻了解 inlining 的里里外外)