由于我们做的是大量循环运算,即使单次循环增加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