引子: 有些程序员过于关注程序的效率;由于太在乎细小的“优化”,他们编写出的程序过于竞标,难以维护。而另外一下程序员很少关注程序的效率;他们编写的程序有着清晰漂亮的结构,但效率极低以至于毫无用处。优秀的程序员将程序的效率难入整体考虑之中:效率只是软件中的众多问题之一,但有时候也很重要。
9.1 典型的故事
问题中到了Van Wyk将一个3000行的程序的运行时间减少了一半。Van做的首先是监视程序的性能,以确定每个函数需要花费的时间,他发现将近70%的运行时间都用在了内存分配函数malloc上,于是他决定利用高速缓存来避免使用malloc。 最终,他成功了。
9.2 急救方案集锦
作者写出了一些比较实用的小技巧,个人感觉还是挺有用的。
1. 整数取模
mod还是比较花时间的,有时候可以考虑使用减法。例如 mod n, 有时可以使用 -n来代替。
“在过去,程序员知道,如果程序的运行时间主要消耗在输入输出上,那么对程序中的计算进行加速是毫无意义的。在现代的体系结构中,如果对内存的访问占用了大量的运行时间,那么减少计算时间同样是毫无意义的。”
2.函数、宏、内联代码
我已经看到很多次不提倡用宏(#define)了,理由有很多,宏只是字符串替换,不能进行类型检查,有时还能会影响程序效率。
习题中举出了一个例子,arrmax(int n)是找出数组中的最大元素,代码如下:
float arrmax(int n) { if(n == 1) return x[0]; else return max(x[n-1],arrmax(n-1)); } #define max(a,b) ((a) > (b) ? (a) : (b))
程序看起来没有错误,但是由于max中的b是一个函数的返回值,假如每次比较都是b大(函数返回值),那么判断a > b会计算一次arrmax,返回结果时又会计算一次arrmax。这样就会重复很多很多次(2的指数级增长),如果换成max函数就不会。我自己做了一次试验,发现用max宏运算时间是3秒,用max函数0秒(估计是小于1秒)
3.顺序搜索
在数组中遍历搜索时,for循环中会判断两次,一次是 i < n,一次是 x[i] == t。作者使用哨兵技术就避免了判断 i < n
int search(t) hold = x[n] x[n] = t for (i = 0 ; ; i++) { if x[i] == t break x[n] = hold if i == n return -1 //表示没有找到 else return i }
作者最后展示了一种更快的算法,不过我不推荐。有兴趣的同学可以自己下来看看。
4.计算球面距离
在计算地球表面两个点时,需要使用非常复杂的计算方法,计算时占用了大量的时间。
“Andrew Appel发现了一个关键点:为什么一定要在数据结构的层面解决这个问题呢?为什么不使用简单的数据结构,将这些点保存在一个数组中,通过调优代码来降低个点之间距离的计算开销”
9.3 大手术——二分搜索
作者在这一节主要对二分搜索进行了代码调优,共给出了3个优化的二分搜索的代码。但是我觉得除了第1个之外,剩下的两个都有一些特殊的技巧在里面,给人一点华而不实的感觉。
现在给出代码
原始二分查找:
l = 0 u = n-1 loop if l > u p = -1 break; m = (l+u) / 2 case x[m] < t : l = m + 1 x[m] == t : p = m; break; x[m] > t : u = m - 1
优化的二分查找:
l = -1 u = n while l + 1 != u { m = (l + u) /2 if x[m] < t l = m else u = m } p = u if p > n || x[p] != t p = -1
优化过的代码只有一次判断,如果x[m] >=t 那么就 u = m。在程序的最后,使用最后的if来断定是找到t还是没有找到。但是由于程序中没有break,也就是说只有l+1 == u的时候循环才会退出,假如程序运行第一次就找到t,由于没有break,程序还是要坚持把while走完。
9.4 原理
作者提出了一些代码调优的原理。 效率的角色。 不成熟的优化是大量编程灾难的根源,当可能的危害影响较大时,请考虑适当将效率放一放。 度量工具。 当效率很重要时,我们要确定某些大量消耗时间的代码,然后进行修改。“我们遵循有名的格言是:没有坏的话就不要修”。 设计层面。效率问题可以有多种方法来解决,参考第六章。只有在确信没有更好的解决方案时才考虑进行代码调优。 双刃剑。在进行“改进”之后,用具有代表性的输入来度量程序的效果是至关重要的。“小心玩火自焚”。