當初在學校實驗室的時候,經常寫一個算法,讓程序跑着四處去晃盪一下回來,結果也就出來了。可工做後,算法效率彷佛重要多了,畢竟得真槍實彈放到產品中,賣給客戶的;不少時候,還要搞到嵌入式設備裏實時地跑,這麼一來真是壓力山大了~~~。這期間,對於程序優化也算略知皮毛,下面就針對這個問題講講。算法
首先說明一下,這裏說的程序優化是指程序效率的優化。通常來講,程序優化主要是如下三個步驟:數組
算法上的優化是必須首要考慮的,也是最重要的一步。通常咱們須要分析算法的時間複雜度,即處理時間與輸入數據規模的一個量級關係,一個優秀的算法能夠將算法複雜度下降若干量級,那麼一樣的實現,其平均耗時通常會比其餘複雜度高的算法少(這裏不表明任意輸入都更快)。性能
好比說排序算法,快速排序的時間複雜度爲O(nlogn),而插入排序的時間複雜度爲O(n*n),那麼在統計意義下,快速排序會比插入排序快,並且隨着輸入序列長度n的增長,二者耗時相差會愈來愈大。可是,假如輸入數據自己就已是升序(或降序),那麼實際運行下來,快速排序會更慢。測試
所以,實現一樣的功能,優先選擇時間複雜度低的算法。好比對圖像進行二維可分的高斯卷積,圖像尺寸爲MxN,卷積核尺寸爲PxQ,那麼優化
直接按卷積的定義計算,時間複雜度爲O(MNPQ)google
若是使用2個一維卷積計算,則時間複雜度爲O(MN(P+Q))
使用2個一位卷積+FFT來實現,時間複雜度爲O(MNlogMN)
若是採用高斯濾波的遞歸實現,時間複雜度爲O(MN)(參見paper:Recursive implementation of the Gaussian filter,源碼在GIMP中有)
很顯然,上面4種算法的效率是逐步提升的。通常狀況下,天然會選擇最後一種來實現。
還有一種狀況,算法自己比較複雜,其時間複雜度難以下降,而其效率又不知足要求。這個時候就須要本身好好地理解算法,作些修改了。一種是保持算法效果來提高效率,另外一種是捨棄部分效果來換取必定的效率,具體作法得根據實際狀況操做。
代碼優化通常須要與算法優化同步進行,代碼優化主要是涉及到具體的編碼技巧。一樣的算法與功能,不一樣的寫法也可能讓程序效率差別巨大。通常而言,代碼優化主要是針對循環結構進行分析處理,目前想到的幾條原則是:
a.避免循環內部的乘(除)法以及冗餘計算
這一原則是能把運算放在循環外的儘可能提出去放在外部,循環內部沒必要要的乘除法可以使用加法來替代等。以下面的例子,灰度圖像數據存在BYTE Img[MxN]的一個數組中,對其子塊 (R1至R2行,C1到C2列)像素灰度求和,簡單粗暴的寫法是:
1 int sum = 0; 2 for(int i = R1; i < R2; i++) 3 { 4 for(int j = C1; j < C2; j++) 5 { 6 sum += Image[i * N + j]; 7 } 8 }
但另外一種寫法:
1 int sum = 0; 2 BYTE *pTemp = Image + R1 * N; 3 for(int i = R1; i < R2; i++, pTemp += N) 4 { 5 for(int j = C1; j < C2; j++) 6 { 7 sum += pTemp[j]; 8 } 9 }
能夠分析一下兩種寫法的運算次數,假設R=R2-R1,C=C2-C1,前面一種寫法i++執行了R次,j++和sum+=...這句執行了RC次,則總執行次數爲3RC+R次加法,RC次乘法;同 樣地能夠分析後面一種寫法執行了2RC+2R+1次加法,1次乘法。性能孰好孰壞顯然可知。
b.避免循環內部有過多依賴和跳轉,使cpu能流水起來
關於CPU流水線技術可google/baidu,循環結構內部計算或邏輯過於複雜,將致使cpu不能流水,那這個循環就至關於拆成了n段重複代碼的效率。
另外ii值是衡量循環結構的一個重要指標,ii值是指執行完1次循環所需的指令數,ii值越小,程序執行耗時越短。下圖是關於cpu流水的簡單示意圖:
簡單而不嚴謹地說,cpu流水技術可使得循環在必定程度上並行,即上次循環未完成時便可處理本次循環,這樣總耗時天然也會下降。
先看下面一段代碼:
1 for(int i = 0; i < N; i++) 2 { 3 if(i < 100) a[i] += 5; 4 else if(i < 200) a[i] += 10; 5 else a[i] += 20; 6 }
這段代碼實現的功能很簡單,對數組a的不一樣元素累加一個不一樣的值,可是在循環內部有3個分支須要每次判斷,效率過低,有可能不能流水;能夠改寫爲3個循環,這樣循環內部就不 用進行判斷,這樣雖然代碼量增多了,但當數組規模很大(N很大)時,其效率能有至關的優點。改寫的代碼爲:
1 for(int i = 0; i < 100; i++) 2 { 3 a[i] += 5; 4 } 5 for(int i = 100; i < 200; i++) 6 { 7 a[i] += 10; 8 } 9 for(int i = 200; i < N; i++) 10 { 11 a[i] += 20; 12 }
關於循環內部的依賴,見以下一段程序:
1 for(int i = 0; i < N; i++) 2 { 3 int x = f(a[i]); 4 int y = g(x); 5 int z = h(x,y); 6 }
其中f,g,h都是一個函數,能夠看到這段代碼中x依賴於a[i],y依賴於x,z依賴於xy,每一步計算都須要等前面的都計算完成才能進行,這樣對cpu的流水結構也是至關不利的,盡 量避免此類寫法。另外C語言中的restrict關鍵字能夠修飾指針變量,即告訴編譯器該指針指向的內存只有其本身會修改,這樣編譯器優化時就能夠無所顧忌,但目前VC的編譯器彷佛不支 持該關鍵字,而在DSP上,當初使用restrict後,某些循環的效率可提高90%。
c.定點化
定點化的思想是將浮點運算轉換爲整型運算,目前在PC上我我的感受差異還不算大,但在不少性能通常的DSP上,其做用也不可小覷。定點化的作法是將數據乘上一個很大的數後,將 全部運算轉換爲整數計算。例如某個乘法我只關心小數點後3位,那把數據都乘上10000後,進行整型運算的結果也就知足所需的精度了。
d.以空間換時間
空間換時間最經典的就是查表法了,某些計算至關耗時,但其自變量的值域是比較有限的,這樣的狀況能夠預先計算好每一個自變量對應的函數值,存在一個表格中,每次根據自變量的 值去索引對應的函數值便可。以下例:
1 //直接計算 2 for(int i = 0 ; i < N; i++) 3 { 4 double z = sin(a[i]); 5 } 6 7 //查表計算 8 double aSinTable[360] = {0, ..., 1,...,0,...,-1,...,0}; 9 for(int i = 0 ; i < N; i++) 10 { 11 double z = aSinTable[a[i]]; 12 }
後面的查表法須要額外耗一個數組double aSinTable[360]的空間,但其運行效率卻快了不少不少。
e.預分配內存
預分配內存主要是針對須要循環處理數據的狀況的。好比視頻處理,每幀圖像的處理都須要必定的緩存,若是每幀申請釋放,則勢必會下降算法效率,以下所示:
1 //處理一幀 2 void Process(BYTE *pimg) 3 { 4 malloc 5 ... 6 free 7 } 8 9 //循環處理一個視頻 10 for(int i = 0; i < N; i++) 11 { 12 BYTE *pimg = readimage(); 13 Process(pimg); 14 }
1 //處理一幀 2 void Process(BYTE *pimg, BYTE *pBuffer) 3 { 4 ... 5 } 6 7 //循環處理一個視頻 8 malloc pBuffer 9 for(int i = 0; i < N; i++) 10 { 11 BYTE *pimg = readimage(); 12 Process(pimg, pBuffer); 13 } 14 free
前一段代碼在每幀處理都malloc和free,然後一段代碼則是有上層傳入緩存,這樣內部就不需每次申請和釋放了。固然上面只是一個簡單說明,實際狀況會比這複雜得多,但總體思想 是一致的。
對於通過前面算法和代碼優化的程序,通常其效率已經比較不錯了。對於某些特殊要求,還須要進一步下降程序耗時,那麼指令優化就該上場了。指令優化通常是使用特定的指令集,可快速實現某些運算,同時指令優化的另外一個核心思想是打包運算。目前PC上intel指令集有MMX,SSE和SSE2/3/4等,DSP則須要跟具體的型號相關,不一樣型號支持不一樣的指令集。intel指令集須要intel編譯器才能編譯,安裝icc後,其中有幫助文檔,有全部指令的詳細說明。
例如MMX裏的指令 __m64 _mm_add_pi8(__m64 m1, __m64 m2),是將m1和m2中8個8bit的數對應相加,結果就存在返回值對應的比特段中。假設2個N數組相加,通常須要執行N個加法指令,但使用上述指令只需執行N/8個指令,由於其1個指令能處理8個數據。
實現求2個BYTE數組的均值,即z[i]=(x[i]+y[i])/2,直接求均值和使用MMX指令實現2種方法以下程序所示:
1 #define N 800 2 BYTE x[N],Y[N], Z[N]; 3 inital x,y;... 4 //直接求均值 5 for(int i = 0; i < N; i++) 6 { 7 z[i] = (x[i] + y[i]) >> 1; 8 } 9 10 //使用MMX指令求均值,這裏N爲8的整數倍,不考慮剩餘數據處理 11 __m64 m64X, m64Y, m64Z; 12 for(int i = 0; i < N; i+=8) 13 { 14 m64X = *(__m64 *)(x + i); 15 m64Y = *(__m64 *)(y + i); 16 m64Z = _mm_avg_pu8(m64X, m64Y); 17 *(__m64 *)(x + i) = m64Z; 18 }
使用指令優化須要注意的問題有:
a.關於值域,好比2個8bit數相加,其值可能會溢出;若能保證其不溢出,則可以使用一次處理8個數據,不然,必須下降性能,使用其餘指令一次處理4個數據了;
b.剩餘數據,使用打包處理的數據通常都是四、8或16的整數倍,若待處理數據長度不是其單次處理數據個數的整數倍,剩餘數據需單獨處理;
補充——如何定位程序熱點
程序熱點是指程序中最耗時的部分,通常程序優化工做都是優先去優化熱點部分,那麼如何來定位程序熱點呢?
通常而言,主要有2種方法,一種是經過觀察與分析,經過分析算法,天然能知道程序熱點;另外一方面,觀察代碼結構,通常具備最大循環的地方就是熱點,這也是前面那些優化手段都針對循環結構的緣由。
另外一種方法就是利用工具來找程序熱點。x86下可使用vtune來定位熱點,DSP下可以使用ccs的profile功能定位出耗時的函數,更近一步地,經過查看編譯保留的asm文件,可具體分析每一個循環結構狀況,瞭解到該循環是否能流水,循環ii值,以及制約循環ii值是因爲變量的依賴仍是運算量等詳細信息,從而進行有針對性的優化。因爲Vtune剛給卸掉,無法截圖;下圖是CCS編譯生成的一個asm文件中一個循環的截圖:
最後提一點,某些代碼使用Intel編譯器編譯能夠比vc編譯器編譯出的程序快不少,我遇到過最快的可相差10倍。對於gcc編譯後的效率,目前還沒測試過。