文章目錄
1.優化編譯器的能力和侷限性
- 兩種指針可能指向同一個內存位置的情況稱爲內存別名引用,在只執行安全的優化中,編譯器必須假設不同的指針可能會指向內存的同一個位置。這樣就限制了編譯器的優化,如下面例子:
//原來的代碼
void twiddle1(long *xp,long *yp)
{
*xp +=*yp;
*xp +=*yp;
}
//優化後的代碼
void twiddle1(long *xp,long *yp)
{
*xp += 2*(*yp);
}
如果yp和xp指向同一個內存,原來的代碼得到4倍結果,而優化後的代碼卻是3倍,故編譯器這樣優化會出問題
- 函數調用產生的問題:
int counter = 0;
long f()
{
return counter++;
}
//原始代碼
long func1()
{
return f()+f()+f()+f();
}
//優化後的代碼
long func2()
{
return 4*f();
}
由上圖可見,雖然優化後只調用了一次f(),但是優化後得到的結果並不一樣
解決辦法:我們可以將f()聲明爲inline內斂函數 ,即調用的時候直接內部展開,這樣既可以減少了函數調用的開銷,也允許對展開的代碼做進一步優化,適合一些經常被調用的少量代碼
2.表示程序性能
- 度量標準:每元素的週期數(Cycles Per Elment,CPE),作爲一種表示程序性能並指導我們改進代碼的方法。
- 處理器活動的順序是由時鐘控制的,時鐘提供了某個頻率的規律信號,通常用千兆赫茲(GHz)表示,例如,當表明一個系統有4GHz處理器,表示處理器時鐘運行頻率爲每秒4*109個週期。
3.用於以下優化的原始函數
typedef long data_t;
typedef IDENT 1 //或0
#define OP * //或+
typedef struct{
long len;
data_t *data;
}vec_rec,*vec_ptr;
vec_ptr new_vec(long len)
{
vec_ptr result = (vec_ptr)malloc(sizeof(vec_rec));
data_t *data = NULL;
if(!result) //如果分配失敗
return NULL;
if(len>0){
//動態分配數組calloc,指定data_t大小的len個空間
data = (data_t*)calloc(len,sizeof(data_t));
if(!data){
free((void *)result);
return NULL;
}
}
result->data = data;
return result;
}
int get_vec_element(vec_ptr v,long index,data_t *dest)
{
if(index<0 || index>=v-len)
return 0;
*dest = v->data[index];
return 1;
}
long vec_length(vec_ptr v)
{
return v->len;
}
void combine1(vec_ptr v,data_t *dest)
{
long i;
*dest = IDENT;
for(i = 0;i<vec_length(v);i++){
data_t val;
get_vec_element(v,i,&val);
*dest = *dest OP val;
}
}
4.優化1——消除循環的低效率
- 將循環中要執行多次但是計算結果不會改變的計算提出來。
void combine2(vec_ptr v,data_t *dest)
{
long i;
long 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;
}
}
5.優化2——減少過程調用
- 減少循環中過程的調用,如get_vec_element每次都會進行邊界檢查,但是我們這裏並不需要邊界檢查,因爲所有索引都是合法的,所以我們直接獲取首地址後使用data[i],缺點是破壞了程序的模塊性,我們並不應該知道他的元素到底是以數組存儲還是鏈表之類的存儲。而且此時的效率並沒有顯著提升
data_t* get_vec_start(vec_ptr v)
{
return v->data;
}
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<length;i++){
*dest = *dest OP data[i];
}
}
6.優化3——消除不必要的內存引用
- 在循環中消除不必要的內存引用,像上述題目中的dest的讀寫過於頻繁,但是不是必要的,每次讀的都是上次寫的,所以使用臨時變量計算值就行
void combine4(vec_ptr v,data_t *dest)
{
long i;
long length = vec_length(v);
data_t *data = get_vec_start(v);
data_t sum = IDENT;
for(i = 0;i<length;i++){
sum = sum OP data[i];
}
*dest = sum;
}
7.兩個界限
- 指令級並行:在代碼級上,看上去似乎是一次執行一條指令,每條指令都包括從寄存器或內存取值,執行一個操作,並把結果存回到一個寄存器或內存位置。在實際的處理器中,是同時對多條指令求值的。
- 描述程序性能的兩個下界:
- 延遲界限:一系列操作必須按照嚴格順序執行,因爲在下一條指令開始之前,這條指令必須結束。主要是代碼中的數據相關限制了處理器利用指令級並行的能力
- 吞吐量界限:刻畫了處理器功能單元的原始計算能力。這個界限是程序性能的中繼限制。
8.循環展開
- **循環展開是一種程序變換,通過增加每次迭代計算的元素的數量,減少循環的迭代次數。**循環展開能夠從兩個方面改進程序的性能。
- 它減少了不直接有助於程序結果的操作的數量,例如循環索引計算和條件分支
- 提供了一些方法,可以進一步變化代碼,減少整個計算中關鍵路徑上的操作數量
- 優化代碼:
void combine5(vec_ptr v,data_t *dest)
{
long i;
long length = vec_length(v);
data_t *data = get_vec_start(v);
data_t sum = IDENT;
for(i = 0;i<length-1;i+=2){
sum = sum OP data[i] OP data[i+1];
}
//完成剩下的索引
for(;i<length;i++){
sum = sum OP data[i];
}
*dest = sum;
}
9.提高並行性
- 執行加法和乘法的功能單元是完全流水化的。所以對於一個可結合和可交換的合併運算來說,比如說整數加法或乘法,我們可以通過將一組合並運算分割成兩個或更多的部分,並在最後合併結果來提高性能。
void combine6(vec_ptr v,data_t *dest)
{
long i;
long length = vec_length(v);
data_t *data = get_vec_start(v);
data_t sum1 = IDENT;
data_t sum2 = IDENT;
for(i = 0;i<length-1;i+=2){
//兩個運算幾乎並行運行,流水線
sum1 = sum1 OP data[i]
sum2 = sum2 OP data[i+1];
}
//完成剩下的索引
for(;i<length;i++){
sum1 = sum1 OP data[i];
}
*dest = sum1 OP sum2;
}
或者:
void combine7(vec_ptr v,data_t *dest)
{
long i;
long length = vec_length(v);
data_t *data = get_vec_start(v);
data_t sum = IDENT;
for(i = 0;i<length-1;i+=2){
sum = sum OP (data[i] OP data[i+1]);
}
//完成剩下的索引
for(;i<length;i++){
sum = sum OP data[i];
}
*dest = sum;
}
10.一些限制因素
(1)寄存器溢出
- 循環並行性的好處受彙編代碼描述計算的能力限制。如果我們的並行度p超過了可用的寄存器數量,那麼編譯器會訴諸溢出。
(2)分支預測和預測錯誤處罰
- 當分支預測邏輯不能正確一個分支是否要跳轉的時候,條件分支可會招致很大的預測錯誤處罰。
- 要求程序員儘量寫出適合用條件傳送語句的程序
10.理解內存性能
- 所有的現代處理器都包含一個或多個高速緩存存儲器,以對這樣少量的存儲器提供快速的訪問。