考纲:
1.面向程序性能的优化
- 面向编译器的程序优化方法:减少过程调用、减少内存引用、指令并行等方法等方法。
- 面向流水线、超标量、向量CPU的程序优化方法。
2.存储器的层次结构
优化编译器的能力和局限性
内存别名使用妨碍函数优化
void twiddle1(long *xp, long *yp){ //假设传入的参数是1 2
*xp + = *yp; //x = x+y=3
*xp + = *yp; //x =x+y=5
}
第一段代码,函数从内存中引用数据需要访问内存两次,然后执行加的操作往内存中写还需要访问依次内存,这就是三次。然后下一句x = 5又访存了三次所以一共访存了六次。
void twiddle2(long *xp, long *yp){ //假设传入的参数是1 2
*xp + =2* *yp; //x = x+2y=5
}
第二个函数他把访存六次变成了访存三次,但是在实际运行中编译器是否会采用第二种方式呢?
采用第二种方式往往得不到我们想要的结果:假设xp和yp指针指向内存的同一位置,那么在下面这种情况下,可能和预期不符。
///twiddle1执行的效果:扩大四倍///
*xp += *xp;
*xp += *xp;
//twiddle2执行的效果扩大三倍///
*xp += 2**xp;
事实上,编译器为了避免xp和yp相等的情况,不会产生类似twiddle2的代码进行优化。
再看书上的一个例子:
x = 1000, y=3000
*q=y;/*3000*/
*p=x;/*1000*/
t1=*q;/*1000 or 3000*/
两个指针指向同一位置的情况叫做内存别名使用,如果指向同一位置那么就是1000指向不同位置那么就是3000,编译器必须假设两种情况,这就限制了编译器的优化。
练习5.1
如果相等
*xp = *xp+*xp //2x
*xp = *xp-*xp//0
*xp = *xp-*xp//0
递归调用
内联函数对函数调用的优化
表示程序的性能
- CPE:每元素的周期数
- CPI:每指令的周期数
- 延迟界限:当一系列指令严格按照顺序执行,执行下一条指令前,上一条指令必须完成
- 吞吐量界限:是程序性能的终极界限,刻画处理器单元的原始计算能力。
一般有用的优化
考虑编译器
不考虑编译器
1.代码移动
如果他总是产生相同的结果,将代码从循环中移出。
2.复杂运算简化
- 比如可以用移位代替乘法:乘法需要三个时钟周期而移位只需要一个时钟周期。
- 将乘法替换成加法如下图蓝色笔所示,演示的乘法和加法是等价的,性能提高了三倍
3.共享公共子表达式
-og 基本的优化 -o1 -o2更高级别的优化
都有i*n+j的表达式,汇编语言掌握即可。
程序员角度优化程序
减少过程调用
为什么编译器不能自动代码移动&&程序员如何提高程序性能
消除不必要的内存引用
CPU和内存之间速度差距很大,所以第六章介绍了多级缓存结构,但是每次CPU访问主存还是要花费时间的,从程序员的角度,可以尝试减少访问时间。
b[i]--------每次都要读出来再写回 为啥编译器不能优化? 内存别名
由第七章的知识可知局部变量是存放在栈中,局部变量运行的时候往往先存放在cache中
这样优化完之后,就将要修改的值存入cache,减少访问内存的次数,增加对缓存的访问,进而提高程序的速度。
练习5.4
A:没经过优化的代码中,%xmm0简单地被用作临时值,每次循环迭代中都会设置和使用,
B:两个版本有相同的功能,甚至内存别名的使用
C:变换可以不改变程序的行为,因为除了第一次迭代开始从dest读取值和前一次迭代最后写入到这个寄存器的值是相同的。因此,合并指令可以简单第使用在循环开始时就已经在%xmm0中的值。
两个函数的区别是,第二个函数访问的次数少,第一个没必要读内存,其实只需要把有用的数据写入内存就行
利用执行集并行
Benchmark例子:向量的数据类型
减少内存引用
采用流水线方式工作
5.8循环展开:
循环展开通过每次增加函数循环迭代的个数,减少迭代的次数,也可以减少关键路径的数量。
但是优化之后不能明显提高程序性能,我们分析一下原因。
练习5.8画关键路径
问题:
内联函数 减少内存访问次数的过程