现在的位置: 首页 > 综合 > 正文

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

2014年03月24日 ⁄ 综合 ⁄ 共 2298字 ⁄ 字号 评论关闭

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

#define max(a, b) ( (a) > (b) ? (a) : (b) )

int a = 5, b = 0;
max(++a, b); // a = a + 2
max(++a, b+10); // a = a + 1

如果这样:

inline int max(int a, int b)
{
     return a > b ? a : b;
}

int a = 5, b = 0;
max(++a, b); // a = a + 1
max(++a, b+10); // a = a + 1

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

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

class stack {
private:
  int i;
  
public:
  int get() {return i;} // inline函数
}; 

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

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

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

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

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

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

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

int test(){
  int a = 6;
  ...... // 此处省略代码未对a经行修改
  int b = inline_func(b);
  ...... // 此处省略代码未对b经行修改
  int c = b + 1;
  ......
}

inline int inline_func(int q) {
  if (q > 10) return -1;
  else if (q > 0) return (1 << q) - 1;
  else return 0;
}

inline后

int test() {
  int a = 6;
  ...... // 此处省略代码未对a经行修改
  int b;
  {
    int _temp_q = 6;
    int _temp;
    if (_temp_q > 10) _temp = -1;
    else if (_temp_q > 0) _temp = (1 << q) - 1;
    else _temp = 0;
    b = _temp;
  }
  ...... // 此处省略代码未对b经行修改
  int c = b + 1;
  ......
}

优化后

int test(){
  int a = 6;
  ...... // 此处省略代码未对a经行修改
  int b = 0x3f;
  ...... // 此处省略代码未对b经行修改
  int c = 0x40;
  ......
}

上面我们主要说了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化。

参考文献:

1、《C++语言的设计和演化》2.4 运行时的效率

2、《Effective C++》 条款33

3、《提高C++性能的编程技术》 第8章 内联基础,第9章 内联-站在性能的角度, 第10章 内联技巧

4、《C++箴言:理解inline化的介入和排除

5、《C++ inline 函数

抱歉!评论已关闭.