深入理解計算機系統(csapp)閱讀筆記——第五章優化程序性能

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.理解內存性能

  • 所有的現代處理器都包含一個或多個高速緩存存儲器,以對這樣少量的存儲器提供快速的訪問
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章