本文几个优化程序性能的方法出自CSAPP第五章,通过不断修改源代码,试图欺骗编译器产生有效的代码
我们先引入度量标准每元素的周期数(CPE),表示程序性能。
我们先定义一个数据结构 data_t 代表数据类型
1 typedef struct{ 2 long len; 3 data_t *data; 4 }vec_rec,*vec_prt;
以及常数IDENT和OP以便在后续的代码中进行不同的操作
//对所有向量的元素求和 #define IDENT 0 #define OP + //对所有向量元素乘积 #define IDENT 1 #define OP *
我们首先看最初的代码版本,这是一个具有很大优化空间的代码,具体函数实现可参考原书。
1 void combine1(vec_ptr v, data_t *dest) 2 { 3 long int i; 4 *dest = IDENT; 5 for (i = 0; i < vec_length(v); i++) { //vec_length返回向量长度 6 data_t val; 7 get_vec_element(v, i, &val);//先进行边界检查再获取索引 i 处的值并赋值给val 8 *dest = *dest OP val; 9 } 10 }
1.消除循环的低效率
因为每次迭代循环的时候都必须对测试条件求值,但在此循环中,向量的长度值并不会随着循环的进行而改变,因此只需要计算一次vec_length(v)并保存在一个变量中,在后续的循环中使用此变量。
因此我们得到第二个版本的代码。这一常见的优化方式称为 代码移动,即识别要执行多次但值不会改变的代码,将其移动到代码前部分,避免重复求值。
1 void combine2(vec_ptr v, data_t *dest) 2 { 3 long int i; 4 long int length = vec_length(v);//只进行一次计算 5 *dest = IDENT; 6 for (i = 0; i < length; i++) { 7 data_t val; 8 get_vec_element(v, i, &val); 9 *dest = *dest OP val; 10 } 11 }
2.减少过程调用
过程调用(函数调用)会带来开销,因此我们增加一个函数 get_vec_start.
1 data_t *get_vec_start(vec_ptr v) 2 { 3 return v->data; 4 }
由此我们可得第三版代码
void combine3(vec_ptr v,data_t *dest) { long i; long length = vec_length(v); data_t *data = get_vec_start(v); *dest = IDENT; for(i = 0;i){ *dest = *dest OP data[i]; //在循环中减少过程调用 } }
3.消除不必要的内存引用
虽然我们在第三版的代码中减少了过程的调用,但是第三版的代码相比第二版代码性能并没有明显的提升,这说明第三版中的代码还有别的制约性能的因素。
先看第三版代码的内循环汇编代码:
//dest in %rbx, data+i in %rdx, data+length in %rax .L17 vmovsd (%rbx),%xmm0 vmulsd (%rdx),%xmm0,%xmm0 vmovsd %xmm0,(%rbx) addq $8,%rbx cmpq %rax,%rdx jne .L17
由汇编代码可见,第三版的代码对内存进行了两次读操作,一次写操作,通过引入一个临时变量,使其在循环中累计值,在循环结束后再讲值写入内存。
这样我们将循环中的内存操作又两次读一次写减少到一次读操作。程序性能显著提高。
void combine4(vec_ptr v, data_t *dest) { long int i; long int length = vec_length(v); data_t *data = get_vec_start(v); data_t acc = IDENT; for (i = 0; i < length; i++) { acc = acc OP data[i]; } *dest = acc; }