全部博文(436)
分类: LINUX
2012-11-25 23:33:40
《深入理解计算机系统》第五章读书笔记 吕依蓉
本书第五章讲述的是如何优化程序性能。我觉得优化程序性能可以分两个方面:第一,与机器相关,第二,与机器无关。第五章的1-6节讲的就是如何“大众化”地优化代码,也就是说不依赖于目标机器的任何特征。
然而,在程序优化的之前,我们必须统一一个东西来表示程序的性能,就像现在在数据结构课中学到的时间复杂度这个概念类似。而现在我知道了我们可以用每元素的周期数(cycles per element,CPE)来度量程序的循环性能,CPE越小表示性能越好。
然后本书大概介绍了以下几种方法:
1.循环展开的技术(loop unrolling)。所谓循环展开就是通过在每次迭代中执行更多的数据操作来减小循环开销的影响。其基本思想是设法把操作对象线性化,并且在一次迭代中访问线性数据中的一小组而非单独的某个。这样得到的程序将执行更少的迭代次数,于是循环开销就被有效地降低了。
2.简单地将命令开关设置为“-O2”。
3.代码移动(code motion)。如果某个函数的计算结果是个常数,那么我们就不应该把他放在循环里面,而可以将计算机移动到代码前面的,不会被多次求值的部分。
4.减少调用过程。但这种方法存在一个缺点,就是它是以损害一些程序的模块性为代价的。所以修改这些代码,需要添加一些对所做改变的说明文档。那么如何权衡这两者呢?
5.消除不必要的存储器的引用。在循环中不停地对指针所指向的变量赋值的时候,我们可以用一个中间变量代替指针,以增加速度。其实读到这里我有一个疑问:在学c的时候,老师曾经告诉我们,使用指针的好处就是提高速度,当时我不是很能理解其中的细节和原理。但现在看来,好像减少指针的使用反而能提告速度,这又是为什么呢?既然多次使用指针会影响效率,那为什么还有指针的存在?
6.转换到指针代码。其实指针和数组代码的相对性会随着机器不同而不同。而编译器对数组代码应用非常高级的优化,而对指针代码只引用最小限度的优化。所以数组代码更可取。
7.寄存器溢出。循环并行性的收益是受到处理器硬件资源特别是寄存器数量的限制的。当并行度导致寄存器数量不足时,只能将临时值存放在堆栈中。一旦出现这种情况,性能就会急剧下降。一个通用的原则是无论何时当程序显示出在某个频繁使用的内循环中存在寄存器溢出的迹象时,都应该重写代码,减少循环中涉及的局部变量。
当然,无论使用哪种优化方案,我发现总有一个坏蛋在捣乱,那就是妨碍优化因素(optimization blocker),例如存储器别名使用(memory aliasing)和函数调用。存储器别名使用的问题之所以会出现是因为编译器必须假设不同的指针可能会指向存储器中同一个位置。
下面再讲讲“与机器有关”的情况,不得不说这一节(5.7)理解起来很困难。“与机器有关”的优化可以想到“并行”的问题,也就是说,可以在每个时钟周期执行多个操作,而且是乱序的。其中书里讲到一个分支预测(branch prediction),我觉得很神奇,这种技术具体的物理实现是怎么样的,它运用的价值体现在哪里?其中它要用到退役单元(Retirement Unit),来记录正在进行的处理,并确保它遵守机器级程序的顺序语义。它的实现比较好理解,是用了队列的数据结构类型。另外,在一个IA32处理器上,所有的浮点操作都是以扩展的80位精度执行的,而浮点寄存器也是按照这个格式存储值的。只有当寄存器中的值写入存储器中时,才把它转换成32位或64位格式。
最后,我们要进行程序剖析(profiling),包括运行程序的这样一个版本,其中插入了工具代码,以确定程序的各个部分需要多少时间。Unix系统提供了这样的一个剖析程序GPROF。为提高系统一部分性能,有一个Amdahl定律:当我们回快系统一个部分的速度时,对系统整体性能的影响依赖于这个部分有多重要和速度提高了多少。
总的来说,想要程序性能提高,既要为问题选择适当的数据结构和算法,还要在编码中考虑各种可能引起低性能的特殊情况。以上都是我自己的理解,有的地方用了自己的语言来描述,可能会有不对的地方,那么读书笔记就到这里,我的问题也已经用横线画出。