Является ли MATLAB OOP медленным, или я делаю что-то неправильно?

Я экспериментирую с MATLAB OOP, в качестве начала я подражал моим классам С++ Logger, и я помещаю все свои вспомогательные функции строки в класс String, полагая, что было бы здорово сделать такие вещи, как a + b, a == b, a.find( b ) из strcat( a b ), strcmp( a, b ), получить первый элемент strfind( a, b ) и т.д.

Проблема: замедление

Я поставил вышеприведенные вещи и сразу заметил резкое замедление. Я делаю это неправильно (что, безусловно, возможно, поскольку у меня довольно ограниченный опыт MATLAB), или MATLAB OOP просто представляет много накладных расходов?

Мой тестовый пример

Вот простой тест, который я сделал для строки, в основном просто добавляя строку и снова удаляя добавленную часть:

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

Результаты для системы регистрации также равны: 0,1 секунды для 1000 вызовов до frpintf( 1, 'test\n' ), 7 (!) секунд для 1000 звонков в мою систему при внутреннем использовании класса String (ОК, в нем есть намного больше логики, но для сравнения с С++: накладные расходы моей системы, которая использует std::string( "blah" ) и std::cout на выходной стороне vs plain std::cout << "blah" составляет порядка 1 миллисекунды.)

Это просто накладные расходы при поиске функций класса/пакета?

Поскольку MATLAB интерпретируется, он должен искать определение функции/объекта во время выполнения. Поэтому мне было интересно, что, возможно, гораздо больше накладных расходов связано с поиском функций класса или пакета с функциями, находящимися на пути. Я попытался проверить это, и он просто становится незнакомцем. Чтобы исключить влияние классов/объектов, я сравнил вызов функции в пути vs функции в пакете:

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 сек, 0,001 сек в ctest

btest 0,060 с, 0,014 с в util.ctest

Итак, все ли это накладные расходы, исходящие только из времени MATLAB, поиск определений для реализации OOP, тогда как эти служебные данные не существуют для функций, которые находятся непосредственно на пути?

Ответ 1

Я работал с OO MATLAB некоторое время, и в итоге посмотрел на похожие проблемы с производительностью.

Короткий ответ: да, MATLAB OOP довольно медленный. Существуют значительные накладные расходы на вызовы методов, которые выше, чем у основных языков ОО, и вы мало что можете с этим поделать. Частично причина может заключаться в том, что идиоматический MATLAB использует "векторизованный" код для уменьшения количества вызовов методов, и издержки на вызов не являются высоким приоритетом.

Я оценил производительность, написав бесполезные "nop" функции как различные типы функций и методов. Вот некоторые типичные результаты.

>> call_nops
Computer: PCWIN   Release: 2009b
Calling each function/method 100000 times
nop() function:                 0.02261 sec   0.23 usec per call
nop1-5() functions:             0.02182 sec   0.22 usec per call
nop() subfunction:              0.02244 sec   0.22 usec per call
@()[] anonymous function:       0.08461 sec   0.85 usec per call
nop(obj) method:                0.24664 sec   2.47 usec per call
nop1-5(obj) methods:            0.23469 sec   2.35 usec per call
nop() private function:         0.02197 sec   0.22 usec per call
classdef nop(obj):              0.90547 sec   9.05 usec per call
classdef obj.nop():             1.75522 sec  17.55 usec per call
classdef private_nop(obj):      0.84738 sec   8.47 usec per call
classdef nop(obj) (m-file):     0.90560 sec   9.06 usec per call
classdef class.staticnop():     1.16361 sec  11.64 usec per call
Java nop():                     2.43035 sec  24.30 usec per call
Java static_nop():              0.87682 sec   8.77 usec per call
Java nop() from Java:           0.00014 sec   0.00 usec per call
MEX mexnop():                   0.11409 sec   1.14 usec per call
C nop():                        0.00001 sec   0.00 usec per call

Аналогичные результаты на R2008a через R2009b. Это на Windows XP x64 под управлением 32-битной MATLAB.

"Java nop()" - это неиспользуемый Java-метод, вызываемый из цикла M-кода, и включает в себя служебные данные диспетчеризации MATLAB-to-Java при каждом вызове. "Java nop() из Java" - это то же самое, что вызывается в цикле Java for(), и оно не влечет за собой такое ограничение границ. Возьмите тайминги Java и C с небольшим количеством соли; умный компилятор может полностью оптимизировать вызовы.

Механизм определения объема пакета является новым и представлен примерно в то же время, что и классы classdef. Его поведение может быть связано.

Несколько предварительных выводов:

  • Методы медленнее, чем функции.
  • Методы нового стиля (classdef) работают медленнее, чем методы старого стиля.
  • Новый синтаксис obj.nop() медленнее, чем синтаксис nop(obj), даже для того же метода объекта classdef. То же самое для объектов Java (не показаны). Если вы хотите идти быстро, позвоните nop(obj).
  • Затраты на вызов метода выше (примерно в 2 раза) в 64-битной среде MATLAB в Windows. (Не показано.)
  • Отправка метода MATLAB происходит медленнее, чем в некоторых других языках.

Сказать, почему это так, было бы спекуляцией с моей стороны. Внутренние компоненты двигателя MATLAB не являются публичными. Это не интерпретируемая или скомпилированная проблема как таковая - MATLAB имеет JIT - но более слабая типизация и синтаксис MATLAB могут означать больше работы во время выполнения. (Например, из одного синтаксиса нельзя определить, является ли "f (x)" вызовом функции или индексом в массиве; это зависит от состояния рабочего пространства во время выполнения.) Это может быть из-за того, что определения класса MATLAB связаны к состоянию файловой системы так, как это делают многие другие языки.

Так что делать?

Идиоматический подход MATLAB к этому состоит в том, чтобы "векторизовать" ваш код, структурируя определения классов таким образом, чтобы экземпляр объекта обернул массив; то есть каждое из его полей содержит параллельные массивы (называемые "планарной" организацией в документации MATLAB). Вместо того, чтобы иметь массив объектов, каждый с полями, содержащими скалярные значения, они определяют объекты, которые сами являются массивами, и имеют методы, которые принимают массивы в качестве входных данных и выполняют векторизованные вызовы полей и входных данных. Это уменьшает количество выполненных вызовов методов, надеюсь, что затраты на диспетчеризацию не являются узким местом.

Подражание классу C++ или Java в MATLAB, вероятно, не будет оптимальным. Классы Java/C++ обычно строятся так, что объекты являются наименьшими строительными блоками, насколько это возможно (то есть, множество различных классов), и вы объединяете их в массивы, объекты коллекций и т.д. И перебираете их с помощью циклы. Чтобы быстро создавать классы MATLAB, выверните этот подход наизнанку. Имейте большие классы, чьи поля являются массивами, и вызывайте векторизованные методы для этих массивов.

Смысл в том, чтобы ваш код соответствовал сильным сторонам языка - обработке массивов, векторизованной математике - и избегал слабых мест.

РЕДАКТИРОВАТЬ: начиная с оригинального поста, R2010b и R2011a вышли. Общая картина та же: вызовы MCOS становятся немного быстрее, а вызовы методов Java и старого стиля - медленнее.

РЕДАКТИРОВАТЬ: Раньше у меня были некоторые заметки о "чувствительности пути" с дополнительной таблицей времени вызовов функций, где время функций зависело от того, как был настроен путь Matlab, но, похоже, это было отклонением от моей конкретной настройки сети в время. Приведенная выше таблица отражает время, типичное для большинства моих тестов с течением времени.

Обновление: R2011b

РЕДАКТИРОВАТЬ (13/02/2012): R2011b отсутствует, и картина производительности изменилась достаточно, чтобы обновить это.

Arch: PCWIN   Release: 2011b 
Machine: R2011b, Windows XP, 8x Core i7-2600 @ 3.40GHz, 3 GB RAM, NVIDIA NVS 300
Doing each operation 100000 times
style                           total       µsec per call
nop() function:                 0.01578      0.16
nop(), 10x loop unroll:         0.01477      0.15
nop(), 100x loop unroll:        0.01518      0.15
nop() subfunction:              0.01559      0.16
@()[] anonymous function:       0.06400      0.64
nop(obj) method:                0.28482      2.85
nop() private function:         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 constant:              1.51890     15.19
classdef property:              0.12992      1.30
classdef property with getter:  1.39912     13.99
+pkg.nop() function:            0.87345      8.73
+pkg.nop() from inside +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() from Java:       0.00010      0.00
MEX mexnop():                   0.08709      0.87
C nop():                        0.00001      0.00
j() (builtin):                  0.00251      0.03

Я думаю, что результатом этого является то, что:

  • Методы MCOS/classdef работают быстрее. Стоимость теперь примерно такая же, как у классов старого стиля, если вы используете синтаксис foo(obj). Таким образом, в большинстве случаев скорость метода больше не является причиной для использования классов старого стиля. (Слава, MathWorks!)
  • Помещение функций в пространства имен делает их медленными. (Не новый в R2011b, просто новый в моем тесте.)

Обновление: R2014a

Я восстановил код тестирования и запустил его на R2014a.

Matlab R2014a on PCWIN64  
Matlab 8.3.0.532 (R2014a) / Java 1.7.0_11 on PCWIN64 Windows 7 6.1 (eilonwy-win7) 
Machine: Core i7-3615QM CPU @ 2.30GHz, 4 GB RAM (VMware Virtual Platform)
nIters = 100000 

Operation                        Time (µsec)  
nop() function:                         0.14 
nop() subfunction:                      0.14 
@()[] anonymous function:               0.69 
nop(obj) method:                        3.28 
nop() private fcn on @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 constant:                      4.18 
classdef property:                      1.18 
classdef property with getter:         19.26 
+pkg.nop() function:                    4.03 
+pkg.nop() from inside +pkg:            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() from Java:               0.01 
MEX mexnop():                           0.91 
builtin j():                            0.02 
struct s.foo field access:              0.14 
isempty(persistent):                    0.00 

Обновление: R2015b: Объекты стали быстрее!

Вот результаты R2015b, любезно предоставленные @Shaked. Это большое изменение: ООП значительно быстрее, и теперь синтаксис obj.method() такой же быстрый, как method(obj), и намного быстрее, чем унаследованные объекты ООП.

Matlab R2015b on PCWIN64  
Matlab 8.6.0.267246 (R2015b) / Java 1.7.0_60 on PCWIN64 Windows 8 6.2 (nanit-shaked) 
Machine: Core i7-4720HQ CPU @ 2.60GHz, 16 GB RAM (20378)
nIters = 100000 

Operation                        Time (µsec)  
nop() function:                         0.04 
nop() subfunction:                      0.08 
@()[] anonymous function:               1.83 
nop(obj) method:                        3.15 
nop() private fcn on @class:            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 constant:                      0.25 
classdef property:                      0.25 
classdef property with getter:          0.64 
+pkg.nop() function:                    0.04 
+pkg.nop() from inside +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 obj.nop() from Java:               0.01 
MEX mexnop():                           0.33 
builtin j():                            0.15 
struct s.foo field access:              0.25 
isempty(persistent):                    0.13 

Обновление: R2018a

Вот результаты R2018a. Это не тот огромный скачок, который мы увидели, когда новый двигатель исполнения был представлен в R2015b, но это все же заметное улучшение по сравнению с прошлым годом. Примечательно, что дескрипторы анонимных функций стали намного быстрее.

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

Operation                        Time (µsec)  
nop() function:                         0.03 
nop() subfunction:                      0.04 
@()[] anonymous function:               0.16 
classdef nop(obj):                      0.16 
classdef obj.nop():                     0.17 
classdef pivate_nop(obj):               0.16 
classdef class.static_nop():            0.03 
classdef constant:                      0.16 
classdef property:                      0.13 
classdef property with getter:          0.39 
+pkg.nop() function:                    0.02 
+pkg.nop() from inside +pkg:            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 obj.nop() from Java:               0.03 
MEX mexnop():                           3.54 
builtin j():                            0.10 
struct s.foo field access:              0.16 
isempty(persistent):                    0.07 

Обновление: R2018b и R2019a: без изменений

Никаких существенных изменений. Я не потрудился включить результаты теста.

Исходный код для тестов

Я поместил исходный код для этих тестов в GitHub, выпущенный под лицензией MIT. https://github.com/apjanke/matlab-bench

Ответ 2

Класс handle имеет дополнительные накладные расходы от отслеживания всех ссылок на себя для целей очистки.

Попробуйте тот же эксперимент, не используя класс дескриптора, и посмотрите, какие у вас результаты.

Ответ 3

Производительность OO существенно зависит от используемой версии MATLAB. Я не могу комментировать все версии, но по опыту знаю, что 2012a значительно улучшилось в версиях 2010 года. Нет контрольных показателей, и поэтому нет цифр для представления. Мой код, написанный исключительно с использованием классов дескрипторов и написанный под 2012a, не будет запускаться вообще в более ранних версиях.

Ответ 4

На самом деле никаких проблем с вашим кодом, но это проблема с 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, а структура - хороший выбор для группировки переменных