小问题大思考之C++里的inline函数

inline,一个神奇的关键字。有了它,你同时就可以获取函数和宏的优点。inline定义的函数,比起没有inline的函数来说,没有执行函数调用所带来的负担(对此可参见《C++程序的内存布局》),因此它是高效率的;比起宏来,它具有函数的可预期行为和参数类型检验。宏的行为难于预期,我们看看下面这个宏定义

 

[cpp][/cpp] view plaincopy

  1. #define max(a, b) ( (a) > (b) ? (a) : (b) )
  2. int a = 5, b = 0;
  3. max(++a, b); // a = a + 2
  4. max(++a, b+10); // a = a + 1

如果这样:

 

 

[cpp][/cpp] view plaincopy

  1. inline int max(int a, int b)
  2. {
  3.      return a > b ? a : b;
  4. }
  5. int a = 5, b = 0;
  6. max(++a, b); // a = a + 1
  7. max(++a, b+10); // a = a + 1

一切都很美好!但是会这么简单吗?

 

C++最初引入inline的原因是不想破坏类的封装,同时保持高效率。例如:

 

[cpp][/cpp] view plaincopy

  1. class stack {
  2. private:
  3.   int i;
  4. public:
  5.   int get() {return i;} // inline函数
  6. };

想访问stack的成员变量i,想保持stack的封装,同时还想调用时高效率,那么请inline。

 

inline对于编译器而言,意味着“在编译阶段,将调用动作以被调用函数的本体替换之”。但是它只是一种建议,编译器可以去做,也可以不去做。从逻辑上来说,编译器将函数inline的步骤如下:

1、将inline函数体复制到inline函数调用点处;

2、为所用inline函数中的局部变量分配内存;

3、将inline函数的的输入参数和返回值映射到调用方法的局部变量空间中;

4、如果inline函数有多个返回点,将其转变为inline函数代码块末尾的分支(使用GOTO)。

经过以上处理,可消除所有与调用相关的痕迹以及性能的损失。inline通过消除调用开销来提升性能,并且允许进行调用间优化。我们看下面这段代码:

 

[cpp][/cpp] view plaincopy

  1. int test(){
  2.   int a = 6;
  3.   …… // 此处省略代码未对a经行修改
  4.   int b = inline_func(b);
  5.   …… // 此处省略代码未对b经行修改
  6.   int c = b + 1;
  7.   ……
  8. }
  9. inline int inline_func(int q) {
  10.   if (q > 10) return -1;
  11.   else if (q > 0) return (1 << q) – 1;
  12.   else return 0;
  13. }

inline后

 

 

[cpp][/cpp] view plaincopy

  1. int test() {
  2.   int a = 6;
  3.   …… // 此处省略代码未对a经行修改
  4.   int b;
  5.   {
  6.     int _temp_q = 6;
  7.     int _temp;
  8.     if (_temp_q > 10) _temp = -1;
  9.     else if (_temp_q > 0) _temp = (1 << q) – 1;
  10.     else _temp = 0;
  11.     b = _temp;
  12.   }
  13.   …… // 此处省略代码未对b经行修改
  14.   int c = b + 1;
  15.   ……
  16. }

优化后

 

 

[cpp][/cpp] view plaincopy

  1. int test(){
  2.   int a = 6;
  3.   …… // 此处省略代码未对a经行修改
  4.   int b = 0x3f;
  5.   …… // 此处省略代码未对b经行修改
  6.   int c = 0x40;
  7.   ……
  8. }

上面我们主要说了inline函数的优点,那么inline函数的缺点有哪些呢?我们来看看:

 

1、代码膨胀。如果inline函数体过大且编译器还让它inline成功,那么你最终的程序会代码膨胀,从而造成设备缓冲命中率低,引起较多的页面错误,读写硬盘的次数增多,这样程序的性能就下降了!建议:inline函数体一般不要超过5行,不包括循环,不包括递归调用。

2、inline函数内部不要有static变量。inline函数的定义几乎总是放在头文件(.h)里,这允许多个实现文件(.cpp)得以引用。我们知道编译器是分别编译的,所以这个时候,在多个实现文件里就会有多个inline函数的展开,也就是说有个多个static变量,这恐怕不是我们期望的!

3、inline函数无法随着函数库升级而升级。如果f是函数库中的一个inline函数,使用它的用户会将f函数实体编译到他们的程序中。一旦函数库实现者改变f,所有用到f的程序都必须重新编译。如果f是non-inline的,用户程序只需重新连接即可。如果函数库采用的是动态连接,那这一升级的f函数可以不知不觉的被程序使用。

4、不要获取inline函数的地址。如果要取得一个inline函数的地址,编译器就必须为此函数产生一个函数实体,无论如何,编译器无法交出一个“不存在函数”的指针。注意,有些编译器可能会使用类的constructors和destructors的函数指针,用以构造和析构一个class对象的数组。另外类的constructors和destructors可能简单,但是其父类的类的constructors和destructors可能是复杂的,所以类的constructors和destructors往往不是inline函数的最佳选择

5、inline虚函数往往是无效的。虚函数往往是运行时确定的,而inline是在编译时进行的,所以inline虚函数往往无效。当然如果直接用类的对象来使用虚函数,那么对有的编译器而言,也可起到优化的作用。

6、inline函数无法调试。原因请参见上面编译器将函数inline的步骤。所以请在项目后期,对程序进行profile后,再决定将那些函数inline化。

标签