本次實驗是CSAPP的第5個實驗,此次實驗主要是讓咱們熟悉如何優化程序,如何寫出更具備效率的代碼。經過此次實驗,咱們能夠更好的理解計算機的工做原理,在之後編寫代碼時,具備能結合軟硬件思考的能力。c++
@算法
本次實驗主要處理優化內存密集型代碼。圖像處理提供了許多能夠從優化中受益的功能示例。在本實驗中,咱們將考慮兩種圖像處理操做:旋轉,可將圖像逆時針旋轉90o,平滑,能夠「平滑」或「模糊」圖片。數組
在本實驗中,咱們將考慮將圖像表示爲二維矩陣M,其中\({M_{i,j}}\)表示M的第(i,j)個像素的值,像素值是紅色,綠色和藍色(RGB)值的三倍。咱們只會考慮方形圖像。令N表示圖像的行(或列)數。行和列以C樣式編號,從0到N − 1。緩存
給定這種表示形式,旋轉操做能夠很是簡單地實現爲如下兩個矩陣運算:bash
轉置:對於每對(i,j),\({M_{i,j}}\)和\({M_{j,i}}\)是互換的數據結構
交換行:第i行與第N-1 − i行交換。函數
具體以下圖所示工具
經過用周圍全部像素的平均值替換每一個像素值(在以該像素爲中心的最大3×3窗口)中替換每一個像素值來實現平滑操做。以下圖所示。像素的值\(M2[1][1]\) 和\(M2[N - 1][N - 1]\)以下所示:性能
\(M2[1][1] = \frac{{\sum\nolimits_{i = 0}^2 {\sum\nolimits_{j = 0}^2 {M1[i][j]} } }}{9}\)測試
\(M2[N - 1][N - 1] = \frac{{\sum\nolimits_{i = N - 2}^{N - 1} {\sum\nolimits_{j = N - 2}^{N - 1} {M1[i][j]} } }}{4}\)
本次實驗中,咱們須要修改惟一文件是kernels.c。driver.c程序是一個驅動程序,可以讓對咱們修改的程序進行評分。使用命令make driver生成驅動程序代碼並使用./driver命令運行它。
圖像的核心數據是用結構體表示的。像素是一個結構,以下所示:
typedef struct { unsigned short red; /* R value */ unsigned short green; /* G value */ unsigned short blue; /* B value */ } pixel;
能夠看出,RGB值具備16位表示形式(「 16位顏色」)。圖像I表示爲一維像素陣列,其中第(i,j)個像素爲I [RIDX(i,j,n)]。這裏n是圖像矩陣的維數, RIDX是定義以下的宏:
#define RIDX(i,j,n) ((i)*(n)+(j))
有關此代碼,請參見文件defs.h。
如下C函數計算將源圖像src旋轉90°的結果,並將結果存儲在目標圖像dst中。dim是圖像的尺寸。
void naive_rotate(int dim, pixel *src, pixel *dst) { 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)]; return; }
上面的代碼掃描源圖像矩陣的行,而後複製到目標圖像矩陣的列中。咱們的任務是使用代碼移動,循環展開和阻塞等技術重寫此代碼,以使其儘量快地運行。(有關此代碼,請參見文件kernels.c。)
平滑功能將源圖像src做爲輸入,並在目標圖像dst中返回平滑結果。這是實現的一部分:
void naive_smooth(int dim, pixel *src, pixel *dst) { int i, j; for(i=0; i < dim; i++) for(j=0; j < dim; j++) dst[RIDX(i,j,dim)] = avg(dim, i, j, src); /* Smooth the (i,j)th pixel */ return; }
函數avg返回第(i,j)個像素周圍全部像素的平均值。咱們的任務是優化平滑(和avg)以儘量快地運行。 (注意:函數avg是一個局部函數,能夠徹底擺脫它而以其餘方式實現平滑)。(這段代碼(以及avg的實現)位於kernels.c文件中。)
咱們的主要性能指標是CPE。若是某個函數須要C個週期來運行大小爲N×N的圖像,則CPE值爲\(C/{N^2}\)。
咱們能夠編寫旋轉和平滑例程的許多版本。爲了幫助您比較編寫的全部不一樣版本的性能,咱們提供了一種「註冊」功能的方式。
例如,咱們提供給您的文件kernels.c包含如下功能:
void register_rotate_functions() { add_rotate_function(&rotate, rotate_descr); }
此函數包含一個或多個調用以添加旋轉函數。在上面的示例中,添加旋轉函數將函數旋轉與字符串旋轉說明一塊兒註冊,該字符串是函數功能的ASCII描述。請參閱文件kernels.c以瞭解如何建立字符串描述。該字符串的長度最多爲256個字符。
將編寫的源代碼將與咱們提供給驅動程序二進制文件的目標代碼連接。要建立此二進制文件,您將須要執行如下命令
unix> make driver
每次更改kernels.c中的代碼時,都須要從新制做驅動程序。要測試您的實現,而後能夠運行如下命令:
unix> ./driver
該驅動程序能夠在四種不一樣的模式下運行:
默認模式,在其中運行實施的全部版本。
Autograder模式,其中僅運行rotation()和smooth()函數。這是當咱們使用驅動程序對您的切紙進行評分時將運行的模式。
文件模式,其中僅運行輸入文件中提到的版本。
轉儲模式,其中每一個版本的單行描述轉儲到文本文件中。而後,您能夠編輯該文本文件,以僅使用文件模式保留要測試的版本。您能夠指定是在轉儲文件以後退出仍是要運行您的實現。
若是不帶任何參數運行,驅動程序將運行全部版本(默認模式)。其餘模式和選項能夠經過驅動程序的命令行參數來指定,以下所示:
-g:僅運行rotate()和smooth()函數(自動分級模式)。
-f
-d
-q :將版本名稱轉儲到轉儲文件後退出。與-d一塊兒使用。例如,要在打印轉儲文件後當即退出,請鍵入./driver -qd dumpfile。
-h:打印命令行用法。
emsp; 回顧下經常使用的優化程序的方法,總結以下:
(1)高級設計
爲遇到的問題選擇適當的算法和數據結構。要特別警覺,避免使用那些會漸進地產生糟糕性能的算法或編碼技術。
(2)基本編碼原則
避免限制優化的因素,這樣編譯器就能產生高效的代碼。
消除連續的函數調用。在可能時,將計算移到循環外。考慮有選擇地妥協程序的模塊性以得到更大的效率。
消除沒必要要的內存引用。引入臨時變量來保存中間結果。只有在最後的值計算出來時,纔將結果存放到數組或全局變量中。
(3)低級優化
結構化代碼以利用硬件功能。
展開循環,下降開銷,而且使得進一步的優化成爲可能。
經過使用例如多個累積變量和從新結合等技術,找到方法提升指令級並行。
用功能性的風格重寫條件操做,使得編譯採用條件數據傳送。
(4)使用性能分析工具
當處理大型程序時,將注意力集中在最耗時的部分變得很重要。代碼剖析程序和相關的工具能幫助咱們系統地評價和改進程序性能。咱們描述了 GPROF,一個標準的Unix剖析工具。還有更加複雜完善的剖析程序可用,例如 Intel的VTUNE程序開發系統,還有 Linux系統基本上都有的 VALGRIND。這些工具能夠在過程級分解執行時間,估計程序每一個基本塊( basic block)的性能。(基本塊是內部沒有控制轉移的指令序列,所以基本塊老是整個被執行的。)
在這一部分中,咱們將優化旋轉以實現儘量低的CPE。您應該編譯驅動程序,而後使用適當的參數運行它以測試您的實現。例如,運行提供的原始版本(用於旋轉)的驅動程序將生成以下所示的輸出:
函數源碼以下:
void naive_rotate(int dim, pixel *src, pixel *dst) { 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)]; return; }
其中,defs.h中RIDX定義爲:#define RIDX(i,j,n) ((i)*(n)+(j))下面詳細分析下程序。
i = 0 j = 0 dest[20] = src[0] i = 1 j = 0 dest[21] = src[5] i = 0 j = 1 dest[15] = src[1] i = 1 j = 1 dest[16] = src[6] i = 0 j = 2 dest[10] = src[2] i = 1 j = 2 dest[11] = src[7] i = 0 j = 3 dest[5] = src[3] i = 1 j = 3 dest[6] = src[8] i = 0 j = 4 dest[0] = src[4] i = 1 j = 4 dest[1] = src[9]
具體以下圖所示:
這段代碼的做用就是將dim * dim大小的方塊中全部的像素進行行列調位、致使整幅圖畫進行了90度旋轉。觀察源代碼咱們發現,程序進行了嵌套循環,隨着dim的增長,循環的複雜度愈來愈大,並且每循環一次,dim-1-j就要計算一次,所以,咱們考慮進行分塊優化。
對於循環分塊,這裏的分塊指的是一個應用級的數據組塊,而不是高速緩存中的塊,這樣構造程序,能將一個片加載到L1高速緩存中去,並在這個片中進行所須要的全部讀和寫,而後丟掉這個片,加載下一個片,以此類推。
/*分塊:8 * 8*/ char rotate_descr[] = "rotate1: Current working version"; void rotate(int dim, pixel *src, pixel *dst) { int i,j,i1,j1; for(i=0; i < dim; i+=8) for(j=0; j < dim; j+=8) for(i1=i; i1 < i+8; i1++) for(j1=j; j1 < j+8; j1++) dst[RIDX(dim-1-j1,i1,dim)] = src[RIDX(i1,j1,dim)]; }
優化後的版本測試以下所示:
右上圖能夠看到,得分有了明顯的提高,Dim規模較小時,提高並不明顯,在Dim爲1024*1024時,由原來的17.1下降到了6.0.說明咱們的方法仍是有效的,可是最後的總得得分只有9.3分,效果不是很好。
char rotate_descr[] = "rotate2: Current working version"; void rotate(int dim, pixel *src, pixel *dst) { int i,j,i1,j1; for(i=0; i < dim; i+=32) for(j=0; j < dim; j+=32) for(i1=i; i1 < i+32; i1++) for(j1=j; j1 < j+32; j1++) dst[RIDX(dim-1-j1,i1,dim)] = src[RIDX(i1,j1,dim)]; }
本次繼續採用的是分塊策略,分爲了32塊,可是由下圖的得分能夠看到,性能基本有提高,因此,須要換個思路了。
在版本二的基礎上,咱們進行循環展開,32路並行,並使用指針代替RIDX進行數組訪問,這裏犧牲了程序的尺寸來換取速度優化。
char rotate_descr[] = "rotate3: Current working version"; void rotate(int dim, pixel *src, pixel *dst) { int i,j; int dst_base = (dim-1)*dim; dst +=dst_base; for(i = 0;i < dim;i += 32){ for(j = 0;j < dim;j++){ *dst = *src; src +=dim; dst++; //31組 *dst = *src; src +=dim; dst++; *dst = *src; src +=dim; dst++; *dst = *src; src +=dim; dst++; *dst = *src; src +=dim; dst++; *dst = *src; src +=dim; dst++; *dst = *src; src +=dim; dst++; *dst = *src; src +=dim; dst++; *dst = *src; src +=dim; dst++; *dst = *src; src +=dim; dst++; *dst = *src; src +=dim; dst++; *dst = *src; src +=dim; dst++; *dst = *src; src +=dim; dst++; *dst = *src; src +=dim; dst++; *dst = *src; src +=dim; dst++; *dst = *src; src +=dim; dst++; *dst = *src; src +=dim; dst++; *dst = *src; src +=dim; dst++; *dst = *src; src +=dim; dst++; *dst = *src; src +=dim; dst++; *dst = *src; src +=dim; dst++; *dst = *src; src +=dim; dst++; *dst = *src; src +=dim; dst++; *dst = *src; src +=dim; dst++; *dst = *src; src +=dim; dst++; *dst = *src; src +=dim; dst++; *dst = *src; src +=dim; dst++; *dst = *src; src +=dim; dst++; *dst = *src; src +=dim; dst++; *dst = *src; src +=dim; dst++; *dst = *src; src +=dim; dst++; *dst = *src; src++; src -= (dim<<5)-dim; //src -=31*dim; dst -=31+dim; } dst +=dst_base + dim; dst +=32; src +=(dim<<5)-dim; //src +=31*dim; } }
咱們在版本二的基礎上,將原來的程序的內循環展開成32個並行,每一次同時處理32個像素點,即將循環次數減小了32倍,大大加速了程序。分數由版本二的10.3分漲到了15.3分。特別是在哦1024 * 1024時,由15.1直接降到了5.0,性能提高仍是很明顯的。
在這一部分中,您將優化平滑度以實現儘量低的CPE。例如,運行提供的樸素版本(爲了平滑)的驅動程序將生成以下所示的輸出:
unix> ./driver Smooth: Version = naive_smooth: Naive baseline implementation: Dim 32 64 128 256 512 Mean Your CPEs 695.8 698.5 703.8 720.3 722.7 Baseline CPEs 695.0 698.0 702.0 717.0 722.0 Speedup 1.0 1.0 1.0 1.0 1.0 1.0
void naive_smooth(int dim, pixel *src, pixel *dst) { int i, j; for(i=0; i < dim; i++) for(j=0; j < dim; j++) dst[RIDX(i,j,dim)] = avg(dim, i, j, src); /* Smooth the (i,j)th pixel */ return; }
這個函數的做用是平滑圖像,在smooth函數中由於要求周圍點的平均值,因此會頻繁的調用avg函數,並且avg函數仍是一個2層for循環,因此咱們能夠考慮循環展開或者消除函數調用等方法,減小avg函數調用和循環。
Smooth函數處理分爲4塊,一爲主體內部,由9點求平均值;二爲4個頂點,由4點求平均值;三爲四條邊界,由6點求平均值。從圖片的頂部開始處理,再上邊界,順序處理下來,其中在處理左邊界時,for循環處理一行主體部分。
未經優化的函數性能以下,得分爲12.2分。
void smooth(int dim, pixel *src, pixel *dst) { pixel_sum rowsum[530][530]; int i, j, snum; for(i=0; i<dim; i++) { rowsum[i][0].red = (src[RIDX(i, 0, dim)].red+src[RIDX(i, 1, dim)].red); rowsum[i][0].blue = (src[RIDX(i, 0, dim)].blue+src[RIDX(i, 1, dim)].blue); rowsum[i][0].green = (src[RIDX(i, 0, dim)].green+src[RIDX(i, 1, dim)].green); rowsum[i][0].num = 2; for(j=1; j<dim-1; j++) { rowsum[i][j].red = (src[RIDX(i, j-1, dim)].red+src[RIDX(i, j, dim)].red+src[RIDX(i, j+1, dim)].red); rowsum[i][j].blue = (src[RIDX(i, j-1, dim)].blue+src[RIDX(i, j, dim)].blue+src[RIDX(i, j+1, dim)].blue); rowsum[i][j].green = (src[RIDX(i, j-1, dim)].green+src[RIDX(i, j, dim)].green+src[RIDX(i, j+1, dim)].green); rowsum[i][j].num = 3; } rowsum[i][dim-1].red = (src[RIDX(i, dim-2, dim)].red+src[RIDX(i, dim-1, dim)].red); rowsum[i][dim-1].blue = (src[RIDX(i, dim-2, dim)].blue+src[RIDX(i, dim-1, dim)].blue); rowsum[i][dim-1].green = (src[RIDX(i, dim-2, dim)].green+src[RIDX(i, dim-1, dim)].green); rowsum[i][dim-1].num = 2; } for(j=0; j<dim; j++) { snum = rowsum[0][j].num+rowsum[1][j].num; dst[RIDX(0, j, dim)].red = (unsigned short)((rowsum[0][j].red+rowsum[1][j].red)/snum); dst[RIDX(0, j, dim)].blue = (unsigned short)((rowsum[0][j].blue+rowsum[1][j].blue)/snum); dst[RIDX(0, j, dim)].green = (unsigned short)((rowsum[0][j].green+rowsum[1][j].green)/snum); for(i=1; i<dim-1; i++) { snum = rowsum[i-1][j].num+rowsum[i][j].num+rowsum[i+1][j].num; dst[RIDX(i, j, dim)].red = (unsigned short)((rowsum[i-1][j].red+rowsum[i][j].red+rowsum[i+1][j].red)/snum); dst[RIDX(i, j, dim)].blue = (unsigned short)((rowsum[i-1][j].blue+rowsum[i][j].blue+rowsum[i+1][j].blue)/snum); dst[RIDX(i, j, dim)].green = (unsigned short)((rowsum[i-1][j].green+rowsum[i][j].green+rowsum[i+1][j].green)/snum); } snum = rowsum[dim-1][j].num+rowsum[dim-2][j].num; dst[RIDX(dim-1, j, dim)].red = (unsigned short)((rowsum[dim-2][j].red+rowsum[dim-1][j].red)/snum); dst[RIDX(dim-1, j, dim)].blue = (unsigned short)((rowsum[dim-2][j].blue+rowsum[dim-1][j].blue)/snum); dst[RIDX(dim-1, j, dim)].green = (unsigned short)((rowsum[dim-2][j].green+rowsum[dim-1][j].green)/snum); } }
在以上的優化中,咱們取消了對avg函數的直接調用,而是直接對像素點的fgb顏色分別求均值,而且將重複利用的數據存儲在了數組之中,所以,速度比以前有所提高,可是提高並不高,由12.2提高到15.4。
void smooth(int dim, pixel *src, pixel *dst) { int i,j; int dim0=dim; int dim1=dim-1; int dim2=dim-2; pixel *P1, *P2, *P3; pixel *dst1; P1=src; P2=P1+dim0; //左上角像素處理 dst->red=(P1->red+(P1+1)->red+P2->red+(P2+1)->red)>>2; dst->green=(P1->green+(P1+1)->green+P2->green+(P2+1)->green)>>2; dst->blue=(P1->blue+(P1+1)->blue+P2->blue+(P2+1)->blue)>>2; dst++; //上邊界處理 for(i=1;i<dim1;i++) { dst->red=(P1->red+(P1+1)->red+(P1+2)->red+P2->red+(P2+1)->red+(P2+2)->red)/6; dst->green=(P1->green+(P1+1)->green+(P1+2)->green+P2->green+(P2+1)->green+(P2+2)->green)/6; dst->blue=(P1->blue+(P1+1)->blue+(P1+2)->blue+P2->blue+(P2+1)->blue+(P2+2)->blue)/6; dst++; P1++; P2++; } //右上角像素處理 dst->red=(P1->red+(P1+1)->red+P2->red+(P2+1)->red)>>2; dst->green=(P1->green+(P1+1)->green+P2->green+(P2+1)->green)>>2; dst->blue=(P1->blue+(P1+1)->blue+P2->blue+(P2+1)->blue)>>2; dst++; P1=src; P2=P1+dim0; P3=P2+dim0; //左邊界處理 for(i=1;i<dim1;i++) { dst->red=(P1->red+(P1+1)->red+P2->red+(P2+1)->red+P3->red+(P3+1)->red)/6; dst->green=(P1->green+(P1+1)->green+P2->green+(P2+1)->green+P3->green+(P3+ 1)->green)/6; dst->blue=(P1->blue+(P1+1)->blue+P2->blue+(P2+1)->blue+P3->blue+(P3+1)->blue)/6; dst++; dst1=dst+1; //中間主體部分處理 for(j=1;j<dim2;j+=2) { //同時處理兩個像素 dst->red=(P1->red+(P1+1)->red+(P1+2)->red+P2->red+(P2+1)->red+(P2+2)->red+P3->red+(P3+1)->red+(P3+2)->red)/9; dst->green=(P1->green+(P1+1)->green+(P1+2)->green+P2->green+(P2+1)->green+(P2+2)->green+P3->green+(P3+1)->green+(P3+2)->green)/9; dst->blue=(P1->blue+(P1+1)->blue+(P1+2)->blue+P2->blue+(P2+1)->blue+(P2+2)->blue+P3->blue+(P3+1)->blue+(P3+2)->blue)/9; dst1->red=((P1+3)->red+(P1+1)->red+(P1+2)->red+(P2+3)->red+(P2+1)->red+(P2+2)->red+(P3+3)->red+(P3+1)->red+(P3+2)->red)/9; dst1->green=((P1+3)->green+(P1+1)->green+(P1+2)->green+(P2+3)->green+(P2+1)->green+(P2+2)->green+(P3+3)->green+(P3+1)->green+(P3+2)->green)/9; dst1->blue=((P1+3)->blue+(P1+1)->blue+(P1+2)->blue+(P2+3)->blue+(P2+1)->blue+(P2+2)->blue+(P3+3)->blue+(P3+1)->blue+(P3+2)->blue)/9; dst+=2; dst1+=2; P1+=2; P2+=2; P3+=2; } for(;j<dim1;j++) { dst->red=(P1->red+(P1+1)->red+(P1+2)->red+P2->red+(P2+1)->red+(P2+2)->red+P3->red+(P3+1)->red+(P3+2)->red)/9; dst->green=(P1->green+(P1+1)->green+(P1+2)->green+P2->green+(P2+1)->green+(P2+2)->green+P3->green+(P3+1)->green+(P3+2)->green)/9; dst->blue=(P1->blue+(P1+1)->blue+(P1+2)->blue+P2->blue+(P2+1)->blue+(P2+2)->blue+P3->blue+(P3+1)->blue+(P3+2)->blue)/9; dst++; P1++; P2++; P3++; } //右側邊界處理 dst->red=(P1->red+(P1+1)->red+P2->red+(P2+1)->red+P3->red+(P3+1)->red)/6; dst->green=(P1->green+(P1+1)->green+P2->green+(P2+1)->green+P3->green+(P3+1)->green)/6; dst->blue=(P1->blue+(P1+1)->blue+P2->blue+(P2+1)->blue+P3->blue+(P3+1)->blue)/6; dst++; P1+=2; P2+=2; P3+=2; } //右下角處理 dst->red=(P1->red+(P1+1)->red+P2->red+(P2+1)->red)>>2; dst->green=(P1->green+(P1+1)->green+P2->green+(P2+1)->green)>>2; dst->blue=(P1->blue+(P1+1)->blue+P2->blue+(P2+1)->blue)>>2; dst++; //下邊界處理 for(i=1;i<dim1;i++) { dst->red=(P1->red+(P1+1)->red+(P1+2)->red+P2->red+(P2+1)->red+(P2+2)->red)/6; dst->green=(P1->green+(P1+1)->green+(P1+2)->green+P2->green+(P2+1)->green+(P2+2)->green)/6; dst->blue=(P1->blue+(P1+1)->blue+(P1+2)->blue+P2->blue+(P2+1)->blue+(P2+2)->blue)/6; dst++; P1++; P2++; } //右下角像素處理 dst->red=(P1->red+(P1+1)->red+P2->red+(P2+1)->red)>>2; dst->green=(P1->green+(P1+1)->green+P2->green+(P2+1)->green)>>2; dst->blue=(P1->blue+(P1+1)->blue+P2->blue+(P2+1)->blue)>>2; }
在這個版本中,咱們在版本一的基礎上繼續優化。將Smooth函數分爲內部-頂點-邊界的四部分,一爲主體內部,由9點求平均值;二爲4個頂點,由4點求平均值;三爲四條邊界,由6點求平均值。從圖片的頂部開始處理,再上邊界,順序處理下來,其中在處理左邊界時,for循環處理一行主體部分,就是以上的代碼。
下圖爲測試結果,由版本一的分15.4分提高到了44.1分,性能提高顯著!
本次實驗的趣味性不如前幾個實驗,難度也沒有前幾個實驗的大。在實際優化程序時,咱們不能一味的爲了速度而展開程序,或者消除函數引用,以程序的體積和可讀性去換取性能的提高是很是不划算的。在保證可讀性的前提下儘量去提高程序的性能。
有任何問題,都可經過公告中的二維碼聯繫我