主要看了第5章,优化程序性能。如同老师所说的,程序优化的技巧是需要平时不断积累,而且通过阅读这一章,充分体会到,真正想要做到程序的优化,下到计算机体系结构,汇编语言,同时对编译器要有充分的了解,上到合适的数据结构以及算法的选择都会对程序的运行速度产生巨大的影响,除外还有任务的的切割,进行并行运算。但是这一章它主要还是讲的是我们应当如何正确的编写代码来引导编译器进行优化,还是很有意思的。最优秀的是作者使用的测试cpu是intel core i7 Haswell,让我感觉到这些技巧确确实实现在仍旧有意义。
分为两个部分,看的内容与收获总结吧
1.了解到编译器的局限性
所谓的局限性是编译器可能不会像你想象中的做一些理想当然的优化,因为编译器必须保证绝对的安全性,举了一个很简单的例子
void twiddle1(long *xp ,long * yp){ *xp+=*yp; *xp+=*yp; } void twiddle2(long *xp ,long * yp){ *xp=2*yp;; }
而且我们按的认为1,2等效,而且2更高效一点,因为内存读写数目少,但是实际上,两者是不等价的,比如xp与yp指向相同元素地址的时候,1变为4倍,2只有三倍。其后还举了函数调用的例子,都是强调,编译器是会自动优化,但是可能并不会为我们优化掉一些我们理算当然的部分,因为可能会带来安全的隐患。
2.具体实例
- 首先讲了代码移动,这个之前老师也提到过,就是把在循环过程中的函数返回是不变量的赋值在循环外,避免重复调用,并指出,一般编译器不会自动做这样的优化吗,因为会带来安全风险,这样的优化一般是需要我们自己动手完成的。
- 消除不必要的内存引用,部分片段如下:
for(int i = 0;i < length; i++){ *dest++; //或者其他运算 } int q =*dest; for(int i = 0;i < length; i++){ q++; //或者其他运算 } *dest = q;
书中为我们展示了两者的汇编代码,2明显比1少了取dest值到寄存器再从起存起赋值回去的两个语句,速度得到了明显提升,整型CPE降到了原来的1/7,浮点数1/3.并且举了例子,编译器出于安全考虑不会做这样的优化。(但是书中还介绍了GCC优化选项如何在保证安全性的基础上做优化,发现本质上还是引入了中间值,不过每一次都会更新*dest的值,相当于减少一次取值到寄存器的操作)
3.理解现代处理器
介绍了一些现代处理器的一些技术,如指令级并行(即如何在底层实现并行运算,但是上层上却显示出顺序执行的结果),分支预测(在条件转移指令处预测转移地址)等等。
比较新奇一点是看到了退役单元的概念,其是一个寄存器文件,储存着最近的指令操作,寄存器更新,只有当一个指令之前所有相关的分支点都确认预测成功了,这条指令就退役,计算结果才真正写入程序下相关的寄存器,也就是说之前用的寄存器都是闲暇的(我猜的,书中写的不是特别清楚),反之,之前有分支预测错误,此指令所有涉及的计算结果全部丢弃。这样就保证了预测错误时结果的正确性。 这样,必然就出现了一个指令之间寄存器值传递的问题,相应的诞生了寄存器重命名机制,但是我没看懂具体是怎么实现的,但是理解了它就是通过一张表来实现寄存器间传递,而不是一条指令写入寄存器,另一条去读,这样就保证了只有退役指令才能写寄存器。
4.增加累加变量进行循环展开
这是我看到的最精彩的一节,简直完全刷新了我的三观
循环展开是并行运算的一个应用,为了讲这个,先是引入了循环的关键路径的概念(其实直接理解为CPE是可以的),就是说,一定程度上一个循环体内的计算语句不是顺序的,而是并行的(循环标志的++与数据的运算,以前从来没有这个概念),一般是数据的浮点运算成为瓶颈,其运算也构成一次循环完成的关键路径,完成循环的时间=关键路径*循环次数,然后作者举了一个例子:
for(int i = 0 ; i < length ; i++){ acc=acc op date[i]; } for(int i= 0; i < length ; i=i+2){ acc1 = accc1 op date[1]; acc2 = acc2 op date[i+1]; }
会发现2会比1快接近一倍,为什么呢,是因为增加一个累计变量后,其实acc1与acc2的计算基本是并行的(多加了一个功能单元),2中每次循环完成时间与1基本相同,但是循环次数减半了!!!! 当然这种优化也是有上限的,最多展开到相应的功能单元个,也就还是说,只要相应功能的计算单元流水线满了,就没有展开的余地了。(这就要求我们要对自己的cpu计算单元有所了解,比如处理整数加法的有多少个,浮点数乘法的有多少个)