MATLAB面向对象编程慢吗,还是我做错了什么?

154

我正在尝试使用MATLABOOP,首先我模仿了我的C++ Logger类,并将所有字符串辅助函数放入一个String类中。我认为这样做很好,可以执行a+ba==ba.find(b)等操作,而不是使用strcat(a,b)strcmp(a,b)、检索第一个元素strfind(a,b)等。

问题:速度变慢

我开始使用上述方法后立即注意到了明显的减速。我是不是做错了什么(由于我对MATLAB的经验非常有限,这当然是可能的),或者MATLAB的OOP确实引入了很多额外的开销?

我的测试案例

这是我为字符串执行的简单测试,基本上只是添加一个字符串并再次删除已添加的部分:

注意:在真正的代码中不要像这样编写String类!现在Matlab有一个原生的string数组类型,您应该使用它。

classdef String < handle
  ....
  properties
    stringobj = '';
  end
  function o = plus( o, b )
    o.stringobj = [ o.stringobj b ];
  end
  function n = Length( o )
    n = length( o.stringobj );
  end
  function o = SetLength( o, n )
    o.stringobj = o.stringobj( 1 : n );
  end
end

function atest( a, b ) %plain functions
  n = length( a );
  a = [ a b ];
  a = a( 1 : n );

function btest( a, b ) %OOP
  n = a.Length();
  a = a + b;
  a.SetLength( n );

function RunProfilerLoop( nLoop, fun, varargin )
  profile on;
  for i = 1 : nLoop
    fun( varargin{ : } );
  end
  profile off;
  profile report;

a = 'test';
aString = String( 'test' );
RunProfilerLoop( 1000, @(x,y)atest(x,y), a, 'appendme' );
RunProfilerLoop( 1000, @(x,y)btest(x,y), aString, 'appendme' );

测试结果

1000次迭代的总时间(秒):

btest 0.550(其中包括String.SetLength 0.138、String.plus 0.065和String.Length 0.057)

atest 0.015

对于记录系统,同样有如下结果:使用fprintf(1, 'test\n')进行1000次调用需要0.1秒;但是,当在内部使用String类时,使用1000次我的系统需要7秒(虽然它里面有更多的逻辑,但与C++进行比较:使用std::string("blah")std::cout作为输出端与使用纯粹的std::cout << "blah"相比,我的系统的开销大约为1毫秒)。

只是查找类/包函数时出现的开销吗?

MATLAB是解释性的语言,因此必须在运行时查找函数/对象的定义。因此,我想知道查找类或包函数与查找路径中的函数相比是否会涉及到更多的开销。我试图进行测试,但结果却越来越奇怪。为了排除类/对象的影响,我将路径中的一个函数与包中的一个函数进行了比较:

function n = atest( x, y )
  n = ctest( x, y ); % ctest is in matlab path

function n = btest( x, y )
  n = util.ctest( x, y ); % ctest is in +util directory, parent directory is in path

结果,以与上述相同的方式收集:

atest 0.004秒,在ctest中为0.001秒

btest 0.060秒,在util.ctest中为0.014秒

那么,所有这些开销是否只是来自MATLAB花费时间查找其面向对象编程实现的定义,而对于直接在路径中的函数,则不存在此开销?


5
谢谢您的提问!Matlab堆(OOP / 闭包)的性能问题困扰了我多年,详情请参见https://dev59.com/hnM_5IYBdhLWcg3wRw91。我非常想知道MatlabDoug / Loren / MikeKatz会对您的帖子做出何种回应。 - Mikhail Poda
1
那是一篇有趣的阅读。 - stijn
1
@MatlabDoug:也许你的同事Mike Karr可以评论一下OP? - Mikhail Poda
4
读者还应该查看这篇最新博客文章(作者是Dave Foti),讨论最新R2012a版本中面向对象MATLAB代码的性能:考虑面向对象MATLAB代码的性能 - Amro
1
一个简单的代码结构敏感性示例,其中子元素方法的调用被移出循环。 for i = 1:this.get_n_quantities() if(strcmp(id,this.get_quantity_rlz(i).get_id())) ix = i; end end 耗时2.2秒,而 nq = this.get_n_quantities(); a = this.get_quantity_realizations(); for i = 1:nq c = a{i}; if(strcmp(id,c.get_id())) ix = i; end end 只需要0.01秒,快了两个数量级。 - Jose Ospina
显示剩余3条评论
4个回答

241
我已经使用面向对象的MATLAB工作了一段时间,最终发现存在类似的性能问题。 简短回答是:是的,MATLAB的面向对象有点慢。方法调用的开销很大,比主流的面向对象语言更高,并且你不能做太多关于它的事情。部分原因可能是惯用的MATLAB使用“向量化”的代码来减少方法调用的数量,而每个调用的开销并不是一个高优先级的问题。
我通过编写“nop”函数来对各种类型的函数和方法进行性能基准测试。以下是一些典型结果。 >> call_nops 计算机: PCWIN 发行版: 2009b 调用每个函数/方法100000次 nop() 函数: 0.02261 秒 0.23 微秒每次调用 nop1-5() 函数: 0.02182 秒 0.22 微秒每次调用 nop() 子函数: 0.02244 秒 0.22 微秒每次调用 @()[] 匿名函数: 0.08461 秒 0.85 微秒每次调用 nop(obj) 方法: 0.24664 秒 2.47 微秒每次调用 nop1-5(obj) 方法: 0.23469 秒 2.35 微秒每次调用 nop() 私有函数: 0.02197 秒 0.22 微秒每次调用 classdef nop(obj): 0.90547 秒 9.05 微秒每次调用 classdef obj.nop(): 1.75522 秒 17.55 微秒每次调用 classdef private_nop(obj): 0.84738 秒 8.47 微秒每次调用 classdef nop(obj) (m-file): 0.90560 秒 9.06 微秒每次调用 classdef class.staticnop(): 1.16361 秒 11.64 微秒每次调用 Java nop(): 2.43035 秒 24.30 微秒每次调用 Java static_nop(): 0.87682 秒 8.77 微秒每次调用 Java nop() from Java: 0.00014 秒 0.00 微秒每次调用 MEX mexnop(): 0.11409 秒 1.14 微秒每次调用 C nop(): 0.00001 秒 0.00 微秒每次调用
类似的结果在R2008a到R2009b上。这是在运行32位MATLAB的Windows XP x64上。
“Java nop()”是一个什么都不做的Java方法,在M代码循环内部调用,并包括每个调用时的MATLAB到Java派遣开销。“Java nop() from Java”是相同的方法,在Java for()循环中调用,不会产生边界惩罚。对于Java和C的计时,请保持警觉;聪明的编译器可以完全优化调用掉。包作用域机制是新的机制,与classdef类大约同时引入。它的行为可能相关。 一些初步结论: •方法比函数慢。 •新样式(classdef)方法比旧样式方法慢。 •新的 obj.nop()语法比相同方法在classdef对象上使用nop(obj)语法慢。Java对象也是如此(未显示)。如果想要快速运行,请调用nop(obj)。 •64位MATLAB在Windows上的方法调用开销更高(约为2倍)。 (未显示。) •MATLAB方法分发比其他一些语言要慢。 为什么是这样的纯属个人猜测。MATLAB引擎的面向对象内部机制不是公开的。这不是解释和编译问题本身 - MATLAB具有JIT - 但MATLAB的宽松类型和语法可能意味着运行时需要更多的工作。(例如,仅从语法无法确定“f(x)”是函数调用还是数组索引;它取决于运行时工作区的状态。)这可能是因为MATLAB的类定义以某种与许多其他语言不同的方式与文件系统状态绑定。 那么,该怎么办? 这方面的一种典型的MATLAB方法是通过将您的类定义结构化以便对象实例包装数组来“向量化”您的代码;也就是说,它的每个字段都包含并行数组(称为“平面”组织在MATLAB文档中)。而不是具有带标量值的字段的对象数组,定义本身是数组的对象,并使方法接受数组作为输入,并对字段和输入进行向量化调用。这减少了所做的方法调用次数,希望足够减少分发开销。 在MATLAB中模仿C ++或Java类可能不是最优的。 Java / C++类通常构建为对象是最小的构建块,尽可能具体(即,有很多不同的类),并在其中使用数组、集合对象等进行组成,并使用循环对其进行迭代。要创建快速的MATLAB类,请将该方法颠倒过来。具有大型字段数组的较大类,并在这些数组上调用向量化方法。 重点是安排代码以发挥语言的优势 - 数组处理,向量化数学 - 并避免弱点。 编辑:自原始帖子发布以来,已经推出了R2010b和R2011a。总体情况相同,MCOS调用变得更快,而Java和旧样式方法调用变得更慢。 编辑:我曾在这里放置有关“路径敏感性”的一些注释,附加了一张功能调用时间表,在该时间表中,函数时间受Matlab路径配置方式的影响,但那似乎是我特定网络设置的异常情况在那个时候。上面的表反映了我多年测试中典型的时间。

更新:R2011b

编辑(2012年2月13日):R2011b已经发布,性能已经得到了足够的改善以便进行更新。

计算机信息:PCWIN,版本:2011b 
设备信息:R2011b,Windows XP,8核心i7-2600 @ 3.40GHz,3 GB RAM,NVIDIA NVS 300
每个操作执行100000次
样式                             总时间          每次调用消耗的µ秒数
nop()函数:                        0.01578         0.16
nop()函数,10倍循环展开:           0.01477         0.15
nop()函数,100倍循环展开:          0.01518         0.15
nop()子函数:                      0.01559         0.16
@()[]匿名函数:                    0.06400         0.64
nop(obj)方法:                     0.28482         2.85
nop()私有函数:                    0.01505         0.15
classdef nop(obj):               0.43323         4.33
classdef obj.nop():              0.81087         8.11
classdef private_nop(obj):       0.32272         3.23
classdef class.staticnop():      0.88959         8.90
classdef 所有常量:                1.51890        15.19
classdef 属性:                   0.12992         1.30
classdef 带getter的属性:         1.39912        13.99
+pkg.nop()函数:                   0.87345         8.73
+pkg.nop()从+pkg内部调用:          0.80501         8.05
Java obj.nop():                  1.86378        18.64
Java nop(obj):                   0.22645         2.26
Java feval('nop',obj):           0.52544         5.25
Java Klass.static_nop():        0.35357         3.54
Java obj.nop()从Java中调用:      0.00010         0.00
MEX mexnop():                    0.08709         0.87
C nop():                         0.00001         0.00
j()(内置):                      0.00251         0.03

我认为这表明:

  • MCOS/classdef方法更快。只要使用foo(obj)语法,成本现在与旧样式类大致相当。因此,在大多数情况下,方法速度不再是坚持旧样式类的原因。(MathWorks干得好!)
  • 将函数放入命名空间会使它们变慢。(在R2011b中不是新问题,只是我的测试中新出现的。)

更新:R2014a

我已重构了基准测试代码,并在R2014a上运行它。

在PCWIN64上的Matlab R2014a Matlab 8.3.0.532 (R2014a) / Java 1.7.0_11,在PCWIN64 Windows 7 6.1 (eilonwy-win7)上运行 设备: Core i7-3615QM CPU @ 2.30GHz, 4 GB RAM (VMware Virtual Platform) 迭代次数nIters = 100000
操作 时间(微秒) nop()函数: 0.14 nop()子函数: 0.14 @()[]匿名函数: 0.69 nop(obj)方法: 3.28 nop()@class私有函数: 0.14 classdef nop(obj): 5.30 classdef obj.nop(): 10.78 classdef pivate_nop(obj): 4.88 classdef class.static_nop(): 11.81 classdef 常量: 4.18 classdef 属性: 1.18 classdef getter属性: 19.26 +pkg.nop()函数: 4.03 +pkg中的+pkg.nop(): 4.16 feval('nop'): 2.31 feval(@nop): 0.22 eval('nop'): 59.46 Java obj.nop(): 26.07 Java nop(obj): 3.72 Java feval('nop',obj): 9.25 Java Klass.staticNop(): 10.54 Java obj.nop()从Java中: 0.01 MEX mexnop(): 0.91 builtin j(): 0.02 结构体s.foo字段访问: 0.14 isempty(persistent): 0.00
更新:R2015b:对象变快了! 这是由@Shaked友情提供的R2015b结果。 这是一个重大的改变:面向对象编程显着更快,现在obj.method()语法与method(obj)一样快,并且比旧的面向对象程序要快得多。在PCWIN64上的Matlab R2015b Matlab 8.6.0.267246 (R2015b) / Java 1.7.0_60在PCWIN64 Windows 8 6.2(nanit-shaked)上 机器:Core i7-4720HQ CPU @ 2.60GHz,16 GB RAM(20378) nIters = 100000
操作 时间(微秒) nop() 函数: 0.04 nop() 子函数: 0.08 @()[] 匿名函数: 1.83 nop(obj) 方法: 3.15 nop() 在@class上的私有fcn: 0.04 classdef nop(obj): 0.28 classdef obj.nop(): 0.31 classdef pivate_nop(obj): 0.34 classdef class.static_nop(): 0.05 classdef 常量: 0.25 classdef 属性: 0.25 classdef 具有getter的属性: 0.64 +pkg.nop() 函数: 0.04 +pkg.nop() 在+pkg内部: 0.04 feval('nop'): 8.26 feval(@nop): 0.63 eval('nop'): 21.22 Java obj.nop(): 14.15 Java nop(obj): 2.50 Java feval('nop',obj): 10.30 Java Klass.staticNop(): 24.48 从Java中的Java obj.nop(): 0.01 MEX mexnop(): 0.33 builtin j(): 0.15 结构体s.foo字段访问: 0.25 isempty(persistent): 0.13

更新:R2018a

这是R2018a的结果。它并没有像R2015b中引入新执行引擎时那样有很大的跳跃,但仍然是一年一度的可观提升。值得注意的是,匿名函数处理速度大大提高。

在MACI64的Matlab R2018a上, Matlab版本为9.4.0.813654(R2018a)/ Java 1.8.0_144, 操作系统为Mac OS X 10.13.5(eilonwy)。 机器配置为Core i7-3615QM CPU @ 2.30GHz,16 GB RAM, nIters = 100000。
操作 时间(微秒) nop()函数 0.03 nop()子函数 0.04 @()[]匿名函数 0.16 classdef nop(obj) 0.16 classdef obj.nop() 0.17 classdef私有nop(obj) 0.16 classdef class.static_nop() 0.03 classdef常量 0.16 classdef属性 0.13 classdef带getter的属性 0.39 +pkg.nop()函数 0.02 +pkg中的+pkg.nop()函数 0.02 feval('nop') 15.62 feval(@nop) 0.43 eval('nop') 32.08 Java obj.nop() 28.77 Java nop(obj) 8.02 Java feval('nop',obj) 21.85 Java Klass.staticNop() 45.49 Java中来自Java的obj.nop() 0.03 MEX mexnop() 3.54 内建j() 0.10 结构体s.foo字段访问 0.16 isempty(persistent) 0.07
更新:R2018b和R2019a:没有改变
更新:R2021a:对象速度更快了!
似乎classdef对象又变得显着更快了,但结构体变慢了。在 MACI64 上运行 Matlab R2021a Matlab 9.10.0.1669831 (R2021a) Update 2 / Java 1.8.0_202 on MACI64 Mac OS X 10.14.6 (eilonwy) 机器: Core i7-3615QM CPU @ 2.30GHz, 4 核心, 16 GB RAM nIters = 100000
操作 时间 (μsec) nop() 函数: 0.03 nop() 子函数: 0.04 @()[] 匿名函数: 0.14 nop(obj) 方法: 6.65 nop() 私有函数在@class中: 0.02 classdef nop(obj): 0.03 classdef obj.nop(): 0.04 classdef pivate_nop(obj): 0.03 classdef class.static_nop(): 0.03 classdef 常量: 0.16 classdef 属性: 0.12 classdef 具有getter的属性: 0.17 +pkg.nop() 函数: 0.02 +pkg.nop() 在+pkg内部: 0.02 feval('nop'): 14.45 feval(@nop): 0.59 eval('nop'): 23.59 Java obj.nop(): 30.01 Java nop(obj): 6.80 Java feval('nop',obj): 18.17 Java Klass.staticNop(): 16.77 Java obj.nop() 来自Java: 0.02 MEX mexnop(): 2.51 builtin j(): 0.21 struct s.foo 字段访问: 0.29 isempty(persistent): 0.26
基准测试的源代码已发布在 GitHub 上,采用 MIT 许可证发布。https://github.com/apjanke/matlab-bench

5
你认为你能否再次使用R2012a运行基准测试?这很有趣。 - Dang Khoa
8
大家好。如果你们还对源代码感兴趣,我已经重建它并在GitHub上进行了开源。https://github.com/apjanke/matlab-bench - Andrew Janke
2
@AndrewJanke 这是链接:https://gist.github.com/ShakedDovrat/62db9e8f6883c5e28fc0 - Shaked
2
哇!如果这些结果保持不变,我可能需要修改整个答案。已添加。谢谢! - Andrew Janke
2
我非常喜欢这些更新,请继续推出新的! - dleal
显示剩余13条评论

4

Handle类还有一个额外的开销,因为它要跟踪所有对自身的引用以进行清理。

不使用Handle类尝试相同的实验,并查看结果。


1
使用值类进行与字符串完全相同的实验(尽管在另一台机器上);atest:0.009,btest:o.356。 这基本上与处理句柄的区别相同,因此我认为跟踪引用并不是关键答案。它也无法解释函数与包中函数之间的开销差异。 - stijn
你使用的Matlab版本是哪个? - MikeEL
1
我已经运行了一些类似于句柄和值类之间的比较,并没有注意到两者之间的性能差异。 - RjOllos
我也不再注意到任何差异了。 - MikeEL
在Matlab中,所有的数组都是引用计数的,因为它们使用写时复制和共享底层原始数据,而不仅仅是句柄对象。 - Andrew Janke

2

OO性能与所使用的MATLAB版本密切相关。我无法评论所有版本,但根据经验得知,2012a版本相比于2010版本有很大改进。没有基准测试数据可以提供。我的代码是专门使用句柄类编写的,并且是在2012a版本下编写的,在早期版本下根本无法运行。


1

实际上,你的代码没有问题,但是Matlab存在问题。我认为这是一种看起来很玩的方式。编译类代码只是额外的开销。 我已经用简单的类点测试过(一次作为句柄,一次作为值类)

    classdef Pointh < handle
    properties
       X
       Y
    end  
    methods        
        function p = Pointh (x,y)
            p.X = x;
            p.Y = y;
        end        
        function  d = dist(p,p1)
            d = (p.X - p1.X)^2 + (p.Y - p1.Y)^2 ;
        end

    end
end

这是一个测试。
%handle points 
ph = Pointh(1,2);
ph1 = Pointh(2,3);

%values  points 
p = Pointh(1,2);
p1 = Pointh(2,3);

% vector points
pa1 = [1 2 ];
pa2 = [2 3 ];

%Structur points 
Ps.X = 1;
Ps.Y = 2;
ps1.X = 2;
ps1.Y = 3;

N = 1000000;

tic
for i =1:N
    ph.dist(ph1);
end
t1 = toc

tic
for i =1:N
    p.dist(p1);
end
t2 = toc

tic
for i =1:N
    norm(pa1-pa2)^2;
end
t3 = toc

tic
for i =1:N
    (Ps.X-ps1.X)^2+(Ps.Y-ps1.Y)^2;
end
t4 = toc

结果

t1 =

12.0212% 处理

t2 =

12.0042% 值

t3 =

0.5489  % vector

t4 =

0.0707 % structure 

因此,为了有效的性能,避免使用面向对象编程,而是使用结构体来组织变量是一个不错的选择。

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接