编程珠玑第九章探讨

本章的内容让我想到了去年参加超算大赛的经历。当时所做的诸多事务中有一环就是代码调优,列举一下调优的经过大概就是:分析代码,找到热点,重构函数,调优参数。在这一章中,热点的寻找和重构函数都有提及,因为是一篇介绍性质的文章所以没有说明怎样调优一个系统。不过书中对于最基础的调优的介绍已经充分了,而且在更多的情况下,代码调优也差不多就是指代这些。

代码调优的最重要原理就是尽量少用它

问题探讨

本章的内容分析了包括基于高速缓存原理的调优,宏定义调优,代码展开等。当我们在调优一个大型的系统时,往往调优的顺序是先考虑串行调优,这时候我们会首先在编译参数上动手脚,因为这是最省事的,然后会进行一些指令级并行的调优,例如代码展开,循环重构等,都有助于高效利用缓存并增加指令并行的可能,当然如果在更高阶的层面上可以优化,比如等价的代数表达或者更换算法的话,固然更好;另一方面,我们还会进行并行的调优,第一步也是编译参数调试,然后我们可能会改写OpenMP或者MPI的部分使得通信瓶颈得到优化,但是这些不在我们的讨论范围内,只提一句,不必细说。

不论是循环展开亦或是循环的重构都可以归结为局部访存优化,书中第一个例子可以归结为这一类,不同的是,作者的实现利用了高速缓存的原理,而不是让系统自己去管理(也没办法自己管理)。另一个不得不提到的就是循环展开,当二分搜索的上下界确定的时候,我们可以展开整个二分循环,下面是书中给出的上界1000的例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
l = -1
if (x[511] < t) l = 1000 - 512
if (x[l + 256] < t) l += 256
if (x[l + 128] < t) l += 128
if (x[l + 64] < t) l += 64
if (x[l + 32] < t) l += 32
if (x[l + 16] < t) l += 16
if (x[l + 8] < t) l += 8
if (x[l + 4] < t) l += 4
if (x[l + 2] < t) l += 2
if (x[l + 1] < t) l += 1
p = l + 1
if (p > 1000 || x[p] != t)
p = -1

4.这个问题挺有意思,归结与宏定义的语义问题可能会大大影响到代码的效率。我想到曾经我打ACM比赛时,在赛场上用到了memset()初始化数据,结果一直TLE而且找不到原因。终于在无数次WA之后,我们更换为一个循环初始化数据后才顺利AC。C++的确存在很多陷阱,但是我在自己的机器上试验的时候,并没有这个问题中提到的现象。

7.因为一个非常的长的字节序列我们可以看成很多个有限长度的字节序列的总和,而有限长度的字节序列,比方说是8位的,当长达10亿或更多的时候,里面必然会存在很多的重复,因为8位数只有256种可能,所以我们只需要一开始统计256个数各个包含多少个1,然后把很多的数位切成8位一组的N个组再统计就起到加速的作用了。

11.最初我领略打表的精妙是在高中时,参加了一次CTSCAPIO然后跟一位大牛学会的。我觉得都可以归结为Hash表的思想,就是快速的索引,这能够解决很多问题,比如减少重复或不必要的计算。

12.先因式分解,减少乘法计算次数。属于对于已有结果加以利用而不是重复计算。

摘录

在过去,程序员知道,如果程序的运行时间主要消耗在输入输出上,那么对程序中的计算进行加速是毫无意义的。在现代的体系结构中,如果对内存的访问占用了大量的运行时间,那么减少计算时间同样是毫无意义的。

对老式计算机来说,降低开销可以加速10%或20%。对于现代计算机来说,将循环展开则有助于避免管道阻塞、减少分支、增加指令级的并行性。

效率的角色。软件的其他许多性质和效率一样重要,甚至更重要。Don Knuth观察发现,不成熟的优化是大量编程灾害的根源,它会危机程序的正确性、功能性以及可维护性。当可能危害影响较大时,请考虑适当将效率放一放。