Effective C++——条款30(第5章)
来源:互联网 发布:信息管理平台软件 编辑:程序博客网 时间:2024/05/12 22:05
条款30: 透彻了解 inlining 的里里外外
Understand the ins and outs of inlininginline 函数看起来像函数,动作像函数,比宏好得多(详见条款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
- Effective C++——条款30(第5章)
- Effective C++——条款5(第2章)
- Effective C++——条款26(第5章)
- Effective C++——条款27(第5章)
- Effective C++——条款28(第5章)
- Effective C++——条款29(第5章)
- Effective C++——条款31(第5章)
- Effective C++——条款10条,条款11和条款12(第2章)
- Effective C++——条款3(第1章)
- Effective C++——条款4(第1章)
- Effective C++——条款6(第2章)
- Effective C++——条款7(第2章)
- Effective C++——条款8(第2章)
- Effective C++——条款9(第2章)
- Effective C++——条款13(第3章)
- Effective C++——条款14(第3章)
- Effective C++——条款15(第3章)
- Effective C++——条款16(第3章)
- POJ 1273:Drainage Ditches 网络流模板题
- 删除环状单向链表中的重复元素的操作
- 包装类的使用
- JAVA上机作业 2.6
- MATLAB:读取文本数据并排序
- Effective C++——条款30(第5章)
- vim的taglist插件的使用和配置
- 深入浅出Android makefile(1)--初探
- Python爬虫框架Scrapy实战之批量抓取招聘
- Chapter4.17 Symbolic link
- Mongodb的常用命令
- Eclipse-properties-android崩溃
- GLPubSub - Glow iOS 中的订阅发布模式
- hdu 2089 不要62