Effective C++——条款30(第5章)

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

Understand the ins and outs of inlining

    inline 函数看起来像函数,动作像函数,比宏好得多(详见条款2),可以调用它们又不需要承受函数调用所导致的额外开销.

    实际获得的比想到的还多,因为"免除函数调用成本"只是故事的一部分而已.编译器最优化机制通常被设计用来浓缩那些"不含函数调用"的代码,所以当 inline 某个函数,或许编译器就因此有能力对它(函数本体)执行语境相关最优化.
    然而天下没有免费的午餐,inline 函数也不例外. inline 函数背后的整体观念是,将"对此函数的每一个调用"都以函数本体替换,这样可能增加目标码大小,在一台内存有限的机器上, 过度热衷 inline 会造成程序体积太大. 即使拥有虚拟内存,inline 造成的代码膨胀亦会导致额外的换页行为(paging),降低指令高速缓存装置的击中率(instruction cache hit rate),以及伴随这些而来的效率损失.
    换个角度来说,如果 inline 函数的本体很小,编译器针对"函数本体"所产出的码可能比针对"函数调用"所产生的码更小.果真如此,将函数 inlining 确实可能导致较小的目标码(object code)和较高的指令高速缓存装置击中率.
    注意,inline 只是对编译器的一个申请,不是强制命令,这项申请可以隐式提出,也可以显式提出.隐式方式是将函数定义于 class 定义式内:
class Person {
public:
    ...
    int age() const { return theAge; }        // 一个隐式申请
    ...
private:
    int theAge;
};
    这样的函数通常是成员函数. 显式声明 inline 函数的做法则是在其定义式加上关键字 inline.例如标准的max template 往往这样实现出来:
template<typename T>
inline const T& std::max(const T& a, const T& b) {
    return a < b ? b : a;
}
    大部分编译器拒绝将太过复杂的函数inlining,而 所有对 virtual 函数的调用也都会使inlining落空.这不令人惊讶, 因为 virtual 意味"等待,直到运行期才确定调用哪个函数",而 inline 意味"执行前,先将调用动作替换为被调用函数的本体".
    这些叙述整合起来的意思是:一个表面上看似 inline 的函数是否真是 inline,取决于建置环境,主要取决于编译器.幸运的是大多数编译器提供了一个诊断级别:如果它们无法将要求的函数 inline 化,就会发出一个警告信息(详见条款53).
     实际上构造函数和析构函数往往是 inlining的糟糕候选人——虽然人们不会这样认为.考虑以下Derived class 构造函数:
class Base {
public:
    ...
private:
    std::string bm1, bm2;
};
class Derived : public Base {
public:
    Derived() { }            // Derived构造函数是空的...
    ...
private:
    std::string dm1, dm2, dm3;
};
    这个构造函数看起来是inlining的绝佳候选人,因为它根本不含任何代码,但是这是表象.
     C++对于"对象被创建和被销毁时发生什么事情"做了各种各样的保证.当使用new,动态创建的对象被其构造函数自动初始化;当使用 delete,对应的析构函数会被调用.当创建一个对象,其每一个base class 及其每一个成员变量都会被自动构造;当销毁一个对象,反向程序的析构行为亦会发生."这些事情如何发生"是编译器的事情,由编译器于编译期间代为产生并插入到程序中的代码,所以之前那个表面上看起来为空的Derived构造函数所产生的代码,相当于如下:
Derived::Derived() {    // 空白Derived构造函数
    Base::Base();
    try { dm1.std::string::string(); }      // 试图构造dm1
    catch (...) {                           //如果抛出异常就销毁base class成分
        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 两者的构造函数,而那些调用会影响编译器是否对此空白函数inlining.
    相同理由也适用于Base构造函数,所以如果它被inlined,所有替换"Base构造函数调用"而插入的代码也都会被插入到"Derived构造函数调用"内.如果string构造函数恰巧也被inlined.那么 Derived构造函数将获得五份"string构造函数代码"副本.因此,"是否将Derived构造函数inline化"并非是个轻松的决定.类似的思考也适用于Derived析构函数.
    程序库设计者必须评估"将函数声明为inline"的冲击:i nline 函数无法随着程序库的升级而升级.换句话说如果f是程序库内的一个 inline 函数,客户将"f函数本体"编进程序中,一旦程序库设计者决定改变f,所有用到f的客户端程序都必须重新编译.这往往是大家不愿看到的.然而如果f是non-inline 函数,一旦它有任何修改,客户端只需要重新连接就好,远比重新编译的负担少很多.
    从纯粹使用观点出发:大部分调试器面对 inline 函数都束手无策.毕竟不可能在并不存在的函数内设立断点.
    掌握一个合理的策略:一开始不要将任何程序声明为 inline,或至少将inlining实施范围局限在那些"一定要成为inline"(详见条款49)的函数上.不要忘记80-20经验法则:平均而言一个程序往往将80%的执行时间花费在20%的代码上.它提醒:作为一个软件开发者,目标是找出这可以有效增进程序整体效率的20%代码,然后将它 inline 或尽可能的瘦身.
    注意:
    将大多数inlining限制在小型,被频繁调用的函数上,这可使日后的调试过程和二进制升级更容易,也可使潜在的代码膨胀问题最小化,使程序的速度提升机会最大化.

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值