149

我正在尝试使用MATLAB OOP,作为开始,我模仿了 C++ 的 Logger 类,并将所有字符串辅助函数放在 String 类中,我认为能够执行类似a + b, a == b,a.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)

测试 0.015

记录器系统的结果同样是:frpintf( 1, 'test\n' )在内部使用 String 类时,对 1000 次调用需要 0.1 秒,对我的系统进行 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

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

在 ctest 中测试 0.004 秒,0.001 秒

btest 0.060 秒,util.ctest 中的 0.014 秒

那么,所有这些开销是否仅来自 MATLAB 花时间查找其 OOP 实现的定义,而直接在路径中的函数则不存在这种开销?

4

4 回答 4

233

我使用 OO MATLAB 已经有一段时间了,最​​终看到了类似的性能问题。

简短的回答是:是的,MATLAB 的 OOP 有点慢。方法调用开销很大,高于主流的 OO 语言,而且您无能为力。部分原因可能是惯用的 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 文件):每次调用 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 中的 Java nop():每次调用 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()”是从 M 代码循环中调用的无操作 Java 方法,并且每次调用都包含 MATLAB 到 Java 的调度开销。“Java nop() from Java”与 Java for() 循环中调用的内容相同,并且不会导致边界损失。对 Java 和 C 的时间持保留态度;一个聪明的编译器可以完全优化调用。

包作用域机制是新的,与 classdef 类几乎同时引入。它的行为可能是相关的。

一些初步的结论:

  • 方法比函数慢。
  • 新样式(classdef)方法比旧样式方法慢。
  • obj.nop()语法比语法慢nop(obj),即使对于 classdef 对象上的相同方法也是如此。Java 对象也一样(未显示)。如果你想走得快,打电话nop(obj)
  • 在 Windows 上的 64 位 MATLAB 中,方法调用开销更高(大约 2 倍)。(未显示。)
  • MATLAB 方法分派比其他一些语言慢。

说为什么会这样只是我的猜测。MATLAB 引擎的 OO 内部不是公开的。这本身不是解释与编译的问题——MATLAB 有一个 JIT——但 MATLAB 更宽松的类型和语法可能意味着在运行时需要更多的工作。(例如,您无法仅从语法中判断“f(x)”是函数调用还是数组的索引;这取决于运行时工作区的状态。)这可能是因为 MATLAB 的类定义是绑定的以许多其他语言所没有的方式来处理文件系统状态。

那么该怎么办?

一种惯用的 MATLAB 方法是通过构造类定义来“矢量化”您的代码,以便对象实例包装一个数组;也就是说,它的每个字段都包含并行数组(在 MATLAB 文档中称为“平面”组织)。与其拥有一个对象数组,每个对象都有保存标量值的字段,不如定义本身是数组的对象,并让方法将数组作为输入,并对字段和输入进行向量化调用。这减少了方法调用的数量,希望足以使调度开销不是瓶颈。

在 MATLAB 中模仿 C++ 或 Java 类可能不是最优的。Java/C++ 类的构建通常使对象成为最小的构建块,尽可能具体(即,许多不同的类),并且将它们组合成数组、集合对象等,并使用循环对其进行迭代。要制作快速的 MATLAB 类,请彻底改变这种方法。拥有更大的类,其字段是数组,并在这些数组上调用矢量化方法。

重点是安排您的代码以发挥语言的优势——数组处理、矢量化数学——并避免弱点。

编辑:自原始帖子以来,R2010b 和 R2011a 已经问世。总体情况是一样的,MCOS 调用变得更快,而 Java 和旧式方法调用变得更慢

编辑:我曾经在这里有一些关于“路径敏感性”的注释,还有一个额外的函数调用时间表,其中函数时间受到 Matlab 路径配置方式的影响,但这似乎是我在特定网络设置的异常时间。上面的图表反映了随着时间的推移,我的测试占优势的典型时间。

更新:R2011b

编辑(2012 年 2 月 13 日):R2011b 已经发布,性能图片已经发生了足够的变化来更新它。

Arch:PCWIN 发布:2011b
机器:R2011b,Windows XP,8x Core i7-2600 @ 3.40GHz,3 GB RAM,NVIDIA NVS 300
每次操作 100000 次
每次调用的样式总微秒
nop() 函数:0.01578 0.16
nop(),10x 循环展开:0.01477 0.15
nop(),100x 循环展开:0.01518 0.15
nop() 子函数:0.01559 0.16
@()[] 匿名函数:0.06400 0.64
nop(obj) 方法:0.28482 2.85
nop() 私有函数:0.01505 0.15
类定义 nop(obj):0.43323 4.33
类定义 obj.nop():0.81087 8.11
classdef private_nop(obj):0.32272 3.23
类定义类.staticnop():0.88959 8.90
类定义常量:1.51890 15.19
类定义属性:0.12992 1.30
带有 getter 的 classdef 属性: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 的 Java obj.nop():0.00010 0.00
墨西哥 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 虚拟平台)
nIters = 100000

操作时间(微秒)  
nop() 函数:0.14
nop() 子函数:0.14
@()[] 匿名函数:0.69
nop(obj) 方法:3.28
@class 上的 nop() 私有 fcn:0.14
类定义 nop(obj):5.30
类定义 obj.nop():10.78
类定义 pivate_nop(obj): 4.88
类定义类.static_nop():11.81
类定义常量:4.18
类定义属性:1.18
带有 getter 的 classdef 属性:19.26
+pkg.nop() 函数:4.03
+pkg.nop() 从内部 +pkg: 4.16
feval('nop'):2.31
feval(@nop):0.22
评估('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 的 Java obj.nop():0.01
墨西哥 mexnop(): 0.91
内置 j(): 0.02
struct s.foo 字段访问:0.14
isempty(持久):0.00

更新:R2015b:对象变得更快!

这是由@Shaked 提供的 R2015b 结果。这是一个很大的变化:OOP 明显更快,现在obj.method()语法与 . 一样快method(obj),并且比传统的 OOP 对象快得多。

PCWIN64 上的 Matlab R2015b  
Matlab 8.6.0.267246 (R2015b) / Java 1.7.0_60 on 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
@class 上的 nop() 私有 fcn:0.04
类定义 nop(obj):0.28
类定义 obj.nop(): 0.31
类定义 pivate_nop(obj): 0.34
classdef class.static_nop(): 0.05
类定义常量:0.25
类定义属性:0.25
带有 getter 的 classdef 属性:0.64
+pkg.nop() 函数:0.04
+pkg.nop() 从内部 +pkg: 0.04
feval('nop'):8.26
feval(@nop):0.63
评估('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
墨西哥 mexnop(): 0.33
内置 j(): 0.15
struct s.foo 字段访问:0.25
isempty(持久):0.13

更新:R2018a

这是 R2018a 结果。这不是我们在 R2015b 中引入新执行引擎时看到的巨大飞跃,但仍是可观的逐年改进。值得注意的是,匿名函数句柄变得更快了。

MACI64 上的 Matlab R2018a  
MACI64 Mac OS X 10.13.5 (eilonwy) 上的 Matlab 9.4.0.813654 (R2018a) / Java 1.8.0_144
机器:Core i7-3615QM CPU @ 2.30GHz,16 GB RAM
nIters = 100000

操作时间(微秒)  
nop() 函数:0.03
nop() 子函数:0.04
@()[] 匿名函数:0.16
类定义 nop(obj):0.16
类定义 obj.nop():0.17
类定义 pivate_nop(obj): 0.16
classdef class.static_nop(): 0.03
类定义常量:0.16
类定义属性:0.13
带有 getter 的 classdef 属性:0.39
+pkg.nop() 函数:0.02
+pkg.nop() 从内部 +pkg: 0.02
feval('nop'):15.62
feval(@nop):0.43
评估('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 obj.nop() 来自 Java:0.03
墨西哥 mexnop(): 3.54
内置 j(): 0.10
struct s.foo 字段访问:0.16
isempty(持久):0.07

更新:R2018b 和 R2019a:没有变化

没有显着变化。我不打扰包括测试结果。

更新:R2021a:更快的物体!

看起来 classdef 对象又变得更快了。但是结构变得更慢了。

MACI64 上的 Matlab R2021a  
MACI64 Mac OS X 10.14.6 (eilonwy) 上的 Matlab 9.10.0.1669831 (R2021a) 更新 2 / Java 1.8.0_202
机器:Core i7-3615QM CPU @ 2.30GHz,4 核,16 GB RAM
nIters = 100000

操作时间(微秒)  
nop() 函数:0.03
nop() 子函数:0.04
@()[] 匿名函数:0.14
nop(obj) 方法:6.65
@class 上的 nop() 私有 fcn:0.02
类定义 nop(obj):0.03
类定义 obj.nop(): 0.04
类定义 pivate_nop(obj): 0.03
classdef class.static_nop(): 0.03
类定义常量:0.16
类定义属性:0.12
带有 getter 的 classdef 属性:0.17
+pkg.nop() 函数:0.02
+pkg.nop() 从内部 +pkg: 0.02
feval('nop'):14.45
feval(@nop):0.59
评估('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
墨西哥 mexnop(): 2.51
内置 j(): 0.21
struct s.foo 字段访问:0.29
isempty(持久):0.26

基准测试源代码

我已将这些基准测试的源代码放在 GitHub 上,根据 MIT 许可证发布。https://github.com/apjanke/matlab-bench

于 2009-11-16T23:58:22.537 回答
4

句柄类有额外的开销,因为它跟踪所有对自身的引用以进行清理。

在不使用句柄类的情况下尝试相同的实验,看看你的结果是什么。

于 2009-11-10T21:57:30.110 回答
2

OO 性能很大程度上取决于所使用的 MATLAB 版本。我不能对所有版本发表评论,但从经验中知道,2012a 比 2010 版本有了很大改进。没有基准,所以没有数字可以呈现。我的代码,专门使用句柄类编写并在 2012a 下编写,在早期版本下根本无法运行。

于 2014-03-09T17:12:30.997 回答
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 

因此,为了提高性能,避免使用 OOP 而不是结构是分组变量的好选择

于 2014-04-09T15:14:37.907 回答