计算机系统之优化程序性能

计算机系统之深入分析优化程序性能

1.优化编译器的能力和局限性
2.表示程序性能
3.程序示例
4.优化—消除循环的低效率
5.优化—减少过程调用
6.优化—消除不必要的内存引用
7.总结

1.优化编译器的能力和局限性

大多数编译器,包括GCC,都向用户提供了他们所使用的的优化控制,最简单的控制就是指定优化级别。例如,用命令行选项“-Og”调用GCC使用一组基本的优化。当我们以“-O1”或者更高如“-O2”或“-O3”调用会让GCC使用更大量的优化,可以进一步提高程序性能。
但我们发现一些C代码,即使用-O1选项编译得到的性能,也比用可能最高的优化等级编译一个原始版本的C代码性能更好。因为编译器只能小心翼翼的对程序使用安全的优化,所以如果期待更大程度上的性能的优化,更多还是需要程序员写出高效的代码。

我们先看下面两个过程

计算机系统之优化程序性能_第1张图片
这两个过程有相同的目的,都是将储存在由指针yp指示的位置处的值两次加到指针xp指示的位置处的值。
我们来看一下这两个过程分别进行的内存引用。
函数twiddle1进行6次内存引用(2次读xp, 2次读yp, 2次写xp)
函数twiddle2进行3次内存引用(1次读
xp, 1次读yp, 1次写xp)

经过对比,我们可以看出twiddle2比twiddle1效率更高。

现在我们站在编译器的角度来看一下。如果编译器编译过程twiddle1时,可能也会认为基于twiddle2会产生更有效率的代码。
但是如果我们考虑一下xp等于yp的情况,twiddle1执行下面操作

*xp += *xp;
*yp += *yp;

结果是xp的值增加4倍。

如果是twiddle2的话会执行以下操作

*xp += 2* *xp;

结果是xp的值增加3倍。

所以结论就出来了,因为编译器不知道twiddle1函数会被作什么用,所以避免出错,不能将其以twiddle2形式编译。

2.表示程序性能

简单来说,每元素的周期数(Cycles Per Element,CPE)是一种用来表示程序性能的参数。该值与程序性能成反比。

3.程序示例

计算机系统之优化程序性能_第2张图片
下图是combine1的CPE度量值
计算机系统之优化程序性能_第3张图片

4.优化—消除循环的低效率

我们仔细观察上面的代码。发现在for循环里面条件是

for(i = 0; i < vec_length(v); i++)

我们可以发现for循环里面的每一次循环都会将vec_length(v)计算一遍。
但是其实vec_length(v)的值并不会改变,所以只需要计算一次,我们可以定义一个新变量length储存vec_length(v)值,然后把新变量length放到for循环里面就行了。

long length = vec_length(v);

for(i = 0; i < length; i++){
	data_t val;
	get_vec_element(v, i, &val);
	*dest = *dest OP val;
	}

代码及CPE值如下图:
计算机系统之优化程序性能_第4张图片

这类优化称为代码移动(code motion),这类优化包括识别要执行多次(例如在循环里)但是计算结果不会改变的计算,因而可以将计算移动到代码前面不会被多次求值的部分。

5.优化—减少过程调用

过程调用会带来开销,在combine2代码看出,每次循环里面调用get_vac_element()来获取下一个向量元素。
作为替代,增加一个函数get_vec_start(),该函数返回数组的起始地址。
在combine3里,在内循环不进行函数调用去获取每个向量元素,而是直接访问数组。
我们看一下代码与CPE值:
计算机系统之优化程序性能_第5张图片
计算机系统之优化程序性能_第6张图片

我们可以看到相较于combine2,combine3的性能并没有明显提升。并且,combine3在整数加法部分甚至性能弱于combine2。
这说明内循环中其他操作形成瓶颈,对性能的限制超过get_vec_element()的调用。
我们暂时可以把这个看成提升性能步骤中的一步,而只有这些步骤加在一起才会让程序性能明显提升。

6.优化—消除不必要的内存引用

在combine3中我们将计算出来的值累积在指针dest指向的位置上。
我们先来看一下内循环产生的汇编代码,如下图:
和把combine3的内循环代码放一起对比看

for(i = 0; i < length; i++){
	*dest = *dest OP data[i];
}

计算机系统之优化程序性能_第7张图片
在这段代码中,可以看到
第一行是指针dest的地址存放在寄存器%rbx中

第五行是将第i个数据元素的指针保存在寄存器%rdx中,注释为data+i。每次迭代,指针加8是因为i为long型元素。

第六行是重点,循环操作通过比较这个指针与保存在寄存器%rax的数值来判断。可以看到每次迭代时,累积变量的数值都要从内存读出然后写入到内存,这样不断的读写造成巨大浪费,因为每次从dest读出来的值就是上一次写入的值

解决方法

为了消除造成浪费的内存读写,我们引入一个临时变量acc,用来累积计算出来的值,只在循环完成之后结果再存放到dest里面。将累积值存放在局部变量acc中,消除了每次循环迭代中从内存中读出并将更新值写回的需要。因为acc在寄存器中,比在内存中更快
计算机系统之优化程序性能_第8张图片
计算机系统之优化程序性能_第9张图片

与combine3相比,在combine4中每次迭代的内存操作从两次读和一次写减少到只需要一次读。
程序性能有显著提升。
计算机系统之优化程序性能_第10张图片

7.总结

计算机系统之优化程序性能_第11张图片

你可能感兴趣的:(计算机系统)