爱人要选温柔似水甜如蜜的,对手要选聪明能干强有力的,同事要选埋头苦干没脾气的,朋友要选猪头狗脑流鼻涕的,别看了,快把鼻涕擦了.....

【转载】MATLAB 效率分析

上一篇 / 下一篇  2008-07-05 07:42:00 / 个人分类:MATLAB

原文链接:http://dahua.spaces.live.com/blog/cns!28AF4251DF30CA42!2459.entry
6Db\9Rn$G0

MATLAB 效率再议

SimWe个人空间9\1Z&k&s3F8k]

于MATLAB的效率问题,很多文章,包括我之前写的一些,主要集中在使用向量化以及相关的问题上。但是,最近我在实验时对代码进行profile的过程 中,发现在新版本的MATLAB下,for-loop已经得到了极大优化,而效率的瓶颈更多是在函数调用和索引访问的过程中。

vg A&@1J A+CA,?0

2h5t'DCT2Vj/U0由于MATLAB特有的解释过程,不同方式的函数调用和元素索引,其效率差别巨大。不恰当的使用方式可能在本来不起眼的地方带来严重的开销,甚至可能使你的代码的运行时间增加上千倍(这就不是多买几台服务器或者增加计算节点能解决的了,呵呵)。 SimWe个人空间BQL5Q;b$e

|(~,i7p ROSzo0下 面通过一些简单例子说明问题。(实验选在装有Windows Vista的一台普通的台式机(Core2 Duo 2.33GHz + 4GB Ram)进行,相比于计算集群, 这可能和大部分朋友的环境更相似一些。实验过程是对某一个过程实施多次的整体进行计时,然后得到每次过程的平均时间,以减少计时误差带来的影响。多次实 验,在均值附近正负20%的范围内的置信度高于95%。为了避免算上首次运行时花在预编译上的时间,在开始计时前都进行充分的“热身”运行。) SimWe个人空间+q'v;I3mwP(Oe

函数调用的效率 SimWe个人空间K,If.{K(Q

'G"O nMg2X0一个非常简单的例子,把向量中每个元素加1。(当然这个例子根本不需要调函数,但是,用它主要是为了减少函数执行本身的时间,突出函数解析和调用的过程。)

*Gyip G/JO1D)A0SimWe个人空间)x ~)TY"S9H

作为baseline,先看看最直接的实现

$e^a#[ R0
% input u: u is a 1000000 x 1 vectorSimWe个人空间\Z%{Ua
v = u + 1; SimWe个人空间%Vs^J1j7xs1n

(` K&a ofL0这个过程平均需要0.00105 sec。而使用长期被要求尽量避免的for-loop SimWe个人空间8Cq o2gVq(u/i

n = numel(u);SimWe个人空间 d/W~T:sd(?mv[:MkS
% v = zeros(n, 1) has been pre-allocated.SimWe个人空间)d d0Th JP[
for i = 1 : n
,^k jJ T9d"U"S0 v(i) = u(i) + 1;
(T\#D#y:Oz1I0end
Qh_;j|+^#F`0
SimWe个人空间X[ OMd"^

所需的平均时间大概是0.00110 sec。从统计意义上说,和vectorization已经没有显著差别。无论是for-loop或者vectorization,每秒平均进行约10亿次“索引-读取-加法-写入”的过程,计算资源应该得到了比较充分的利用。 SimWe个人空间'?&m foQ z Vrc

fq6yhN v@0要是这个过程使用了函数调用呢?MATLAB里面支持很多种函数调用方式,主要的有m-function, function handle, anonymous function, inline, 和feval,而feval的主参数可以是字符串名字,function handle, anonymous function或者inline。 SimWe个人空间?-f_t(K

'pY6@+?.D|s0用m-function,就是专门定义一个函数SimWe个人空间D(`l%D9Oj

function y = fm(x)
'u8|YAB(Z0 y = x + 1;
.F}K7R$`pl0
在调用时
for i = 1 : nSimWe个人空间@0e\:WG
v(i) = fm(u(i));SimWe个人空间 N%k],W z3dp$p_ E%X&T
endSimWe个人空间+^3sYJ n4w

-s8Q9De/~b|0function handle就是用@来把一个function赋到一个变量上,类似于C/C++的函数指针,或者C#里面的delegate的作用

2~KQ2uj0
fh = @fm;SimWe个人空间
XvilEtR:If3q
for i = 1 : nSimWe个人空间 K;Uz7g2P2N
v(i) = fh(u(i));
(E3}Q(lS2U0end
jldyAWb1px0

2u?*S#l Wm-Q \'?-a0anonymous function是一种便捷的语法来构造简单的函数,类似于LISP, Python的lambda表达式

*w#I,^ C0c0H0
fa = @(x) x + 1;SimWe个人空间Mi5b!F
q }2^.M;u~h4L
for i = 1 : n
C2tEM5VI lc*|0 v(i) = fa(u(i));SimWe个人空间F.gp u@P&w1G)x.I
endSimWe个人空间5x&] N,E4vAe,[i7D1t@
SimWe个人空间E6j9f#} ?DI9Z[

inline function是一种传统的通过表达式字符串构造函数的过程

+R6ky7P)a0
fi = inline('x + 1', 'x');SimWe个人空间7FV
Y}(sh|5v2V:^
for i = 1 : nSimWe个人空间o*~*K W6F"U2N
v(i) = fi(u(i));
7r/o8|*@,f3@~?0X0endSimWe个人空间)f,rQ(zu/]"D
SimWe个人空间?l9x$|,ax9E4^^A

feval的好处在于可以以字符串方式指定名字来调用函数,当然它也可以接受别的参数。

a }'w}yL4^7f\0
v(i) = feval('fm', u(i));
qu|&Qm*s0v(i) = feval(fh, u(i));SimWe个人空间?gG3GP"s C
v(i) = feval(fa, u(i));
vc u:?(^;s["p0
对于100万次调用(包含for-loop本身的开销,函数解析(resolution),压栈,执行加法,退栈,把返回值赋给接收变量),不同的方式的时间差别很大:
m-function 0.385 sec
function handle 0.615 sec
anonymous function 0.635 sec
inline function 166.00 sec
feval('fm', u(i)) 8.328 sec
feval(fh, u(i)) 0.618 sec
feval(fa, u(i)) 0.652 sec
feval(@fm, u(i)) 2.788 sec
feval(@fa, u(i)) 34.689 sec

F|5g O h(E"Jv5x0从这里面,我们可以看到几个有意思的现象:

5N^u5T"V6n0
  • 首先,调用自定义函数的开销远远高于一个简单的计算过程。直接写 u(i) = v(i) + 1 只需要  0.0011 秒左右,而写u(i) = fm(v(i)) 则需要0.385秒,时间多了几百倍,而它们干的是同一件事情。这说明了,函数调用的开销远远大于for-loop自己的开销和简单计算过程——在不同情 况可能有点差别,一般而言,一次普通的函数调用花费的时间相当于进行了几百次到一两千次双精度浮点加法。
  • 使用function handle和anonymous function的开销比使用普通的m-函数要高一些。这可能是由于涉及到句柄解析的时间,而普通的函数在第一次运行前已经在内存中进行预编译。
  • inline function的运行时间则令人难以接受了,竟然需要一百多秒(是普通函数调用的四百多倍,是直接计算的十几万倍)。这是因为matlab是在每次运行时临时对字符串表达式(或者它的某种不太高效的内部表达)进行parse。
  • feval(fh, u(i))和fh(u(i)),feval(fa, u(i))和fa(u(i))的运行时间很接近,表明feval在接受句柄为主参数时本身开销很小。但是,surprising的是
    for i = 1 : n
    tC%@ qqLf0|0 v(i) = feval(@fm, u(i));
    3uza y-qG0endSimWe个人空间2{!h'@0Ht(H;l4j+C$n
    比起
    n$tq;})Fui[0fh = @fm;SimWe个人空间,kwP:x o}!Y
    for i = 1 : nSimWe个人空间Byed7a8W/P9D@
    v(i) = feval(fh, u(i));
    fr*K,t7uD+G~0endSimWe个人空间"ZDu Zr
    慢了4.5倍 (前者0.618秒,后者2.788秒)。SimWe个人空间 ^/e(x!^*YzETW
    for i = 1 : n
    jg7G HKER S ~0 v(i) = feval(@(x) x + 1, u(i));
    %KsFj%B)G\/p0end
    f:{9o^:][q0比起SimWe个人空间G5NrYo-Q
    fa = @(x) x + 1;SimWe个人空间C@5s*dW!J1oA+P
    for i = 1 : nSimWe个人空间y)E3K[6J;R/[#DL
    v(i) = feval(fa, u(i));
    SV7vOs Szy W+B0end
    ]Lea'l6N[0竟然慢了53倍(前者0.652秒,后者34.689秒)。SimWe个人空间|KD'Jd0ez4f\
    SimWe个人空间 H5z:IJ:YJ

    由于在MATLAB的内部实现中,function handle的解析是在赋值过程中进行的,所以预先用一个变量把句柄接下,其效果就是预先完成了句柄解析,而如果直接把@fm或者@(x) x + 1写在参数列上,虽然写法简洁一些,但是解析过程是把参数被赋值到所调函数的局部变量时才进行,每调用一次解析一次,造成了巨大的开销。

    #zt+q_P#{H0
  • feval使用字符串作为函数名字时,所耗时间比传入句柄大,因为这涉及到对函数进行搜索的时间(当然这个搜索是在一个索引好的cache里面进行(除了第一次),而不是在所有path指定的路径中搜索。)
SimWe个人空间}4[N2`A

在2007年以后,MATLAB推出了arrayfun函数,上面的for-loop可以写为

@7fI!q"zoD5O"b0
v = arrayfun(fh, u)SimWe个人空间(TA2R9t"k6o(~ P9n
这平均需要4.48 sec,这比起for-loop(需时0.615 sec)还慢了7倍多。这个看上去“消除了for-loop"的函数,由于其内部设计的原因,未必能带来效率上的正效果。

元素和域的访问

6ytsV |S/?$x0SimWe个人空间c[w%@BC aXX

除了函数调用,数据的访问方式对于效率也有很大影响。MATLAB主要支持下面一些形式的访问: SimWe个人空间%YOVE#s;Je

  • array-index  A(i): 
  • cell-index:  C{i}; 
  • struct field:  S.fieldname
  • struct field (dynamic):  S.('fieldname')
SimWe个人空间%BW.cup|&|X?

这里主要探索单个元素或者域的访问(当然,MATLAB也支持对于子数组的非常灵活整体索引)。 SimWe个人空间:MW nea,]vpI-Z#X

h&qT(H Q TZM B0对于一百万次访问的平均时间

$sh aR"b w CC0
A(i) for a numeric array 0.0052 sec
C{i} for a cell array 0.2568 sec
struct field 0.0045 sec
struct field (with dynamic name) 1.0394 sec
SimWe个人空间,Kl{ApU.j&T.z.p

我们可以看到MATLAB对于单个数组元素或者静态的struct field的访问,可以达到不错的速度,在主流台式机约每秒2亿次(连同for-loop的开销)。而cell array的访问则明显缓慢,约每秒400万次(慢了50倍)。MATLAB还支持灵活的使用字符串来指定要访问域的语法(动态名字),但是,是以巨大的 开销为代价的,比起静态的访问慢了200倍以上。

-H&r@j G0

关于Object-oriented Programming

a6i XR7np0SimWe个人空间.R m^#ylH?%[

MATLAB在新的版本中(尤其是2008版),对于面向对象的编程提供了强大的支持。在2008a中,它对于OO的支持已经不亚于 python等的高级脚本语言。但是,我在实验中看到,虽然在语法上提供了全面的支持,但是matlab里面面向对象的效率很低,开销巨大。这里仅举几个 例子。

#} dn|&~ D(g.z e@0
  • object中的property的访问速度是3500万次,比struct field慢了6-8倍。MATLAB提供了一种叫做dependent property的属性,非常灵活,但是,效率更低,平均每秒访问速度竟然低至2.6万次(这种速度基本甚至难以用于中小规模的应用中)。
  • object中method调用的效率也明显低于普通函数的调用,对于instance method,每百万次调用,平均需时5.8秒,而对于static method,每百万次调用需时25.8秒。这相当于每次调用都需要临时解析的速度,而matlab的类方法解析的效率目前还明显偏低。
  • MATLAB中可以通过改写subsref和subsasgn的方法,对于对象的索引和域的访问进行非常灵活的改造,可以通过它创建 类似于数组的对象。但是,一个符合要求的subsref的行为比较复杂。在一个提供了subsref的对象中,大部分行为都需要subsref进行调度, 而默认的较优的调度方式将被掩盖。在一个提供了subsref的类中(使用一种最快捷的实现),object property的平均访问速度竟然降到1万5千次每秒。

建议

5P ^3`o"a_ XJg0

~$H:vx!]J Z/o0根据上面的分析,对于撰写高效MATLAB代码,我有下面一些建议: SimWe个人空间 S:C3fYu4o

  1. 虽然for-loop的速度有了很大改善,vectorization(向量化)仍旧是改善效率的重要途径,尤其是在能把运算改写成矩阵乘法的情况下,改善尤为显著。
  2. 在不少情况下,for-loop本身已经不构成太大问题,尤其是当循环体本身需要较多的计算的时候。这个时候,改善概率的关键在于改善循环体本身而不是去掉for-loop。
  3. MATLAB的函数调用过程(非built-in function)有显著开销,因此,在效率要求较高的代码中,应该尽可能采用扁平的调用结构,也就是在保持代码清晰和可维护的情况下,尽量直接写表达式 和利用built-in function,避免不必要的自定义函数调用过程。在次数很多的循环体内(包括在cellfun, arrayfun等实际上蕴含循环的函数)形成长调用链,会带来很大的开销。
  4. 在调用函数时,首选built-in function,然后是普通的m-file函数,然后才是function handle或者anonymous function。在使用function handle或者anonymous function作为参数传递时,如果该函数被调用多次,最好先用一个变量接住,再传入该变量。这样,可以有效避免重复的解析过程。
  5. 在可能的情况下,使用numeric array或者struct array,它们的效率大幅度高于cell array(几十倍甚至更多)。对于struct,尽可能使用普通的域(字段,field)访问方式,在非效率关键,执行次数较少,而灵活性要求较高的代 码中,可以考虑使用动态名称的域访问。
  6. 虽然object-oriented从软件工程的角度更为优胜,而且object的使用很多时候很方便,但是MATLAB目前对于OO的实现效率很低,在效率关键的代码中应该慎用objects。
  7. 如果需要设计类,应该尽可能采用普通的property,而避免灵活但是效率很低的dependent property。如非确实必要,避免重载subsref和subsasgn函数,因为这会全面接管对于object的接口调用,往往会带来非常巨大的开 销(成千上万倍的减慢),甚至使得本来几乎不是问题的代码成为性能瓶颈。
SimWe个人空间UYzX-r])sP

TAG:

 

评分:0

我来说两句

显示全部

:loveliness: :handshake :victory: :funk: :time: :kiss: :call: :hug: :lol :'( :Q :L ;P :$ :P :o :@ :D :( :)

日历

« 2008-08-29  
     12
3456789
10111213141516
17181920212223
24252627282930
31      

数据统计

  • 访问量: 32448
  • 日志数: 58
  • 图片数: 3
  • 文件数: 24
  • 书签数: 12
  • 建立时间: 2006-06-26
  • 更新时间: 2008-08-20

RSS订阅

Open Toolbar