計算機系統之優化程序性能

計算機系統之深入分析優化程序性能

1.優化編譯器的能力和侷限性
2.表示程序性能
3.程序示例
4.優化—消除循環的低效率
5.優化—減少過程調用
6.優化—消除不必要的內存引用
7.總結

1.優化編譯器的能力和侷限性

大多數編譯器,包括GCC,都向用戶提供了他們所使用的的優化控制,最簡單的控制就是指定優化級別。例如,用命令行選項“-Og”調用GCC使用一組基本的優化。當我們以“-O1”或者更高如“-O2”或“-O3”調用會讓GCC使用更大量的優化,可以進一步提高程序性能。
但我們發現一些C代碼,即使用-O1選項編譯得到的性能,也比用可能最高的優化等級編譯一個原始版本的C代碼性能更好。因爲編譯器只能小心翼翼的對程序使用安全的優化,所以如果期待更大程度上的性能的優化,更多還是需要程序員寫出高效的代碼。

我們先看下面兩個過程

在這裏插入圖片描述
這兩個過程有相同的目的,都是將儲存在由指針yp指示的位置處的值兩次加到指針xp指示的位置處的值。
我們來看一下這兩個過程分別進行的內存引用。
函數twiddle1進行6次內存引用(2次讀xp, 2次讀yp, 2次寫xp)
函數twiddle2進行3次內存引用(1次讀
xp, 1次讀yp, 1次寫xp)

經過對比,我們可以看出twiddle2比twiddle1效率更高。

現在我們站在編譯器的角度來看一下。如果編譯器編譯過程twiddle1時,可能也會認爲基於twiddle2會產生更有效率的代碼。
但是如果我們考慮一下xp等於yp的情況,twiddle1執行下面操作

*xp += *xp;
*yp += *yp;

結果是xp的值增加4倍。

如果是twiddle2的話會執行以下操作

*xp += 2* *xp;

結果是xp的值增加3倍。

所以結論就出來了,因爲編譯器不知道twiddle1函數會被作什麼用,所以避免出錯,不能將其以twiddle2形式編譯。

2.表示程序性能

簡單來說,每元素的週期數(Cycles Per Element,CPE)是一種用來表示程序性能的參數。該值與程序性能成反比。

3.程序示例

在這裏插入圖片描述
下圖是combine1的CPE度量值
在這裏插入圖片描述

4.優化—消除循環的低效率

我們仔細觀察上面的代碼。發現在for循環裏面條件是

for(i = 0; i < vec_length(v); i++)

我們可以發現for循環裏面的每一次循環都會將vec_length(v)計算一遍。
但是其實vec_length(v)的值並不會改變,所以只需要計算一次,我們可以定義一個新變量length儲存vec_length(v)值,然後把新變量length放到for循環裏面就行了。

long length = vec_length(v);

for(i = 0; i < length; i++){
	data_t val;
	get_vec_element(v, i, &val);
	*dest = *dest OP val;
	}

代碼及CPE值如下圖:
在這裏插入圖片描述

這類優化稱爲代碼移動(code motion),這類優化包括識別要執行多次(例如在循環裏)但是計算結果不會改變的計算,因而可以將計算移動到代碼前面不會被多次求值的部分。

5.優化—減少過程調用

過程調用會帶來開銷,在combine2代碼看出,每次循環裏面調用get_vac_element()來獲取下一個向量元素。
作爲替代,增加一個函數get_vec_start(),該函數返回數組的起始地址。
在combine3裏,在內循環不進行函數調用去獲取每個向量元素,而是直接訪問數組。
我們看一下代碼與CPE值:
在這裏插入圖片描述
在這裏插入圖片描述

我們可以看到相較於combine2,combine3的性能並沒有明顯提升。並且,combine3在整數加法部分甚至性能弱於combine2。
這說明內循環中其他操作形成瓶頸,對性能的限制超過get_vec_element()的調用。
我們暫時可以把這個看成提升性能步驟中的一步,而只有這些步驟加在一起纔會讓程序性能明顯提升。

6.優化—消除不必要的內存引用

在combine3中我們將計算出來的值累積在指針dest指向的位置上。
我們先來看一下內循環產生的彙編代碼,如下圖:
和把combine3的內循環代碼放一起對比看

for(i = 0; i < length; i++){
	*dest = *dest OP data[i];
}

在這裏插入圖片描述
在這段代碼中,可以看到
第一行是指針dest的地址存放在寄存器%rbx中

第五行是將第i個數據元素的指針保存在寄存器%rdx中,註釋爲data+i。每次迭代,指針加8是因爲i爲long型元素。

第六行是重點,循環操作通過比較這個指針與保存在寄存器%rax的數值來判斷。可以看到每次迭代時,累積變量的數值都要從內存讀出然後寫入到內存,這樣不斷的讀寫造成巨大浪費,因爲每次從dest讀出來的值就是上一次寫入的值

解決方法

爲了消除造成浪費的內存讀寫,我們引入一個臨時變量acc,用來累積計算出來的值,只在循環完成之後結果再存放到dest裏面。將累積值存放在局部變量acc中,消除了每次循環迭代中從內存中讀出並將更新值寫回的需要。因爲acc在寄存器中,比在內存中更快
在這裏插入圖片描述
在這裏插入圖片描述

與combine3相比,在combine4中每次迭代的內存操作從兩次讀和一次寫減少到只需要一次讀。
程序性能有顯著提升。
在這裏插入圖片描述

7.總結

在這裏插入圖片描述

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章