这部分的代码出自《深入理解计算机系统》(CS:APP)第五章,其目的是通过手工改变代码结构,而不是算法效率和数据结构优化,提高执行效率。有些编译器在某些优化选项下可能会做出类似的改动。
为了便于以后的查阅和使用,本文进行了摘录和简要分析,其中包含了一些个人理解。对于更深层次的原理如汇编、处理器结构等请参考原书。
大致地,越靠后的代码性能越好,版本6和7性能近似,版本6略好一些。二者均能达到版本1性能的10倍左右。
示例演示对于一个向量的所有元素完成一个运算。这个运算可以是所有元素的累加
#define IDENT 0 #define OP +
或这所有元素的累计乘积
#define IDENT 1 #define OP *
data_t代表一种数据类型,在这个示例中可以是int、float、double
typedef struct { long int len; data_t *data; } vec_rec, *vec_ptr;
对于vec_rec,有以下操作
创建向量
vec_ptr new_vec(long int len) { /* Allocate header structure */ vec_ptr result = (vec_ptr) malloc(sizeof(vec_rec)); if (!result) return NULL; /* Couldn’t allocate storage */ result->len = len; /* Allocate array */ if (len > 0) { data_t *data = (data_t *)calloc(len, sizeof(data_t)); if (!data) { free((void *) result); return NULL; /* Couldn’t allocate storage */ } result->data = data; } else result->data = NULL; return result; }
根据索引号获取向量元素
int get_vec_element(vec_ptr v, long int index, data_t *dest) { if (index<0||index >= v->len) return 0; *dest = v->data[index]; return 1; }
获得向量长度
long int vec_length(vec_ptr v) { return v->len; }
用红色标记各个版本的改变。
void combine1(vec_ptr v, data_t *dest) { long int i; *dest = IDENT; for (i = 0; i < vec_length(v); i++) { data_t val; get_vec_element(v, i, &val); *dest = *dest OP val; } }
如果不考虑错误的参数传递,这是一个可用版本,有着巨大的优化空间。
并非不使用循环,而是将循环中每次都需要重新计算但实际并不会发生改变的量用常量代替。本例中,这个量是向量v的长度。类似的还有使用strlen(s)求得的字符串长度。
显然,如果这个量在每次循环时都会被改变,是不适用的。
void combine2(vec_ptr v, data_t *dest) { long int i; long int length = vec_length(v); *dest = IDENT; for (i = 0; i < length; i++) { data_t val; get_vec_element(v, i, &val); *dest = *dest OP val; } }
也即减少函数调用。通过对汇编代码的学习,可以知道函数调用是需要一些额外的开销的,包括建栈、传参,以及返回。通过把访问元素的函数直接扩展到代码中可以避免这个开销。但是这样做牺牲了代码的模块性和抽象性,对这种改变编写文档是一种折衷的补救措施。
在这里,是通过把get_vec_element()展开来达到这个目的的,这需要编程人员对这个数据结构的细节有所了解,增加了阅读和改进代码的难度。
更通用地方法是使用内联函数inline,可以兼顾模块性和抽象性。
void combine3(vec_ptr v, data_t *dest) { long int i; long int length = vec_length(v); data_t *data = get_vec_start(v); *dest = IDENT; for (i = 0; i < length; i++) { *dest = *dest OP data[i]; } }
在对于vec_rec结构操作时,其中间结果*dest是存放在存储器中的,每次取取值和更新都需要对存储器进行load或store, 要慢于对寄存器的操作。如果使用寄存器来保存中间结果,可以减少这个开销。
这个中间结果,可以通过register显式声明为寄存器变量。不过下面的代码经过汇编后可以发现acc是存放在寄存器中的,不必显示地声明。
然而,中间变量并非越多越好。原书5.11.1节展示了一个情形,如果同时使用的中间变量数过多,会出现“寄存器溢出”现象,部分中间变量仍然需要通过存储器保存。同理,多于机器支持能力的register声明的变量并不一定全部使用了寄存器来保存。
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; }
这个优化措施利用了对CPU数据流的知识,比汇编代码更接近机器底层。简单地说是利用了CPU的并行性,将数据分成不相关的部分并行地处理。版本5~7的更多细节和原理可以参考原书。类似的原理在练习题5.5和5.6中展示了为什么Horner法比一般的多项式求值的运算次数少,反而更慢的原因。
展开的次数可以根据情况而定,下面的代码只展开了两次。对于未处理的部分元素,不能遗漏。
gcc可以通过-funroll-loops选项执行循环展开。
void combine5(vec_ptr v, data_t *dest) { long int i; long int length = vec_length(v); long int limit = length-1; data_t *data = get_vec_start(v); data_t acc = IDENT; /* Combine 2 elements at a time */ for (i = 0; i < limit; i+=2) { acc = (acc OP data[i]) OP data[i+1]; } /* Finish any remaining elements */ for (; i < length; i++) { acc = acc OP data[i]; } *dest = acc; }
和版本5的思想类似,但由于并行化更高,性能更好一些,充分利用了向量中各个元素的不相关性。
版本6使用多个累积变量方法。
/* Unroll loop by 2, 2-way parallelism */ void combine6(vec_ptr v, data_t *dest) { long int i; long int length = vec_length(v); long int limit = length-1; data_t *data = get_vec_start(v); data_t acc0 = IDENT; data_t acc1 = IDENT; /* Combine 2 elements at a time */ for (i = 0; i < limit; i+=2) { acc0 = acc0 OP data[i]; acc1 = acc1 OP data[i+1]; } /* Finish any remaining elements */ for (; i < length; i++) { acc0 = acc0 OP data[i]; } *dest = acc0 OP acc1; }
版本7是在版本5的基础上打破顺序相关,改变了并行执行的操作数量。
void combine7(vec_ptr v, data_t *dest) { long int i; long int length = vec_length(v); long int limit = length-1; data_t *data = get_vec_start(v); data_t acc = IDENT; /* Combine 2 elements at a time */ for (i = 0; i < limit; i+=2) { acc = acc OP (data[i] OP data[i+1]);
//in combine5:
//acc = ( acc OP data[i]) OP data[i+1];
} /* Finish any remaining elements */ for (; i < length; i++) { acc = acc OP data[i]; } *dest = acc; }
这个示例中没有提到的改进方法还有:书写适合条件传送实现的代码,下面是原书的两段用于对比的代码,后者更适合条件传送实现。
void minmax1(int a[], int b[], int n) { int i; for(i=0;i<n; i++) { if (a[i] > b[i]) { int t = a[i]; a[i] = b[i]; b[i] = t; } } } /* Rearrange two vectors so that for each i, b[i] >= a[i] */ void minmax2(int a[], int b[], int n) { int i; for(i=0;i<n; i++) { int min = a[i] < b[i] ? a[i] : b[i]; int max = a[i] < b[i] ? b[i] : a[i]; a[i] = min; b[i] = max; } }