深入理解計算機系統CSAPP-perfLab:kernels.c性能優化實驗:rotate優化詳細實驗日誌(含六個優化版本)

一、實驗內容

1、 學習圖像旋轉與圖像平滑功能的實現;
2、 理解Makefile的規則與make程序的工作原理;
3、 瞭解Cache寫缺失與讀缺失的基本原理,並嘗試用Cache相關原理進行性能優化
4、 修改kernels.c文件並編譯運行driver程序,儘可能優化程序性能。

二、相關知識

1、 圖形逆時針旋轉90°的實現

在圖像的存儲的基礎上進行線性變換。首先將圖像座標變換爲數學座標,之後求矩陣轉置,再執行行交換,即實現了圖形逆時針旋轉90°。
在這裏插入圖片描述我們將其用一維的形式表示:
可以看出,用常規的行訪問方式實現旋轉功能的方法,讀操作的局部性較好,但寫操作的空間局部性很差,這爲性能優化提供了方向。

2、 服務器與本地計算機之間複製文件的方法

a) 把本地文件拷貝到服務器
scp 本地文件路徑 用戶名@服務器地址:/服務器路徑

b) 把服務器文件拷貝到本地計算機
scp 用戶名@服務器地址:/服務器路徑 本地文件路徑

3、 常見性能優化方法

代碼移動、減少函數調用、減少訪存次數、分支預測、循環展開、並行處理、cache友好等。

4、 Makefile規則

target …:prerequisites …
command 1
command 2

target:即目標文件,可以是Object File、執行文件、一個標籤;
prerequisites:即要生成該target所需要(依賴)的文件或是目標;
command即make需要執行的命令,可以是任意的Shell命令。

注意,要執行的命令行一定要以一個Tab鍵作爲開頭。在Makefile中的定義的變量,就像是C/C++語言中的宏一樣,代表了一個文本字串,Makefile中執行的時候其會自動原模原樣地展開。變量在聲明時需要給予初值,而在使用時,需要給在變量名前加上“$”符號,但最好用小括號“()”或是大括號“{}”。

5、 kernels.c中的結構體team
team_t team={
“R1701”,//班級
“SCRECT”//姓名
“2017001”,//學號
“”,
“”
};

6、 64位系統中RGB像素點(結構體)的存儲
在這裏插入圖片描述
可以看到,每一個像素點佔0.75個字長。
進一步分析可知,只有1/2的像素點存儲在一個塊中,讀或寫餘下的像素點需要訪問兩個塊。

7、 數組在內存中的存儲原理

內存是一維的,因此不管是一維數組、二維數據、n維數組,在內存中都是以一維的方式存儲的。二維數組是以行優先的規則在內存中存儲的。

在這裏插入圖片描述

三、實驗步驟

說明:由於我的老師是把實驗文件直接放在了服務器裏並且設置好了每位同學的用戶名,這裏我將整個perfLab文件夾複製到本地計算機,通過本地編譯器編譯,以便進一步研究優化方案。如果老師是通過課程中心、通知羣、郵箱等下發的實驗文件,直接下載文件之後修改kernels.c就好啦!

1、 進入終端。若爲Windows操作系統,按下”Windows”+”R”,輸入”cmd”進入;若爲Linux操作系統,在首頁選擇”Terminal”。

2、 輸入指令” ssh 域名-l用戶名”,輸入密碼。登陸服務器。

3、 成功登陸之後,可以通過pwd命令與ls命令查看當前所在目錄與當前目錄下的文件。如果需要退出登錄,輸入exit。

4、 在登錄狀態下,cd進入perfLab文件夾內,執行perf_init命令進行初始化。
在這裏插入圖片描述

5、 從服務器中拷貝文件到本地:
通過”scp 本地文件路徑 用戶名@服務器地址:/服務器路徑”將本地文件拷貝到服務器,也可以通過” scp 用戶名@服務器地址:/服務器路徑 本地文件路徑”將服務器文件拷貝到本地。對於文件夾的拷貝,需要在scp後加”-r”參數。

這裏我先將實驗文件從服務器中拷貝到本地,修改完成之後再上傳回服務器。也可以直接在服務器通過vim修改文件。

6、 回到服務器,在perfLab文件夾下執行make。
在這裏插入圖片描述

7、 輸入“./driver”命令進行評分。
在這裏插入圖片描述

四、 程序優化各個版本

初始版本

在這裏插入圖片描述
對應代碼:

int i, j;
for (i = 0; i < dim; i++)
for (j = 0; j < dim; j++)
dst[RIDX(dim-1-j, i, dim)] = src[RIDX(i, j, dim)];

其他所有版本均與此版本作比較。可以看到Rotate的總分維持在5.0左右,與實驗環境有關。

版本一:分塊,旨在提高空間局部性

核心代碼如下:

int i,j,ki,kj;
//分塊,我這裏分爲8*8作爲一塊 
   for (i = 0; i < dim; i+=8) //每塊8行
	for (j = 0; j < dim; j+=8) //每塊8列
		for(ki=i; ki<i+8; ki++) 
			for(kj=j; kj<j+8; kj++) 
					dst[RIDX(dim-1-kj, ki, dim)] = src[RIDX(ki, kj, dim)];

運行結果爲:
在這裏插入圖片描述
可以看到Rotate的Summary由5.0提高至7.9,Dim規模較小時CPE優化不明顯,當Dim規模較大時可以看到CPE明顯有所下降(如對於1024*1024,CPE由35.4下降到11.6)。但總體來說,這一優化方法有效但不高效,需要再尋找其他方法。

版本二:在分塊的基礎上,循環展開(降低了循環開銷,但犧牲程序的尺寸)

核心代碼如下:

int i,j,ki,kj;
//分塊+循環展開,這裏32*32分塊,4*4循環展開
   for (i = 0; i < dim; i+=32) //每塊32行
	for (j = 0; j < dim; j+=32) //每塊32列
		for(ki=i; ki<i+32; ki+=4) //控制循環展開的行
			//控制循環展開的列
			for(kj=j; kj<j+32; kj+=4) {
				//相當於在4*4的小塊內,手寫每一個像素點的旋轉變換
				dst[RIDX(dim-1-kj, ki, dim)] = src[RIDX(ki, kj, dim)];
				dst[RIDX(dim-1-kj-1, ki, dim)] = src[RIDX(ki, kj+1, dim)];
				dst[RIDX(dim-1-kj-2, ki, dim)] = src[RIDX(ki, kj+2, dim)];
				dst[RIDX(dim-1-kj-3, ki, dim)] = src[RIDX(ki, kj+3, dim)];
				dst[RIDX(dim-1-kj, ki+1, dim)] = src[RIDX(ki+1, kj, dim)];
				dst[RIDX(dim-1-kj-1, ki+1, dim)] = src[RIDX(ki+1, kj+1, dim)];
				dst[RIDX(dim-1-kj-2, ki+1, dim)] = src[RIDX(ki+1, kj+2, dim)];
				dst[RIDX(dim-1-kj-3, ki+1, dim)] = src[RIDX(ki+1, kj+3, dim)];
				dst[RIDX(dim-1-kj, ki+2, dim)] = src[RIDX(ki+2, kj, dim)];
				dst[RIDX(dim-1-kj-1, ki+2, dim)] = src[RIDX(ki+2, kj+1, dim)];
				dst[RIDX(dim-1-kj-2, ki+2, dim)] = src[RIDX(ki+2, kj+2, dim)];
				dst[RIDX(dim-1-kj-3, ki+2, dim)] = src[RIDX(ki+2, kj+3, dim)];
				dst[RIDX(dim-1-kj, ki+3, dim)] = src[RIDX(ki+3, kj, dim)];
				dst[RIDX(dim-1-kj-1, ki+3, dim)] = src[RIDX(ki+3, kj+1, dim)];
				dst[RIDX(dim-1-kj-2, ki+3, dim)] = src[RIDX(ki+3, kj+2, dim)];
				dst[RIDX(dim-1-kj-3, ki+3, dim)] = src[RIDX(ki+3, kj+3, dim)];		
			}

運行結果如圖:
在這裏插入圖片描述
總體上性能優於原始版本與僅分塊版本,Summary達到9.4。但仔細分析CPE的變化,發現在輸入規模較小(64、128)時,使用循環展開後的性能反而不如不展開,但當輸入規模較大時,性能提升很明顯(如對於1024*1024,CPE由35.4下降到9.7)。

版本三:在前兩個版本的基礎上,改善讀寫順序

具體來說,先處理矩陣第一列的前32個元素,再處理第二列前32個元素,以此類推直到處理完畢矩陣的前32行,再以相同的方法繼續處理餘下的矩陣元素。核心代碼爲:

int i,j;
//分塊+循環展開+改變順序
   for (i = 0; i < dim; i+=16)
	for (j = 0; j < dim; j++) {
			  	//在這裏不再以行優先,而是每32行爲一塊,列優先
				dst[RIDX(dim-1-j, i, dim)] = src[RIDX(i, j, dim)];
				dst[RIDX(dim-1-j, i+1, dim)] = src[RIDX(i+1, j, dim)];
				dst[RIDX(dim-1-j, i+2, dim)] = src[RIDX(i+2, j, dim)];
				dst[RIDX(dim-1-j, i+3, dim)] = src[RIDX(i+3, j, dim)];
				dst[RIDX(dim-1-j, i+4, dim)] = src[RIDX(i+4, j, dim)];
				dst[RIDX(dim-1-j, i+5, dim)] = src[RIDX(i+5, j, dim)];
				dst[RIDX(dim-1-j, i+6, dim)] = src[RIDX(i+6, j, dim)];
				dst[RIDX(dim-1-j, i+7, dim)] = src[RIDX(i+7, j, dim)];
				dst[RIDX(dim-1-j, i+8, dim)] = src[RIDX(i+8, j, dim)];
				dst[RIDX(dim-1-j, i+9, dim)] = src[RIDX(i+9, j, dim)];
				dst[RIDX(dim-1-j, i+10, dim)] = src[RIDX(i+10, j, dim)];
				dst[RIDX(dim-1-j, i+11, dim)] = src[RIDX(i+11, j, dim)];
				dst[RIDX(dim-1-j, i+12, dim)] = src[RIDX(i+12, j, dim)];
				dst[RIDX(dim-1-j, i+13, dim)] = src[RIDX(i+13, j, dim)];
				dst[RIDX(dim-1-j, i+14, dim)] = src[RIDX(i+14, j, dim)];
				dst[RIDX(dim-1-j, i+15, dim)] = src[RIDX(i+15, j, dim)];
}

運行結果如圖:
在這裏插入圖片描述
總體上性能進一步提高,但仍存在不足。仔細分析CPE的變化,發現在輸入規模較小時改善讀寫順序可以大幅度提高程序性能,但對於輸入規模爲1024*1024這種情況下,版本三的CPE不如版本二的CPE。結合數組的存儲原理,推測這是由於當圖像規模較大時,同一列的相鄰32個像素點的讀寫間隔着比較多的讀寫,導致局部性變差。

注:初始版本、版本一到版本三運行環境爲公用1核2G服務器,
版本四到版本六運行環境爲自用1核2G服務器,
自用服務器評分偏高,對初始版本的評分爲6.5。

版本四:修改pixel結構,旨在令每一個像素點佔1整個塊

在這裏插入圖片描述
但考慮到複製每一個像素點需要訪問更多的字節,因此取結構體的red、green、blue分別複製。除此之外,改善讀寫順序。具體來說,先處理矩陣第一列的前32個元素,再處理第二列前32個元素,以此類推直到處理完畢矩陣的前32行,再以相同的方法繼續處理餘下的矩陣元素。核心代碼爲:

for (i = 0; i < dim; i+=32)
	for (j = 0; j < dim; j++)
		for(k=0; k<32; k++) {
			dst[RIDX(dim-1-j, i+k, dim)].red = src[RIDX(i+k, j, dim)].red;
			dst[RIDX(dim-1-j, i+k, dim)].green = src[RIDX(i+k, j, dim)].green;
			dst[RIDX(dim-1-j, i+k, dim)].blue = src[RIDX(i+k, j, dim)].blue;
	}

運行結果爲:
在這裏插入圖片描述
可以看到Rotate的Summary由6.5提高至12.9,但Dim規模較小時CPE優化不明顯,尤其是當Dim=64時該版本對應的CPE不降反增。另一方面,當Dim規模較大時可以看到CPE明顯有所下降,Dim=512對應的加速比更是達到了20.8。當Dim繼續增大時,加速比降低,由此推測該版本優化在Dim=512時的優化效果最爲明顯。但分析原理,雖然對於每個像素點都只需要訪問一個塊,但這樣處理無疑增加了塊的總數,推測這是制約程序性能進一步提高的因素。

版本五:消除函數調用並改善讀寫順序。

說明:考慮到程序過多次調用RIDX函數,故消除該函數的調用。此外,改善讀寫順序。具體來說,先處理矩陣第一列的前32個元素,再處理第二列前32個元素,以此類推直到處理完畢矩陣的前32行,再以相同的方法繼續處理餘下的矩陣元素。核心代碼爲:

int i,j,k;
for (i = 0; i < dim; i+=32)
	for (j = 0; j < dim; j++)
		for(k=0; k<32; k++) {
		//dst[RIDX(dim-1-j, i+k, dim)] = src[RIDX(i+k, j, dim)];
		dst[(dim-1-j)*dim+i+k] = src[(i+k)*dim+j];
		}

運行結果如圖:
在這裏插入圖片描述
總體上性能優於其他所有版本,推測cache塊結構爲32。在fcyc.c中查看到” #define CACHE_BLOCK 32”,證明推測正確。該版本Summary達到16.1。與版本四相比,當Dim<1024時性能均有明顯提升,但當Dim=1024時加速比出現明顯下降。

版本六:改善讀寫順序並循環站靠。

說明:改善讀寫順序的具體方案與版本五相同。循環展開,並使用指針代替RIDX進行數組訪問。核心代碼爲:

int i, j;
//src初始化爲第一行第一個像素點,dst與之對應,應指向最後一行第一個像素點
dst += (dim-1)*dim;
for (i = 0; i < dim; i+=32){ 
	for (j = 0; j < dim; j++){ 
		//循環展開,共32:
		*dst=*src; src+=dim; dst+=1;
        *dst=*src; src+=dim; dst+=1;
		*dst=*src; src+=dim; dst+=1;
		*dst=*src; src+=dim; dst+=1;
		*dst=*src; src+=dim; dst+=1;
		*dst=*src; src+=dim; dst+=1;
		*dst=*src; src+=dim; dst+=1;
		*dst=*src; src+=dim; dst+=1;
		*dst=*src; src+=dim; dst+=1;
		*dst=*src; src+=dim; dst+=1;
		*dst=*src; src+=dim; dst+=1;
		*dst=*src; src+=dim; dst+=1;
		*dst=*src; src+=dim; dst+=1;
		*dst=*src; src+=dim; dst+=1;
		*dst=*src; src+=dim; dst+=1;
		*dst=*src; src+=dim; dst+=1;
		*dst=*src; src+=dim; dst+=1;
		*dst=*src; src+=dim; dst+=1;
		*dst=*src; src+=dim; dst+=1;
		*dst=*src; src+=dim; dst+=1;
		*dst=*src; src+=dim; dst+=1;
		*dst=*src; src+=dim; dst+=1;
		*dst=*src; src+=dim; dst+=1;
		*dst=*src; src+=dim; dst+=1;
		*dst=*src; src+=dim; dst+=1;
		*dst=*src; src+=dim; dst+=1;
		*dst=*src; src+=dim; dst+=1;
		*dst=*src; src+=dim; dst+=1;
		*dst=*src; src+=dim; dst+=1;
		*dst=*src; src+=dim; dst+=1;
		*dst=*src; src+=dim; dst+=1;
		*dst=*src;
		//注意修改src與dst指向的位置,改爲下一列的相應32行
		src++;
		src-= (dim<<5)-dim; 
		dst-=31+dim;
	}
	dst+=dim*dim;
	dst+=32;
	src += (dim<<5)-dim;
}

運行結果如圖:
在這裏插入圖片描述
總體上性能不如版本五,對每個dim的加速比同樣不如版本五。分析加速比變化趨勢,發現當輸入圖像規模不斷增大,加速比先升後降,在Dim=512時取得最大值25.1。總的來說,Summary在14~16之間浮動,表明使用指針+循環展開+改善讀寫順序的程序性能受輸入圖像的影響較大。

五、遇到的問題及解決方法

1、 未進入perfLab文件夾導致無法make
在這裏插入圖片描述
解決方法:導致該問題的原因是沒有在指定的文件夾下執行make,解決方法爲cd進入perfLab文件夾,再執行make。
在這裏插入圖片描述
2、 選擇了不合適的分塊大小且未加入邊界檢查

選擇18x18作爲塊大小,運行時出現錯誤。執行評分時可以清楚地看到,第一個測試用例對應的Dim規模爲32x32,因此在未進行邊界檢查的情況下,會發生數組訪問越界,無法得到預期結果。加入邊界檢查,由於if判斷所帶來的時間開銷,導致程序性能明顯下降。綜合考慮,放棄了這種無法整分的分塊方法。

3、 代碼出現細節上的錯誤

在程序修改階段未仔細檢查,評分時出現:
在這裏插入圖片描述
檢查代碼,原來是實現分塊時,內循環與外循環的循環控制變量混淆了。這也提醒了我,在修改代碼時一定要認真仔細,有了這一次出錯的經驗,在後面實現循環展開時沒有再出現同樣的錯誤。

寫在最後:
如果覺得這篇博客幫到了你,請給博主點一個大大的贊!

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