寫程序最主要的目標就是使它在全部可能的狀況下都正確工做,另外一方面,在不少狀況下,讓程序運行得很快也是一個重要的考慮因素。運算優化html
編寫高效程序須要作到如下兩點:算法
第一點合適的算法和數據結構每每是你們寫程序時會首先考慮到的,而第二點常被忽略。這裏咱們就代碼優化而言,主要討論如何編寫可以被編譯器有效優化的源代碼,其中理解優化編譯器的能力和侷限性是很重要的。數組
如下咱們將舉例對常見的矩陣操做進行代碼優化。緩存
旋轉操做用下面兩步操做完成:數據結構
原理圖:
即對原有圖像矩陣先進行一次對摺,而後再進行一次翻轉,就能夠獲得咱們須要的逆時針旋轉90°以後的矩陣。函數
其中咱們用如下結構體表示一張圖像的像素點:性能
typedef struct { unsigned short red; /* R value */ unsigned short green; /* G value */ unsigned short blue; /* B value */ } pixel;
red、green、blue分別表示一張彩色圖像的紅綠藍三個通道。測試
原旋轉函數以下:優化
#define RIDX(i,j,n) ((i)*(n)+(j)) 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; }
圖像是標準的正方形,用一維數組表示,第(i,j)個像素表示爲I[RIDX(i,j,n)],n爲圖像邊長。編碼
參數:
RIDX(i,j,dim)讀取目標像素點,RIDX(dim-1-j,i,dim)將i、j參數位置互換,實現了斜角對摺,dim-1-j實現了上下翻轉。
當前咱們擁有一個driver.c文件,能夠對原函數和咱們優化的函數進行測試,獲得表示程序運行性能的CPE(每元素週期數)參數。
咱們的任務就是實現優化代碼,與原有代碼同時運行進行參數的對比,查看代碼優化狀況。
循環主體只存在一條語句,該語句爲內存的讀寫(讀取一個源像素,再寫入目標像素),不涉及函數調用與計算。因此咱們的優化手段有提升Cache命中率、避免複雜運算、分塊運算、循環展開與並行計算。
在矩陣運算中,提升Cache命中率是最容易想到的方法,常見的是外循環按行遍歷與外循環按列遍歷的對比,由於存儲順序是行序,因此前者的運行速度會明顯優於後者。
在已給出的naive_rotate函數中,核心循環語句涉及到讀取一個像素點與寫入一個像素點,顯然寫入像素點比讀取像素點更耗費時間,這是由存儲器的性質決定的,因此咱們應該優先對寫入像素點的索引進行優化。
上圖描述了8種數組索引順序,位於上方的藍色方塊表明原始圖像,黃色箭頭表示原始像素的讀取順序,位於下方的藍色方塊表明旋轉後圖像,紅色箭頭表示目標像素的寫入順序。
因爲循環體執行速度主要與數據寫入相關,因此咱們優先考慮紅色箭頭也就是寫入像素的cache命中率。
第一組到第四組的寫入像素都是按照列序,理論上寫入效果應該最差,第五第六組正向行序寫入執行效果應該是最好的,第七第八組逆向行序應該稍差。下面咱們給出分別按照8種不一樣順序索引的代碼,使用driver測試出他們的運行效率:
void rotate_leftup(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)]; } void rotate_leftdown(int dim, pixel *src, pixel *dst) { int i, j; for (i = dim-1; i > -1; i--) for (j = 0; j < dim; j++) dst[RIDX(dim-1-j, i, dim)] = src[RIDX(i, j, dim)]; } void rotate_rightup(int dim, pixel *src, pixel *dst) { int i, j; for (i = 0; i < dim; i++) for (j = dim-1; j > -1; j--) dst[RIDX(dim-1-j, i, dim)] = src[RIDX(i, j, dim)]; } void rotate_rightdown(int dim, pixel *src, pixel *dst) { int i, j; for (i = dim-1; i > -1; i--) for (j = dim-1; j > -1; j--) dst[RIDX(dim-1-j, i, dim)] = src[RIDX(i, j, dim)]; } void rotate_upleft(int dim, pixel *src, pixel *dst) { int i, j; for (j = 0; j < dim; j++) for (i = 0; i < dim; i++) dst[RIDX(dim-1-j, i, dim)] = src[RIDX(i, j, dim)]; } void rotate_upright(int dim, pixel *src, pixel *dst) { int i, j; for (j = dim-1; j > -1; j--) for (i = 0; i < dim; i++) dst[RIDX(dim-1-j, i, dim)] = src[RIDX(i, j, dim)]; } void rotate_downleft(int dim, pixel *src, pixel *dst) { int i, j; for (j = 0; j < dim; j++) for (i = dim-1; i > -1; i--) dst[RIDX(dim-1-j, i, dim)] = src[RIDX(i, j, dim)]; } void rotate_downright(int dim, pixel *src, pixel *dst) { int i, j; for (j = dim-1; j > -1; j--) for (i = dim-1; i > -1; i--) dst[RIDX(dim-1-j, i, dim)] = src[RIDX(i, j, dim)]; }
CPE與機器運行速度有關,測試機比較老,又是虛擬機環境,因此測得的CPE很低
與理論估計的同樣,前4組表現明顯最差,其中的第一組正是原始待優化的函數,與理論估計相符。
第5-8組差別不大,第五第六組比第七第八組效果略好,但整體優化效果很不明顯,從新檢查循環體的執行語句,發如今索引時宏定義中包含了乘法運算,嚴重阻礙了程序的執行效率。
以前在索引像素點時,是經過乘法運算進行索引,加大了沒必要要的開銷。若是使用矩陣的分塊運算,雖然可以利用局部性原理在必定程度上優化程序,但依舊會受到乘法運算的嚴重影響,因而咱們打算避免複雜運算經過循環展開的方式來對程序進一步優化。
具體的操做邏輯是,使用指針對元素進行索引,能夠把以前的8種圖像索引中的箭頭,分拆成32個平行的箭頭,經過指針運算一次處理32個像素,下面給出代碼來更好的理解:
//1 void rotate_pleftup(int dim, pixel *src, pixel *dst) { int i,j; for(i=0;i<dim;i+=32) for(j=0;j<dim;j++){ pixel *dptr=dst+RIDX(dim-1-j,i,dim); pixel *sptr=src+RIDX(i,j,dim); int step = -1; while(++step < 32){ *(dptr++) = *sptr; sptr += dim; } } } //2 void rotate_pleftdown(int dim, pixel *src, pixel *dst) { int i,j; for(i=dim-1;i>30;i-=32) for(j=0;j<dim;j++){ pixel *dptr=dst+RIDX(dim-1-j,i,dim); pixel *sptr=src+RIDX(i,j,dim); int step = 1; while(--step > -32){ *(dptr--) = *sptr; sptr -= dim; } } } //3 void rotate_prightup(int dim, pixel *src, pixel *dst) { int i,j; for(i=0;i<dim;i+=32) for(j=dim-1;j>-1;j--){ pixel *dptr=dst+RIDX(dim-1-j,i,dim); pixel *sptr=src+RIDX(i,j,dim); int step = -1; while(++step < 32){ *(dptr++) = *sptr; sptr += dim; } } } //4 void rotate_prightdown(int dim, pixel *src, pixel *dst) { int i,j; for(i=dim-1;i>30;i-=32) for(j=dim-1;j>-1;j--){ pixel *dptr=dst+RIDX(dim-1-j,i,dim); pixel *sptr=src+RIDX(i,j,dim); int step = 1; while(--step > -32){ *(dptr--) = *sptr; sptr -= dim; } } } //5 void rotate_pupleft(int dim, pixel *src, pixel *dst) { int i,j; for(j=0;j<dim;j+=32) for(i=0;i<dim;i++){ pixel *dptr=dst+RIDX(dim-1-j,i,dim); pixel *sptr=src+RIDX(i,j,dim); int step = -1; while(++step < 32){ *dptr = *(sptr++); dptr -= dim; } } }//6 void rotate_pupright(int dim, pixel *src, pixel *dst) { int i,j; for(j=dim-1;j>30;j-=32) for(i=0;i<dim;i++){ pixel *dptr=dst+RIDX(dim-1-j,i,dim); pixel *sptr=src+RIDX(i,j,dim); int step = -1; while(++step < 32){ *dptr = *(sptr--); dptr += dim; } } } //7 void rotate_pdownleft(int dim, pixel *src, pixel *dst) { int i,j; for(j = 0; j < dim; j+=32) for(i = dim-1; i > -1; i--){ pixel *dptr=dst+RIDX(dim-1-j,i,dim); pixel *sptr=src+RIDX(i,j,dim); int step = -1; while(++step < 32){ *dptr = *(sptr++); dptr -= dim; } } } //8 void rotate_pdownright(int dim, pixel *src, pixel *dst) { int i,j; for(j = dim-1; j > 30; j -= 32) for(i = dim-1; i > -1; i--){ pixel *dptr=dst+RIDX(dim-1-j,i,dim); pixel *sptr=src+RIDX(i,j,dim); int step = -1; while(++step < 32){ *dptr = *(sptr--); dptr += dim; } } }
指針每循環找到一個像素,會對其所在的某一行或某一列的32個像素進行變換,這樣既經過局部性提升了cache命中率,也可以有效的避開乘法運算形成的性能損失。如下是對優化一中的8個函數進行循環展開的優化狀況:
能夠看到,一、3的運行效果最好,二、4的運行效果相對略低,5-8運行效果最差,但即使是按照最差的順序循環展開,也遠遠超過了優化一中最好的索引順序,這也證實了乘法運算是阻礙以前優化的主要因素。
優化二中爲何變成了一、3運行效率最好?
經過以前的8種循環次序的分析圖,咱們能夠看到一、3兩組在寫入的時候,若是使用32路循環展開,每次均可以經過指針索引到後面31個像素(黑色箭頭表明其他31路的寫入),cache命中率最高:
優化二中的循環展開,其實也能夠看做是一種特殊的分塊運算,分塊大小爲1*32的小矩陣,各類優化方法之間整體來講具備相關性,大多都是基於cache緩存考慮。
優化三中咱們提升循環主語句運行的並行性,這裏咱們須要在32路循環時加入一個新的指針,在宏觀上來看循環主體每條語句是沒法並行的,但每一行代碼並非一個原子操做,微觀到線程級別來看是能夠出現並行的,這裏咱們只對優化二中最好的第一組進行修改:
void rotate_pleftup_4(int dim, pixel *src, pixel *dst) { int i,j; for(i=0;i<dim;i+=32) for(j=0;j<dim;j++) { pixel* dptr=dst+RIDX(dim-1-j,i,dim); pixel* sptr=src+RIDX(i,j,dim); pixel* dptr_ = dptr+1; pixel* sptr_ = sptr+dim; int step = -1; while(++step < 16){ *dptr = *sptr; sptr += dim+dim; dptr += 2; *dptr_ = *sptr_; sptr_ += dim+dim; dptr_ += 2; } } }
測試結果以下:
屢次運行的話,獲得的測試結果基本沒有性能差距,可是若是將循環指針繼續增長,使用4指針或者8指針循環,反而會出現性能降低的狀況。
從新對原函數進行分析,函數主要執行的只是像素點的讀寫而已,而且咱們已經去掉了耗時的乘法運算。這樣一來,沒什麼能並行運算的地方,代碼的並行性實際上並無什麼提高的空間,反而會隨着多個指針的加入使得循環過程變得複雜增大開銷,甚至可能會下降程序編譯時的效率。
另外,在沒什麼性能提高的狀況下,採用多個指針變量使得代碼可讀性變差,因此這裏咱們選擇優化二的版本。
這並不意味着提升並行性的方法很差,只是在當前環境下不適用而已,若是使用得當會在原有基礎上給程序帶來更好的性能提高。
下面對比一下優化前和優化後的代碼:
多出了5行循環語句,但加速比卻從1.2到了7.8,提高了6.5倍,不採用並行優化的狀況下代碼可讀性也未降低,這顯然是值得的。
咱們常常會涉及到關於矩陣的處理,特別是圖像處理方面,而圖像處理對性能有很高的需求。這只是一個矩陣操做/二維數組的簡單例子,代碼優化不侷限於此,咱們平時編碼中不少時候並無考慮那麼多,都是按照常規寫法逐步實現,這並無什麼不妥。可是當開始對本身的程序有提高性能的需求時,嘗試對本身的代碼作出優化不妨是一種更好的選擇,這是寫出高質量代碼的必要途徑。