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

来源:互联网 发布:信息管理平台软件 编辑:程序博客网 时间:2024/05/12 22:05

条款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"的冲击:inline 函数无法随着程序库的升级而升级.换句话说如果f是程序库内的一个 inline 函数,客户将"f函数本体"编进程序中,一旦程序库设计者决定改变f,所有用到f的客户端程序都必须重新编译.这往往是大家不愿看到的.然而如果f是non-inline 函数,一旦它有任何修改,客户端只需要重新连接就好,远比重新编译的负担少很多.
    从纯粹使用观点出发:大部分调试器面对 inline 函数都束手无策.毕竟不可能在并不存在的函数内设立断点.
    掌握一个合理的策略:一开始不要将任何程序声明为 inline,或至少将inlining实施范围局限在那些"一定要成为inline"(详见条款49)的函数上.不要忘记80-20经验法则:平均而言一个程序往往将80%的执行时间花费在20%的代码上.它提醒:作为一个软件开发者,目标是找出这可以有效增进程序整体效率的20%代码,然后将它 inline 或尽可能的瘦身.
    注意:
    将大多数inlining限制在小型,被频繁调用的函数上,这可使日后的调试过程和二进制升级更容易,也可使潜在的代码膨胀问题最小化,使程序的速度提升机会最大化.

0 0
原创粉丝点击
热门问题 老师的惩罚 人脸识别 我在镇武司摸鱼那些年 重生之率土为王 我在大康的咸鱼生活 盘龙之生命进化 天生仙种 凡人之先天五行 春回大明朝 姑娘不必设防,我是瞎子 声卡驱动删除了怎么办 白色水彩没有了怎么办 大学毕业我想考军校怎么办? 考军校分数不够怎么办 大专工作不好找怎么办 小学二年级插班怎么办 进厂年龄不到怎么办 入伍批准书丢了怎么办 考驾照期间参军怎么办 学位房被占用怎么办 教室里回音太大怎么办 教室里味道太大怎么办 键盘只能打拼音怎么办 一师一优课件上传慢怎么办? 药店买药不给退怎么办 小孩热感冒发烧怎么办 孕妇热感冒了怎么办 孕妇热感冒喉咙痛怎么办 空军一号有划痕怎么办 高铁上乘客太吵怎么办 军官礼服丢了怎么办 空军大檐帽帽袋坏了怎么办 保安不发工资怎么办 做保安工资不资不发怎么办 公安改革辅警怎么办 皮带带子丢了怎么办 警校学生证丢了怎么办 警校证丢了怎么办 盘查没带身份证怎么办 网线拔不出来怎么办 车间压强差过大怎么办 不遵守交通规则交警怎么办 西裤屁股磨出光该怎么办 中暑发烧不退烧怎么办 小孩中暑反复发烧怎么办 上火导致的发烧怎么办 夏季运动中暑后怎么办 感觉中暑了头疼怎么办 轻度中暑怎么办小妙招 人中暑了头疼怎么办 中暑怎么办三字步骤