对于程序运行效率的改进可以从以下几个方面入手:调整代码顺序以避免重复的复杂运算、改进算法和数据结构以降低计算复杂度、了解和掌握硬件的特性以便充分发挥硬件系统的性能、以及使用编译系统的优化选项对程序的可执行码进行优化。
代码调整是一种最简单的程序优化技术,易于掌握和使用。一般来说,代码调整应该作为一种优化的辅助手段,在对算法和数据结构优化的基础之上进行。但是,由于这种技术比较便于初步接触编程和程序优化的人员理解和掌握,而且如果使用得当,常常可以取得显著的效果,因此我们首先讨论这一优化技术。
代码调整包括提取和集中处理公共表达式、将不变式条件移出循环体、将条件判断移出循环体、展开代码、预先计算、以及用低价操作替代高价操作等。这些方法分别适用于不同的条件,既可以单独使用,也可以综合使用。
1. 提取公共表达式
提取和集中处理公共表达式的基本思想是,对于需要多次使用的表达式的值只计算一次,并将计算结果保存在变量中,以避免对相同的表达式多次求值。例如,在下列表达式中:
x = sqrt(dx * dx + dy * dy) + (sqrt(dx * dx + dy * dy) > 0) ? vx : -vx;
子表达式sqrt(dx * dx + dy * dy)被使用了两次。因此在这个表达式的计算中可以把该子表达式提取出来单独计算和保存。这样,上述表达式的计算可以改写为:
sq = sqrt(dx * dx + dy * dy);
x = sq + (sq > 0) ? vx : -vx;
2. 将与循环无关的表达式移出循环语句
当一个表达式处在循环语句中时,每一次循环都会对其求值。与循环无关的表达式只需要求值一次即可,将与循环无关的表达式移出循环语句可以避免不必要的重复计算。例如,假设在下面的代码中,变量k的值在循环语句中不改变:
for (i = 0; i < MAX_I; i++) {
for (j = 0; j < MAX_J; j++) {
x = sqrt(k);
... ...;
那么,语句x = sqrt(k)就是与循环无关的表达式,可以将其移出循环语句:
x = sqrt(k);
for (i = 0; i < MAX_I; i++) {
for (j = 0; j < MAX_J; j++) {
... ...;
有时,一些重复求值的表达式不易被注意到。例如,在下面代码的内层循环语句中,arr[i]就是一个根据下标对数组元素求值的表达式。因为该数组下标i在内层循环中是不变的,所以arr[i]也是一个重复求值的表达式:
for (i = 0; i < MAX; i++) {
for (j = 0; j < arr[i]; j++) {
... ...;
这段代码可以改写为下面的形式,以提高效率:
for (i = 0; i < MAX; i++) {
k = arr[i];
for (j = 0; j < k; j++) {
... ...;
}
除了在这些底层代码中有可能在循环语句中出现可以被移出的与循环无关的表达式之外,在程序的计算过程中,在更高的层次和更大的范围上也有可能存在有包含在循环过程中的与循环无关的操作。将这些与循环无关的操作移出循环体所产生的效果可能更加显著。例如,假设我们需要产生图片沿给定的路径飞入屏幕的动画效果,这一效果可以通过将图片按一定的时间间隔显示在给定的路径上来实现。如果图片的实际尺寸与其在屏幕上所要显示的大小不同,就需要在显示图片之前将其缩放到所需要的尺寸。对此,一种可能采取的做法是不断地执行将图片缩放到所需要的显示尺寸,然后再根据动画效果中规定的路线将其复制到指定的位置的操作序列。如果在图片飞入的路径中需要在N个位置上复制图片,那么这一操作序列所需要的操作就是N 次缩放加N 次复制。如果在飞入过程中图片的大小不需要改变,那么对图片的缩放就是与循环无关的操作,因此可以移出循环过程。这样,显示动画效果的操作序列就可以改为一次缩放加N次复制。与图片的直接复制相比,对图片的缩放是一个比较耗时的操作,减少图片的缩放次数可以有效地提高操作序列的效率。
3. 将与循环无关的条件判断移出循环语句
在循环语句的循环体中常常包含有条件语句。如果这些条件语句与循环变量没有任何直接或间接的关系,那么就可以将这些条件移到循环语句之外,以便改进程序的运行效率。例如,在下面的代码中条件语句if (agent_works(a)) 需要执行MAX次:
for (i = 0; i < MAX; i++) {
if (agent_works(a)) {
... ...
else {
... ...
}
}
如果条件if (agent_works(a))与循环无关,那么可以把代码改成如下的形式:
if (agent_works(a)) {
for (i = 0; i < MAX; i++) {
... ...
}
else {
for (i = 0; i < MAX; i++) {
... ...
}
}
尽管代码长度有所增加,但是条件判断语句if (agent_works(a))却只需要执行一次。当循环的次数MAX 较大以及if语句中条件表达式的计算比较复杂时,这种改进的效果是显著的。
4. 展开循环体中的代码
当程序执行循环语句时,除了要执行循环体中的代码外,还需要执行循环控制语句,包括循环条件的检查以及语句执行的流向控制,而这些语句也需要占用CPU运行时间。当循环体很小的时候,循环控制语句所占用的CPU运行时间在整个循环语句的执行中所占的比例会很大。如果在执行语句的流向控制时引起了CPU中指令流水线的断流,则可能更显著地影响程序的效率。这时展开循环体中的代码,使之按顺序执行,可能会增加一些代码的长度,但是对于程序的执行速度会有较大的帮助。下面是一个展开循环体中代码的例子:
for (i = 0; i < 3; i++)
a[i] = b[i] + c[i];
在IA32/Linux环境下,执行这段代码大约需要450个时钟周期。这段代码可以改写为下面的样子:
a[0] = b[0] + c[0];
a[1] = b[1] + c[1];
a[2] = b[2] + c[2];
在同样的环境下,展开后的代码的执行只需要大约140个时钟周期,不到原来代码的1/3。如果这段代码在程序中只被执行一次,那么这一改进微不足道。但是,如果这段代码是包含在其他需要多次执行的代码中的,那么这一改进对程序的执行效率的影响可能就相当可观了。
5. 预先计算可能用到的数值
这是一种典型的用存储空间换取运行时间的技术,常常用于数值计算、图像处理、信号处理等方面的编程中。在这类程序中,常常会用到大量的函数值、质数表、或多项式的系数表等。这些函数值、质数表或系数表既可以在被用到时临时调用相应的函数或计算公式进行计算,也可以事先计算出来保存在相应的表格中,以便减少计算工作量,提高程序运行速度。以三角函数为例, sin()、cos()等三角函数是信号处理中经常用到的函数,也是比较耗时的数值计算函数。如果在程序中需要大量地使用三角函数,就以预计算好这些三角函数的值,保存在数组sin[]和cos[]中,并在程序中以对数组元素的访问代替对三角函数的调用。例如,如果程序中需要使用以度为自变量单位的正弦函数和余弦函数的值,就可以定义三角函数表如下:
double sin_tab[] = {
... ... // 以度为自变量单位的sin值
}
double cos_tab[] = {
... ... // 以度为自变量单位的cos值
}
这样,程序中对正弦函数和余弦函数的调用就可以转换为对数组元素的访问。例如,下面的代码:
x = sin(r);
就可以改写为
x = sin_tab[(int) (r * 360 / M_PI)];
三角函数是一种复杂的数值计算函数。在很多计算平台上,上述两条语句的执行时间相差百倍以上。
6. 用低价操作替代高价操作
同一个计算过程,在C语言中往往可以使用不同的机制和描述方式。这些在功能上等价的计算机制和描述方式在性能上可能会有比较大的差别。因此在计算中使用性能较高的“低价”操作替代性能较低的“高价”操作,是程序优化中的一种常用技术。例如,当将局部数组变量的所有元素初始化为0时,一般可以使用的下面的两种方法:
(1) int i, array[N_ITEMS];
for (i = 0; i < N_ITEMS; i++)
array[i] = 0;
(2) int array[N_ITEMS] = {0};
初学者往往习惯于使用第一种方法。但是实际上第二种方法不但运行效率远高于第一种方法,而且描述也更加简洁。又例如,当把一个整数数组的全部元素都置为-1时,可以使用下列语句:
int array[N_ITEMS];
memset(array, 0xff, sizeof(int) * N_ITEMS);
这个语句的执行效率与使用循环语句的效率差异也是显著的。再例如,在计算过程中常常需要判断一个变量x的平方根是否大于或小于某一个数值。最直观的描述是:
if (sqrt (x) > y)
因为sqrt()是一个浮点数值计算函数,所以其运行效率较低。在对效率有较高要求的程序中,常常使用if (x > y * y)来代替if (sqrt (x) > y)。这两种语句在语义上是完全等价的,但是执行效率的差距很大。在【例8-1】Antiprime的函数divisors()中就有这样的语句:i >= sqrt(n)。这一语句可以用等价的i * i >= n来取代。在参数为2*106时,未调整代码前的程序需要运行67.13秒,而代码调整后的程序只需要运行31.09秒。一个小小的改动就改进了整个程序的运行效率超过50%。
此外,在大多数计算平台上,浮点类型的计算速度要低于整型数据的计算。特别是当计算平台中没有提供浮点运算部件时,这一差距就更加显著。如果在程序中需要进行大量的计算,在对运行效率要求较高的情况下,应当分析运算的性质,以及初始数据、中间结果、最终结果的数据类型、取值范围和精度要求。如果可以使用整型数据类型完成计算,就不必使用浮点数据类型。这样可以有效地提高程序的运行速度。实际上,早期的计算平台大多没有浮点运算部件,很多数值计算程序都使用比例因子把浮点数转换成整型数进行计算,以提高程序的计算速度。
7. 避免无效语句
初学者的程序中有时容易包含一些无效语句,例如无用的初始化、未被使用的表达式计算和赋值、以及对一个变量的重复赋值等。无效语句的出现往往反映了编程人员的思维不够严密,编程方法不够系统。也有一些无效语句是在对程序的反复调试和修改过程中引入的。如果这些无效语句只是被执行一次,那么它们除了影响到程序的风格和简洁程度外,对程序执行效率的影响并不大。但是如果这些无效语句是被包含在一些需要频繁执行的程序段落中的,那么就可能对程序的效率产生较大的影响,并且有可能引起潜在的错误。避免无效语句的关键在于注意编码方法的系统化和规范化。在编码之前对于相关的计算过程和算法的描述应当严格、准确,并认真推敲,在这一层面上避免描述的冗余和无效。编码应当严格按照对计算过程的描述进行。当编码和调试过程中出现与描述不一致的情况时,应当在描述层面上认真考虑所涉及的改动是否正确、是否必要。在调试完毕后应当清理在调试过程中加入的临时性语句。当然,要做到完全避免程序中出现无效代码,经验的积累也是必要的。
8. 避免不必要的重复操作
重复操作也属于无效操作之类,其对程序效率的负面影响是显著的,特别是当被无效重复的是比较复杂的操作时。这方面常见的例子有对同一文件的反复打开和关闭,以及对内存的重复分配和释放。例如,如果需要在程序中不同的段落和不同的时间对同一个文件进行多次读写,那么就应该在第一次对文件读写操作时打开文件,在完成最后一次读写操作后再关闭文件,而不应该在每次读写操作时打开文件,在读写完毕后立即关闭文件,除非程序中同时打开的文件数量过多,有可能妨碍程序的正确运行。