由于我們做的是大量循環運算,即使單次循環增加0.01s的額外運作時間,積累1E7次也是28小時,是以我們要優化循環内的代碼,以提高單次執行效率。
一、代碼優化的步驟
1. 定位需要進行優化的代碼段單次執行或者小量循環的代碼,不值得去花太大力氣去優化,是以我們首先定位到大量循環内的代碼(也即parfor循環内的代碼)。然後用matlab工具profiler分析代碼段中每一行的執行時間。假定待測定的代碼段寫在test.m中;在matlab command window中輸入:
profile on; test; profile viewer
運作完之後即可看到彈出視窗内對代碼執行時間記錄。
如下圖所示,函數ffrt_OneBlock中執行了Nsamp次循環,共占用18.362秒,而該循環中,耗時最長的是ssearch函數,為15.349秒;是以我們鎖定需要優化的代碼為ssearch函數。在視窗中可以繼續點選ssearch,檢查ssearch中具體是哪一行代碼最耗時。
2. matlab代碼優化方法在matlab官網文檔中,給出了優化代碼的建議[1][2],參考官方文檔和其他大神的部落格[3],總結優化建議如下:
1) 向量化計算,MATLAB的優勢是矩陣運算,是以能不寫循環就不要寫循環。
2) 盡量調用系統函數,少用自己寫的函數
3) 預先申請記憶體,是因為如果不預配置設定的話,MATLAB在每次循環執行的時候都會改變數組a的大小,則每次都會去記憶體查找适合此大小的記憶體區間,然後重新配置設定記憶體。該過程不僅會造成MATLAB代碼的執行速度變慢,而且很容易造成記憶體碎片化。
4) 少用cell,cell的數組不需要連續的記憶體空間進行存儲,是以需要記錄每個cell單元的位址等資訊,即使是空的cell單元,也會占用4個位元組的存儲空間。而且cell的通路是很慢的。
5) 不管是在MATLAB還是C/C++中,都有一個準則叫:變勤拿少取為少拿多取。具體含義就是減小循環的次數,盡量在每次循環中做比較多的資料處理,不管是資料讀取還是寫入。是以,在編寫帶有嵌套循環的代碼時,循環次數多的盡量在内層,少的盡量在外層。
6) 邏輯索引比數值索引更快
下面2種方式的索引:
a(a>0.5) = 0; % 邏輯索引
a(find(a>0.5)) = 0; % 數值索引
前者的代碼執行速度更快。
7) 指派給類型不比對的變量,比指派給新定義的變量更慢。
8) 如果要判斷多個條件是否滿足,盡量使用邏輯運算符 && 或者 || ,因為邏輯運算符隻有在第一個邏輯條件成立的時候才會計算第二個邏輯條件[4]。
9) 避免使用全局變量。
10) 如果需要大段代碼産生常值資料,可以考慮産生該資料,并儲存在mat檔案中,然後在運算中加載這些資料。
11) 盡量使用m函數,替代m腳本。
12) 盡量使用本地函數(local function)替代nested function。所謂本地函數,是指在同一個m檔案中寫了多個函數[5]。下面是一個帶有本地函數的檔案mystats.m:
function [avg, med] = mystats(x) n = length(x); avg = mymean(x,n); med = mymedian(x,n); end function a = mymean(v,n) % MYMEAN Example of a local function. a = sum(v)/n; end13) 子產品化程式設計,盡量将長段代碼分解為簡單函數。長段代碼中往往包含一些不常使用的代碼段,分解之後可以降低處理器讀取代碼的時間。子產品化程式設計可以降低首次運作時間。
14) 避免使用eval,evalc,evalin,feval(fname),改為使用feval+函數句柄的方式。從文本間接調用函數比較耗時。
15) 避免使用查詢狀态的函數,例如 inputname, which, whos, exist(var), dbstack, 這些函數比較耗時。
16) 需大量運作且無法向量化的深度循環,考慮将之改為c語言函數。
3. Matlab和c語言混合程式設計以本文中提及的ssearch函數為例,檢查其代碼并參考以上優化建議,發現隻有改為c語言函數或許能夠提高效率。是以考慮進行matlab和c語言混合程式設計。
首先,将m函數ssearch用c語言重寫,并儲存為csearch.c,并驗證其正确性。
其次,在csearch.c檔案中,增加matlab和c的接口函數mexFunction,該函數由mex.h提供,函數名稱、類型和參數都是确定的,我們隻需要改寫其實作。下面用一個矩陣相加的簡單例子給出mexFunction的使用方法:
#include "mex.h"//提供mexFunction函數的頭檔案,必須包含
#include "stdlib.h" #include "stdio.h" #include "math.h" int myfun(double *x, double *y, double *z, int col, int row) { int i=0, j=0; for(i=0; i<row; i++) for(j=0; j<col; j++) z[i*col+j]=x[i*col+j]+y[i*col+j]; return 1; } void mexFunction(int nlhs, mxArray *plhs[], int nrhs, const mxArray *prhs[]) {// mexFunction函數的參數定義為:nlhs—輸出參數個數, *plhs[]—輸出參數指針數組,
// nrhs—輸入參數個數, *prhs[]— 輸入參數指針數組。
double *X,*Y,*Z; int n,m;//取出輸入參數
m = mxGetM(prhs[0]);// 得到第一個輸入參數 X 的行數【Z=matrix_add(X,Y)】
n = mxGetN(prhs[0]);// 得到第一個輸入參數 X 的列數
X= mxGetPr(prhs[0]);// 得到指向第一個輸入參數 X 的指針
Y = mxGetPr(prhs[1]);//得到指向第二個輸入參數 Y 的指針
//定義輸出參數Z [ Z=matrix_add(X,Y) ]
plhs[0] = mxCreateDoubleMatrix(m,n, mxREAL);// 建立矩陣,并将輸出指針指向矩陣位址
Z=(double*)mxGetData(plhs[0]);//将指針Z也指向該矩陣
//使用使用者定義的函數進行計算:Z=X+Y
myfun(X,Y,Z,n,m);//調用使用者定義函數
//調用函數後, 輸出指針plhs[0]指向的位址中已經儲存了X+Y的結果。
//在matlab指令行視窗中, 輸入“mex matrix_add.c” 進行編譯,可以得到matrix_add.mexa64
//在matlab指令行視窗中, 輸入Z=matrix_add(X,Y) ,即可得到z=x+y的運算結果。
}改寫完之後,在matlab command window中輸入mex csearch.c即可得到執行檔案csearch.mexa64(linux) 或csearch.mexmaci64(mac) 或csearch.mexwin64(windows64)。然後,csearch即可與m函數一樣的在matlab中調用。
下面兩圖對比了使用c語言函數csearch和m函數ssearch的運作時間。運作4200次,兩個函數耗時分别為:0.114s和10.886s。如果運作1E7次,則耗時分别為4.5分鐘和7.2小時。
三、結論
- 在并行運算中,一般要執行很多次循環,優化代碼結構,提升單次循環的代碼效率能夠大大降低整個任務的運作時間。
- 準确定位影響運作時間的關鍵代碼是優化的前提。
- 如果無法通過向量化等方法優化matlab代碼,還可以嘗試使用matlab和c語言混合程式設計。
參考文獻
[1] https://www.mathworks.com/help/matlab/matlab_prog/techniques-for-improving-performance.html
[2] https://www.mathworks.com/company/newsletters/articles/accelerating-matlab-algorithms-and-applications.html
[3] http://blog.sina.com.cn/s/blog_6b597bfb01018sjm.html
[4] https://uk.mathworks.com/help/matlab/ref/logicaloperatorsshortcircuit.html
[5] https://uk.mathworks.com/help/matlab/matlab_prog/local-functions.html?searchHighlight=local%20functions&s_tid=doc_srchtitle