[Effective C++ --030]透彻了解inlining的里里外外

引言  inline函数

在函数声明或定义中函数返回类型前加上关键字inline即把min()指定为内联。

inline函数对编译器而言必须是可见的,以便它能够在调用点内展开该函数。与非inline函数不同的是,inline函数必须在调用该函数的每个文本文件中定义。当然,对于同一程序的不同文件,如果inline函数出现的话,其定义必须相同。

为保证不会发生这样的事情,建议把inline函数的定义放到头文件中。在每个调用该inline函数的文件中包含该头文件。这种方法保证对每个inline函数只有一个定义,且程序员无需复制代码,并且不可能在程序的生命期中引起无意的不匹配的事情。

第一节 C++ inline函数的使用

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

当你inline某个函数,或许编译器就因此又能力对它(函数本体)执行语境相关最优化。
然而,inline函数背后的整体观念是,将“对此函数的每一个调用”都已函数本体替换之,这样做可能增加你的目标码(object code)大小。在内存有限的机器上,过度inline会造成程序体积太大,导致换页行为,降低缓存的命中率等一些带来效率损失的行为。
如果inline函数的本体很小,编译器针对“函数本体”所产生的码可能比针对“函数调用”所产出的码更小。将函数inline可以导致更小的目标码,从而提高效率。

1.inline函数的声明:

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

隐喻方式:

1 class Person{
2 public:
3     ...
4     int age() const {return theAge;}               //一个隐喻的inline申请
5     ...
6 private:
7     int theAge;
8 };

明确声明:

1 template<typename T>
2 inline const T& std::max(const T& a, const T& b)   // 明确申请inline
3 {
4     return a < b? b: a;
5 }

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

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

2.inlining与virtual

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

inline:执行前,先将调用动作替换为被调用函数的本体

virtual:等待,直到运行期才确定调用哪个函数

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

1 inline void f(){…}      //假设编译器有意愿inline“对f的调用”
2 void (*pf)() = f;
3 f();                    //这个调用将被inlined,因为是一个正常调用
4 pf();                   //这个调用或许不被inlined,因为通过指针达成

即时你从未使用函数指针,“未被成功inlined”的inline函数还是有可能缠住你,因为程序猿并非唯一要求函数指针的人!有时候编译器会生成构造函数和析构函数的outlined副本,如此一来他们就可以获得指针指向那些函数吗,在array内部元素的构造和析构过程中使用。

3.inline与构造/析构函数

实际上构造函数和析构函数往往都是inlining的糟糕候选人。

 1 class base {
 2 public:
 3     ...
 4 private:
 5     std::string bm1, bm2;
 6 };
 7
 8 class Derived : public Base {
 9 public:
10     Derived(){}                     //Derived 构造函数是空的 是吗?
11     ...
12 private:
13     std::string dm1, dm2, dm3;
14 };

这个构造函数看起来是inlining的绝佳候选人,因为他根本不含任何代码,但是你的眼睛会欺骗你!

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

 1 Derived::Derived()
 2 {
 3    Base::Base();
 4     try{dm1.std::string::string();}
 5     catch(...){
 6         Base::~Base();
 7         throw;
 8     }
 9     try{dm2.std::string::string();}
10     catch(...){
11         dm1.std::string::~string();
12         Base::~Base();
13         throw;
14     }
15     try{dm3.std::string::string();}
16     catch(...){
17         dm2.std::string::~string();
18         dm1.std::string::~string();
19         Base::~Base();
20         throw;
21     }
22 }

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

相同的理由也适用于Base构造函数。

程序库设计者必须评估“将函数声明为inline”的冲击:inline函数无法随着程序库的升级而升级。f是程序库内的一个inline函数,客户将“f函数本体”编进其程序中,一旦程序库设计者决定改变f,所有用到f的客户端程序都必须重新编译。然而若f是non-inline函数,客户端只要重新连接就好了,如果是程序库采用动态链接,升级后的函数甚至可以不知不觉的被应用程序吸纳。
从实用观点出发,大部分调试器对inline函数都束手无策。

◆总结

1.将大多数inlining限制在小型及被频繁调用的函数身上。这可使日后的调试过程和二进制升级(binary upgradability)更容易,也可使潜在的代码膨胀问题最小化,使程序的速度提升机会最大化。

2.不要只因为function templates出现在头文件,就将它们声明为inline。

时间: 2024-09-28 17:33:32

[Effective C++ --030]透彻了解inlining的里里外外的相关文章

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

(一) inline函数,可以调用它们而又不需蒙受函数调用所招致的额外开销. inline函数背后的整体观念是,将"对此函数的每一个调用"都已函数本体替换之,这样做可能增加你的目标码(object code)大小.在内存有限的机器上,过度inline会造成程序体积太大,导致换页行为,降低缓存的命中率等一些带来效率损失的行为.如果inline函数的本体很小,编译器针对"函数本体"所产生的码可能比针对"函数调用"所产出的码更小.将函数inline可以

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

inline函数为什么放到头文件 inline函数是特殊的函数,它有宏的优点,却克服了宏的缺点(**条款**2).inline函数可以免除函数调用所招致的额外开销,但你实际获得的好处可能比你想象的还多,编译器会对inline函数本体执行语境相关最优化. 但使用inline函数会导致目标码(object code)变大,因为对inline函数的调用都会以函数本体替换.在内存比较小的机器上,不宜过多使用inline函数.即使使用虚拟内存,也会导致额外的换页行为(paging),降低指令高速缓存装置的

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

1. inline函数既和带参宏一样不带来函数调用的额外开销,又具有和非inline函数相同的功能,也就是说,inline函数同时具备带参宏和非inline函数的优点. 此外,编译器优化机制通常针对于那些不含参数调用的代码,因此inline某个函数就有可能使编译器对它执行语句相关最优化. 2. 虽然inline函数有诸多优点,但由于inline函数对每一次函数调用都用函数本体替换,这无疑加重了编译负担,更重要的是它增加了代码量,此外,由于inline造成的代码膨胀"会导致额外的换页行为(pagi

[Effective C++系列]-透彻了解inlining的里里外外

Understand the ins and outs of inlining. [原理] Inline函数背后的做法是将“对函数的每一个调用”都用函数本体替换之.其好处是: 可以消除函数调用所带来的开销. 编译器最优化机制通常被设计用来浓缩那些“不含函数调用”的代码,因此当你inline某个函数,或许编译器有能力对它(函数本体)执行语境相关最优化.大部分编译器不会为一个“outlined函数调用”执行这种最优化动作. 然而inline函数这些美好的一面也伴随着代价:inline函数可能增加程序

条款30:透彻了解inline的里里外外(understand the ins and outs of inlining)

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

条款30:透彻了解inline的里里外外。

inline可以带来各种好处: 首先其可以使得消除函数调用带来的开销,再者编译器对这种非函数的代码可以做出更多的优化策略. 但是inline函数首先肯定是会导致程序代码的大小更加的庞大,这样会带来代码膨胀,造成的损害首先就是会导致计算机额外的换页行为,降低指令告诉缓存的集中率,这要就会带来效率上的损失. 所以说,使用inline的关键点就在于,如果inline函数的本体很小的话,那么inline函数展开的代码大小可能比调用非inline函数展开的代码更小,这样就可以提高缓存的击中率,从而提高函数

《Effective C++》读书笔记汇总

我之前边读<Effective C++>边写下每个条款的读书笔记,这一版是C++11之前的版本.这里我将每个条款令我印象深刻的点小结一下. 1.C++包括:Plain C(面向过程).OOP(面向对象).模板(泛型和模板元编程).STL(C++标准库). 2.用inline.enum.const代替#define.#define定义的宏,一旦复杂起来,高手都很难掌控.不要带入C的习惯. 3.灵活使用const前缀.不需要进行改变的数据加上const前缀.指针的const前缀有两种形式,cons

学习日记之适配器模式和Effective C++

适配器模式(Adapter):将一个类的接口转换为客户希望的另一个接口.Adapter 模式使得原本由于接口不兼容而不能一起工作的那些类可以一起工作. (1),系统的数据和行为都正确,但接口不符时,我们应该考虑适配器模式,目的是使控制范围之外的一个原有对象与某个接口匹配.适配器模式主要应用于希望复用一些现存的类,但是接口又与复用环境要求不一致的情况. (2),使用一个已经存在的类,但如果他的接口,也就是他的方法和你的要求不相同时,经营该考虑适配器模式. (3),两个类所做的事情相同或相似,但是具

C++基础和STL,Effective C++笔记

C++基础 static static变量存储在静态数据区 相对于function:在函数内,变量,内存只被分配一次,多次调用值相同 相对于其他模块(.c文件):变量和函数,不能被模块外其他函数访问(private) 相对于类:类中的static变量和函数属于整个类,而不是对象 全局变量 VS 全局静态变量 若程序由一个源文件构成时,全局变量与全局静态变量没有区别. 若程序由多个源文件构成时,全局变量与全局静态变量不同:全局静态变量使得该变量成为定义该变量的源文件所独享,即:全局静态变量对组成该