4.4.1 内联(inlning)
通过对适当选择的函数进行内联而节省运行时间是相当重要的。然而,选择正确的内联函数往往要比人们预想中的困难许多。
1.内联的神话
程序员们往往都赞同这样的看法:内联以增加代码数量为代价,加快了程序的执行速度,减少了程序的运行时间。大多数情况下内联的效果确实如此,但并不是所有的情况都是这样。假设某个函数的代码数量非常地小,那么内联扩展所占用的空间将比建立一个函数调用和一个函数返回所需要的空间少;就是说,在这种情况下,程序中使用内联扩展的地方越多,相应地使用程序行调用的地方越少,程序中所包含的代码就会越少。这就不符合上面以增加代码数量为代价的前提。
而且,内联也可以降低程序的执行速度。假设一个大函数、几个小函数或者中等大小的函数在一个循环里面实现内联,从而内联将导致循环中代码实体急剧增加,而且超过了指令缓存的大小(假设在一台有指令缓存的机器上),那么这个程序的执行速度比起没有实现内联的程序的执行速度还要慢。另外,同时各种机器的其他特性对内联的效果也有不利影响。
程序员还可能会这样考虑:只有小的或者中等大小的函数才能是内联的候选函数。但事实是任何函数-无论函数大小-都可以是内联的候选函数。如果在某个特定的位置内联某个函数,那么就可以省去那些由于没有内联而花在参数调入和调出上的时间。现在假设在程序执行频率较高的代码模块中(并且指令缓存并不是目标机器的瓶颈),程序只在一个地方调用某个较大的函数,那么内联这个函数就不会增加代码的数量,但可能会增加程序的运行时间。
2.内联的优缺点
由于使用内联而节省的运行时间将不仅仅包括由于不使用内联而花在压入和弹出参数(也可能是拷贝参数)的时间、返回参数值的时间,从函数调用和返回的时间。而且,内联还经常给程序优化器创造了机会。对于某个程序,内联可能会创建更大的基础块(关于基础块的定义和这里使用的另外一些最优化的概念,请参阅[ASU86])。如果在函数调用时使用了常量参数,那么内联将有利于常量在这整个程序中的传送。而且,在内联函数的实体内,我们将可以省去在调用上下文中进行计算的某些子表达式。从而,在包含内联扩展的函数中,寄存器也可以高效地分配存储空间。
然而,内联有两个潜在的缺点:首先,对于那些由于内联扩展产生的代码多于不使用内联而因函数调用和返回所需要的代码的函数,如果我们在程序中多处使用这个函数,那么程序的代码数量将会增加。代码增加的数量取决于函数调用的次数和函数本身的大小,而且函数本身的大小不能只取决于函数的定义。例如,考虑下面代码:
- void f() {
- g();
- h();
- };
函数f看起来很小,但是如果g函数或者h函数是内联函数的话(或者两者都是内联函数),f的实际代码大小就可能会很大。另一个例子,考虑下面代码:- class D : public B {
- public:
- inline D() { }
- //...
- };
上面代码中,D的构造函数是空函数,但它包含了一个对B的默认构造函数的隐式调用。如果B的构造函数是内联函数的话,那么D的构造函数至少会和B的构造函数一样大。另外,某些编译器还会添加一些其他的开销给D的构造函数,如调用malloc函数分配存储空间等。
内联另一个潜在的缺点是:对内联函数实现的更改往往并不可以保持链接兼容性-这就是说,这样的更改需要用户重新编译他们的代码(我们将在7.5节进一步讨论链接兼容性)。
3.使用内联的时机
很明显,如果我们需要保持某个函数的向前链接兼容性(而不是要获得更快的程序执行速度),我们就不应该把这个函数定义为内联函数。如4.3.2节所讨论的内容,许多编译器拒绝内联某些函数,或者在某种上下文情况下,不会内联某些函数。因此,如果在多个翻译单元中出现许多外联(out-of-line)拷贝的话(指内联由于上面原因失败后执行的操作),那么相对于内联带来的好处,内联带来的坏处将会更多。而且,大多数编译器不能内联对虚函数的调用,因为它很难静态地决定调用内联虚函数的对象的实际类型(我们将在4.4.2节进一步讨论这个话题),因此把虚函数声明为内联函数会使代码数量高度膨胀。于是,对于那些链接兼容性并不是程序的主要问题,而且大多数编译器可以对它们实现内联的函数,我们是否应该进行内联呢?这主要取决于内联这个函数所带来的下面两方向阴影响:代码大小和运行时间(见表4.1)。
表4.1 是否应该内联某个函数
|
内联对运行时间的影响< xml:namespace prefix = o ns = "urn:schemas-microsoft-com:office:office" /> |
内联对代码大小的影响 |
|
减少 |
影响不大 |
增加 |
|
减少 |
是 |
是 |
可能 |
|
影响不大 |
是 |
可能 |
不 |
|
增加 |
可能 |
不 |
不 |
虽然因未能在具体用户的系统上测试某些应用程序,而不能总是知道内联对这些应用程序的代码大小和程序执行速度两方面有何影响,但是我们往往可以知道关于一般用法和用户运行程序的系统方面的信息,从而利用这些信息来做出正确的决定。
如果内联f函数既可以减少用户程序的代码大小,又可以减少程序的执行时间,那么我们当然应该内联这个函数。相似的,如果内联f函数可以减少用户的代码或者程序的执行时间,并对另外一个不产生重大的影响,我们也应该内联这个函数。
如果内联函数f既增加用户的代码大小,又增加程序的执行时间,或者增加其中之一,而对另一个并没有产生得大的影响,那么我们就不应该内联这个函数。
如果内联函数f可以减少种序运行时间,但增加用户程序的代码大小,或者减少用户程序的代码大小,但增加程序运行时间,那么是否对函数f使用内联要取决于在这两个影响中,每个影响的程度和我们的用户更注重哪一部分资源。
如果内联对代码大小和运行时间的影响都是可以忽略不计的,那么是否内联函数f要取决于内联是否有利于程序的进一步优化。