2.3 程序示例
1 /* Creat abstract data type for vector */
2 typedef struct
3 {
4 long len;
5 data_t *data;
6 }vec_rec, *vec_ptr;
7 /* Creat vector of specified length */
8 vec_ptr new_vec(long len)
9 {
10 vec_ptr result = (vec_ptr)malloc(sizeof(vec_rec));
11 data_t *data = NULL;
12 if (NULL == result)
13 return NULL;
14
15 result->len = len;
16 if (len > 0)
17 {
18 data = (data_t *)calloc(len, sizeof(data_t));
19 if (NULL == data)
20 {
21 free(result);
22 return NULL;
23 }
24 }
25
26 result->data = data;
27 return result;
28 }
29
30 /*
31 ** Restrieve vector element and store at dest
32 ** Return 0(out of bounds) or 1(succesful)
33 */
34 int get_vec_element(vec_ptr v, long index, data_t * dest)
35 {
36 if (index < 0 || index >= v->len)
37 return 0;
38
39 *dest = v->data[index];
40
41 return 1;
42 }
43
44 /* Return length of vector */
45 long vec_length(vec_ptr v)
46 {
47 return v->len;
48 }
typedef long data_t;
#define IDENT 1
#define OP *
1 void combine1(vec_ptr v, data_t *dest)
2 {
3 long i;
4
5 *dest = IDENT;
6 for(i=0; i<vec_length(v); i++)
7 {
8 data_t val;
9 get_vec_element(v, i, &val);
10 *dest = *dest OP val;
11
针对combine1进行整数数据和浮点数据进行的测试结果如下: 可知,未经优化的代码是从C语言到既期待吗的直接翻译,通常效率明显降低。若简单的使用命令行选项“-O1”,就会进行基本的优化,此时程序员并不需要做什么就能显著的提高程序性能——超过两个数
2.4 消除循环的低效率
通过观察combine1调用函数vec_length作为for循环的测试条件,每次循环迭代时都必须对测试条件求值,而向量的长度并不会随着循环的进行而改变,则我们只需要计算一次向量的长度即可。 针对这个问题,修改了版本,如combine2,如下所示
1 void combine2(vec_ptr v, data_t *dest)
2 {
3 long i;
4 long length = vec_length(v);
5
6 *dest = IDENT;
7 for(i=0; i<length; i++)
8 {
9 data_t val;
10 get_vec_element(v, i, &val);
11 *dest = *dest OP val;
12 }
13 }
此类优化方法称为代码移动,包括优化识别要执行多次(如在循环里)但是计算结果不会被改变的计算。因而可将计算移动到代码前面不会被多次求值的部分。 极端示例如下: 测试结果如下: 通过以上的数据,可字符串的长度每增加一倍,则运行时间也会增加一倍。对于更长的字符串,运行时间的改进会更大。此示例看起来是无足轻重的代码片段但却隐藏着渐近低效率。而一个有经验的程序员工作的一部分就是避免引入这样的渐近低效率。
2.5 减少过程调用
从combine2的代码可知,每次循环都需要迭代都会调用get_vec_element来获取来获取下一个向量的元素。作为替代,假设我们的抽象数据类型增加一个函数get_vec_start,此函数将会返回数组的起始地址。于是combine3的示例代码,如下所示。
1 data_t *get_vec_start(vec_ptr v)
2 {
3 return v->data;
4 }
1 void combine3(vec_ptr v, data_t *dest)
2 {
3 long i;
4 long length = vec_length(v);
5 data_t *data = get_vec_start(v);
6
7 *dest = IDENT;
8 for(i=0; i<length; i++)
9 {
10 *dest = *dest OP data[i];
11 }
12 }
由上,性能并没有明显的上升,于是易知内循环中的其他操作形成了瓶颈,限制性能超过调用get_vec_element。
2.6 消除不必要的内存引用
Combine3的代码将合并运算计算的值累积在指针的dest指定的位置,通过检查编译出来的内循环产生的汇编代码,可看出此属性。注意:以下汇编代码为数据类型为double,合并运算为乘法的x86-64代码: 在这段汇编代码中,指针dest的地址存放在寄存器 %rbx中,它还改变了代码,将第i个数据元素的指针保存在寄存器 %rdx中,注释中显示为data+i。每次迭代,这个指针都加8。循环终止操作通过比较这个指针与保存在寄存器 %rax中的数值来判断。我们可以看到每次迭代时,累积变量的数值都要从内存读出在写入到内存。这样的读写是很浪费,因为每次迭代开始时从dest读出的值就是上次迭代最后写入的值。 为消除这种不必要的内存读写,重写该部分代码如combine4所示。
1 void combine4(vec_ptr v, data_t *dest)
2 {
3 long i;
4 long length = vec_length(v);
5 data_t *data = get_vec_start(v);
6 data_t acc = IDENT;
7
8 for(i=0; i<length; i++)
9 {
10 acc = acc OP data[i];
11 }
12 *dest = acc;
13 }
我们可以明显的看到程序性能有了显著的提高,如下所示: 通过几种的变换后,对于每个元素的计算,都只需要1.255个时钟周期。比起最开始采用优化时的9 11个时钟周期,这是相当大的提高了。