上面的代码中twiddle1的功能是,将 yp 指针指向地址的值加到 xp 指针指向地址的值,执行两次。twiddle2的功能是将 yp 指针指向地址的值得两倍加到 xp 指针指向地址的值。看起来两个的功能是一样的。但当 xp 和 yp 指向同一个存储器位置时,两个程序结果不在相同,因此编译器不会优化它。这就是一个典型的“妨碍优化因素”,称为存储器别名使用。
为表示程序性能,我们引入每元素的周期数(Cycles Per Element, CPE)的概念,使用时钟周期来表示度量标准比用事件表示有用的多,因为时钟周期表示的是执行了多少条指令,而程序运行了多少时间,可能与机器的频率有关。
程序示例
以如下程序为示例,记录几种通用的优化方法:
vec.h
123456789101112131415
#ifndef VEC_H#define VEC_H#define IDENT 0#define OP +// Create abstract data type for vectortypedefintdata_t;typedefstruct{longintlen;data_t*data;}vec_rec,*vec_ptr;#endif
// Create vector of specified lengthvec_ptrnew_vec(longintlen){//Allocate header structurevec_ptrresult=(vec_ptr)malloc(vec_rec);if(!result)returnNULL;//Could not allocate storageresult->len=len;//Allocate arrayif(len>0){data_t*data=(data_t*)calloc(len,sizeof(data_t));if(!data){free((void*)result);returnNULL;//could not allocate storage}result->data=data;}else{result->data=NULL;}returnresult;}//Retrieve vector element and store at dest.//Return 0 (out of bounds) or 1 (successful)intget_vec_element(vec_ptrv,longintindex,data_t*dest){if(index<0||index>=v->len)return0;*dest=v->data[index];return1;}//Return length of vectorlongintvec_length(vec_ptrv){returnv->len;}//Implementation with maximum use of data abstractionvoidcombine1(vec_ptrv,data_t*dest){longinti;*dest=IDENT;for(i=0;i<vec_length(v);i++){data_tval;get_vec_element(v,i,&val);*dest=*destOPval;}}
消除循环的低效率
观察上面的 combine1 函数,可以看出在 for 循环的测试条件中,每次都要执行 vec_length,但这个函数每次执行结果都一样,可以提到测试条件外面,修改代码如下:
12345678910111213
//move call to vec_length out of loopvoidcombine2(vec_ptrv,data_t*dest){longinti;longintlength=vec_length(v);*dest=IDENT;for(i=0;i<length;i++){data_tval;get_vec_element(v,i,&val);*dest=*destOPval;}}
减少调用过程
函数调用会带来巨大的时间开销,再来看上面的combine2函数中,每次循环都会调用get_vec_element函数,而且这个函数的返回值 val 都是随着 i 的递增而在内存中连续存放,因此可以将该函数从循环中拿出。
12345678910111213141516
data_t*get_vec_start(vec_ptrv){returnv->data;}//direct access to vector datavoidcombine3(vec_ptrv,data_t*dest){longinti;longintlength=vec_length(v);data_t*data=get_vec_start(v);*dest=IDENT;for(i=0;i<length;i++){*dest=*destOPdata[i];}}
消除不必要的存储器引用
观察上面的函数,在每次 for 循环中,都会有两次的读取内存(dest 和 data[i])的操作,一次写内存(dest)的操作。每次读写内存都会耗时,因此可以使用临时变量,从而使每次循环只有一次读即可。
12345678910111213
//Accumulate result in local variablevoidcombine4(vec_ptrv,data_t*dest){longinti;longintlength=vec_length(v);data_t*data=get_vec_start(v);data_tacc=IDENT;for(i=0;i<length;i++){acc=accOPdata[i];}*dest=acc;}
//Unroll loop by 2voidcombine4(vec_ptrv,data_t*dest){longinti;longintlength=vec_length(v);longintlimit=length-1;data_t*data=get_vec_start(v);data_tacc=IDENT;//Combine 2 elements at a timefor(i=0;i<length;i+=2){acc=accOPdata[i]OPdata[i+1];}// Finish any remaining elementsfor(;i<length;i++){acc=accOPdata[i];}*dest=acc;}
提高并行性
两次循环展开,并且使用两路并行,该方法利用了功能单元的流水线能力
12345678910111213141516171819202122
//Unroll loop by 2, 2-way parallelismvoidcombine6(vec_ptrv,data_t*dest){longinti;longintlength=vec_length(v);longintlimit=length-1;data_t*data=get_vec_start(v);data_tacc0=IDENT;data_tacc1=IDENT;//combine 2 elements at a timefor(i=0;i<limit;i+=2){acc0=acc0OPdata[i];acc1=acc1OPdata[i+1];}//Finish any remaining elementsfor(;i<length;i++){acc0=acc0OPdata[i];}*dest=acc0OPacc1;}