這部分的代碼出自《深入理解計算機系統》(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; }
用紅色標記各個版本的改變。
版本1:初始版本
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; } }
如果不考慮錯誤的參數傳遞,這是一個可用版本,有着巨大的優化空間。
版本2:消除循環的低效率
並非不使用循環,而是將循環中每次都需要重新計算但實際並不會發生改變的量用常量代替。本例中,這個量是向量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; } }
版本3:減少過程調用
也即減少函數調用。通過對匯編代碼的學習,可以知道函數調用是需要一些額外的開銷的,包括建棧、傳參,以及返回。通過把訪問元素的函數直接擴展到代碼中可以避免這個開銷。但是這樣做犧牲了代碼的模塊性和抽象性,對這種改變編寫文檔是一種折衷的補救措施。
在這里,是通過把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]; } }
版本4:消除不必要的存儲器引用
在對於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; }
版本5:循環展開
這個優化措施利用了對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; }
版本6與版本7:提高並行性
和版本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; } }