01 January 2005

本文讨论了

  • 为什么建议用内联函数而非宏定义
  • 为什么 inline 只是对编译器的建议
  • 内联机制的一个陷阱

为什么建议用内联函数而非宏定义

在C、C++中函数调用需要少量开销。有时候这少量开销积少成多,对程序性能造成影响。 有时候函数本身很简单,函数调用的开销比执行函数内容本身的开销还大。C程序员一定知 道可以采用宏(Macro)机制来改善上述情况。但是宏基本上是在预编译阶段做文本替换, 因此它有以下缺陷:

  1. 它无法进行类型检查;
  2. 传入有副作用(side effect)的表达式作为参数可能引起微妙的程序臭虫;
  3. 无法单步调试。
  4. 代码膨胀。

内联机制被引入C++作为对宏(Macro)机制的改进和补充(不是取代)。内联函数的参数 传递机制与普通函数相同。但是编译器会在每处调用内联函数的地方将内联函数的内容展 开。这样既避免了函数调用的开销又没有宏机制的前三个缺陷。

为什么 inline 只是对编译器的建议

但是程序代码中的关键字 inline 只是对编译器的建议:被 inline 修饰的函数不一 定被内联(但是无 inline 修饰的函数一定不是)。许多书上都会提到这是因为编译器 比绝大多数程序员都更清楚函数调用的开销有多大,所以如果编译器认为调用某函数的开 销相对该函数本身的开销而言微不足道或者不足以为之承担代码膨胀的后果则没必要内联 该函数。这当然有一定道理,但是按照C、C++一脉相承的赋予程序员充分自由与决定权的 风格来看,理由还不够充分。 我猜想 最主要的原因是为了避免编译器陷入无穷递归。 如果内联函数之间存在递归调用则可能导致编译器展开内联函数时陷入无穷递归。有时候 函数的递归调用十分隐蔽,程序员并不容易发现,所以简单起见,将内联与否的决定权交 给编译器。另一种不被内联的情况是使用函数指针来调用内联函数。

内联机制的一个陷阱

对于C++中内联机制的一个常见误解是:关键字 inline 只是对编译器的建议,如果编译 器发现指定的函数不适合内联就不会内联;所以即使内联使用的不恰当也不会有任何副作 用。这句话只对了一半,内联使用不恰当是会有副作用的:会带来代码膨胀,还有可能引 入难以发现的程序臭虫。

根据规范,当编译器认为希望被内联的函数不适合内联的时候,编译器可以不内联该函数。 但是不内联该函数不代表该函数就是一个普通函数了,从编译器的实际实现上来讲,内联 失败的函数与普通函数是有区别的。

普通的函数在编译时被单独编译一个对象,包含在相应的目标文件中。目标文件链接时, 函数调用被链接到该对象上。 若一个函数被声明成内联函数,编译器即使遇到该函数的声 明也不会为该函数编译出一个对象,因为内联函数是在用到的地方展开的。可是若在调用 该内联函数的地方发现该内联函数的不适合展开时怎么办?一种选择是在调用该内联函数 的目标文件中为该内联函数编译一个对象。这么做的直接后果是:若在多个文件调用了内 联失败的函数,其中每个文件对应的目标文件中都会包含一份该内联函数的目标代码。

如果编译器真的选择了上面的做法对待内联失败的函数,那么最好的情况是:没吃到羊肉, 反惹了一身骚。即内联的好处没享受到,缺点却承担了:目标代码的体积膨胀得与成功内 联的目标代码一样,但目标代码的效率确和没内联一样。运气差的话就会由于存在多份函 数目标代码带来一些程序臭虫。最明显的例子是:内联失败的函数内的静态变量实际上就 不在只有一份,而是有若干份。这显然是个错误,但是如果不了解内幕就很难找到原因。

例子

为证实上述言论,我写了个小例子。此例子在 uninline.h 中定义一个内联函数 uninline 。为了让该函数内联失败,我在 uninline 中添加了很多代码。但是实际 上=uninline= 的工作很简单:每次调用时将函数的静态变量 i 增加10。分别在两个 文件(f1.cc,f2.cc)中调用 uninline 函数。也许你预期每次调用 uninline 后i 的值应当在前一次调用结果的基础上递增10,但是不是所有的编译器都会按照你要求去 做的。至少solaris上CC: WorkShop Compilers 5.0 98/12/15 C++ 5.0编译器的编译结 果不能正常工作。这个例子在gcc version 3.3.3 (cygwin special) 和msvc6sp6上可以 得到正确的结果。但是我不知道那是因为 g++ 和 VC 采用了不同的内联技术还是仅仅是 因为它们的内联门槛比CC要低一些。

运行结果:

$ test.exe
f1
static int i = 10
static int i = 20
f2
static int i = 10
f1 again
static int i = 30

源代码:

//--- test.cc ---
#include "f1.h"
#include "f2.h"

int main()
{
    cout << "f1" << endl;
    f1(); // i should = 10
    f1(); // i should = 20
    cout << "f2" << endl;
    f2(); // i was expected to = 30, but since f2 call another copy of function
          // uninline in f2.o, i = 10
    cout << "f1 again" << endl;
    f1(); // f1 calls uninline in f1.o, so i increase 10 to 30 based on 20
    return 0;
}

//------- f1.h -------
#ifndef F1_H
#define F1_H

#include "uninline.h"

void f1();

#endif

//------- f1.cc -------
#include "f1.h"

void f1()
{
    uninline();
}

//-------f2.h--------
#ifndef F2_H
#define F2_H 1

#include "uninline.h"

void f2();

#endif // F2_H

//--------f2.cc-------
#include "f2.h"

void f2()
{
    uninline();
}

//------ uninline.h -------
#ifndef UNINLINE_H
#define UNINLINE_H 1
#include <iostream>
using namespace std;

inline int recursive(int n)
{
    if (n > 0) {
       return  n * recursive(n-1);
    }
    else {
        return 1;
    }
}

inline void uninline()
{
    static int i = 0;
    int tmp = i + 10;
    for ( ; i <  tmp; ++i) {
        // the following code is only to make this function complicate
        // enough to be not be inlined
        for (int j = 0; j < 10; ++j) {
            char c[10];
            char* s = c;
            for (int k = 0; k < sizeof(c) -1; ++k) {
                *s = c[k];
                if (*s != '\0') {
                    *s == '\0';
                }
            }
        }
        recursive(i);
    }
    cout <<"static int i = " << i << endl;
}

#endif // UNINLINE_H


blog comments powered by Disqus