MATLAB的编程效率杂谈第一篇:重新认识矢量,向量化编程

对于算法开发人员来说,MATLAB是常用的一个工具,但是由于历史原因以及传统认识的偏差,我们很多人只是把MATLAB当成一个高级计算器或者算法验证工具。很多人在编写MATLAB程序的时候,没有发挥MATLAB的优势,或者生硬的应用向量化编程,导致MATLAB程序运行效率很低,进而得出MATLAB只适合做预研,不适合产品部署的结论。

从大学时代开始接触MATLAB到现在,掐指一算马上10个年头了,从读研到现在的7年时间里,一直在研究MATLAB,使用MATLAB。对MATLAB的认识越来越多,也越来越觉得太博大精深,其多学科、跨领域的应用背景决定了我们每个个人不大可能涉猎其全部内容。在这里,仅就个人的经验做一些讨论。这里,先来说说矢量化编程(也叫向量化编程)这个MATLAB特有的,关于MATLAB的永恒的话题。

其传统的流行的观点大致如下:

尽量避免循环的使用,多使用MATLAB的内置函数。能用逻辑索引解决的就不用数值索引。使用变量前养成预分配内存的习惯。向量化计算代替逐点计算。能用普通数值数组完成的工作尽量不用Cell型数组。如果矩阵含有大量0元素,采用稀疏矩阵来提高运算速度和减少存储空间。

MATLAB在不断发展,随着版本的升级,有些矢量化编程所描述的观点基本没变,但是有些观点就需要修正了。如果还按原来的观点为指导进行编程,那么很多情况下并不能得到效率明显的改善,甚至还不如按原来观点所不提倡的编程方式编写的代码效率高。高版本的MATLAB矢量化编程是一个比较复杂的问题,遵循的原则之间都有互相制约、相辅相成的作用。

从MATLAB6.5版开始,MATLAB引入了JIT(just in time)技术和加速器(accelerator),并在后续版本中不断优化。到了现在(R2011b),很多情况下,循环体本身已经不是程序性能提高的瓶颈了,瓶颈更多的来源于循环体内部的代码实现方式,以及使用循环的方式。循环就是多次重复做一件事,如果这件事本身代码写得不优化,放在循环体内多次实现后必造成运行时间过长。

老版本的MATLAB对循环机制的支持不好,所以我们提倡避免循环,而高版本的MATLAB对循环机制的支持大大提高了,我们就不能像过去那样谈“循环”色变,非得千方百计避免循环。当使用了循环,造成程序运行时间过长时,我们不要武断地将代码运行效率低归结到使用了循环。有的时候我们千方百计的把一段代码矢量化了,凭自己编程经验(很多是从老版本遗留下来的经验)和常识(从老的教科书得到的)觉得程序很标准,很优化,殊不知实际测量时会发现性能不比采用很自然的想法实现的程序效率高多少,甚至还会降低。我们看下面这个例子:

 1 function JITAcceleratorTest
2 u = rand(1e6,1);%随机生成一个1*1000000的向量
3 v = zeros(1e6,1);
4 tic
5 u1 = u + 1;
6 time = toc;
7 disp([\'用向量化方法的时间是:\',num2str(time),\'秒!\']);
8 tic
9 for ii = 1:1000000
10 v(ii) = u(ii)+1;
11 end
12 time = toc;
13 disp([\'循环的时间是:\',num2str(time),\'秒!\']);
14
15 feature jit off;
16 tic
17 for ii = 1:1000000
18 v(ii) = u(ii)+1;
19 end
20 time = toc;
21 disp([\'只关闭jit的时间是:\',num2str(time),\'秒!\']);
22
23 feature accel off;
24
25 tic
26 for ii = 1:1000000
27 v(ii) = u(ii)+1;
28 end
29 time = toc;
30 disp([\'关闭accel和JIT的时间是:\',num2str(time),\'秒!\']);
31
32 feature accel on;%测试完毕重新打开accelerator和JIT
33 feature jit on;
34 end

我的电脑上运行结果如下:

用向量化方法的时间是:0.0095308秒!

循环的时间是:0.010176秒!

只关闭JIT的时间是:0.084027秒!

关闭accel和JIT的时间是:1.2673秒!

自从引入JIT和accelerator后,MATLAB对这两项功能默认都是打开的,这也是为什么高版本的MATLAB对循环的支持好。关闭JIT和accelerator需要用到MATLAB一个未公开(undocumented)的函数:feature。feature accel on/off 即为打开/关闭accelerator,类似的打开/关闭JIT是feature jit on/off。

上面代码算一个长向量与一个标量的和,我们会发现,JIT和accelerator都打开的状态下,循环和矢量化运算所需要的时间从统计意义上来讲,已经没有显著差别了。关闭JIT后,运行时间变为原来的8倍左右。而再关闭accelerator,运行时间立刻变为原来的100多倍。

今天先到这里,后续我准备陆续介绍一些心得。这里先梗概说下:MATLAB中调用效率最高的是built-in函数,单次标量方式调用的开销基本上比C++慢不了多少。如果是向量化的调用譬如:一次计算1到1000000这100万个数的正弦:sin(1:1e6)比C++的循环效率要高。MATLAB数组的单个元素访问效率大概是C++的1/3到1/4。其余数据类型和函数类型的调用效率千差万别,需要我们注意。