Czy MATLAB OOP jest wolny, czy robię coś źle?

144

Ja eksperymentuje z MATLAB OOP , jako początek I naśladował moje C ++ 's klasy Logger i Kładę wszystkie moje ciąg funkcji pomocniczych w klasy String, myśląc, że byłoby wspaniale móc robić takie rzeczy jak a + b, a == b, a.find( b )zamiast strcat( a b ), strcmp( a, b ), pobrać pierwszy element strfind( a, b )itp.

Problem: spowolnienie

Skorzystałem z powyższych rzeczy i od razu zauważyłem drastyczne spowolnienie. Czy robię to źle (co jest z pewnością możliwe, ponieważ mam raczej ograniczone doświadczenie w MATLAB-ie), czy też OOP MATLABa wprowadza po prostu dużo narzutów?

Mój przypadek testowy

Oto prosty test, który przeprowadziłem dla ciągu znaków, po prostu dodając ciąg i ponownie usuwając dołączoną część:

Uwaga: nie pisz takiej klasy String w prawdziwym kodzie! Matlab ma teraz natywny stringtyp tablicy i powinieneś go zamiast tego użyć.

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' );

Wyniki

Całkowity czas w sekundach dla 1000 iteracji:

btest 0.550 (z String.SetLength 0,138, String.plus 0,065, String.Length 0,057)

co najmniej 0,015

Wyniki dla systemu loggera są podobne: 0,1 sekundy na 1000 wywołań do frpintf( 1, 'test\n' ), 7 (!) Sekund na 1000 wywołań mojego systemu przy użyciu klasy String wewnętrznie (OK, ma dużo więcej logiki, ale w porównaniu z C ++: narzut mojego systemu, który używa std::string( "blah" )i std::coutpo stronie wyjściowej w porównaniu do zwykłego, std::cout << "blah"jest rzędu 1 milisekundy.)

Czy to tylko narzut podczas wyszukiwania funkcji klas / pakietów?

Ponieważ MATLAB jest interpretowany, musi sprawdzić definicję funkcji / obiektu w czasie wykonywania. Zastanawiałem się więc, że być może znacznie więcej narzutów wiąże się z wyszukiwaniem klas lub funkcji pakietu w porównaniu z funkcjami, które są na ścieżce. Próbowałem to przetestować i stało się dziwniejsze. Aby wykluczyć wpływ klas / obiektów, porównałem wywołanie funkcji w ścieżce z funkcją w pakiecie:

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

Wyniki zebrane w taki sam sposób jak powyżej:

atest 0,004 sek., 0,001 sek. w ctest

btest 0,060 sek., 0,014 sek. w użytkowaniu .ctest

Czy więc cały ten narzut pochodzi tylko z MATLAB-a spędzającego czas na szukaniu definicji dla jego implementacji OOP, podczas gdy ten narzut nie dotyczy funkcji, które są bezpośrednio na ścieżce?

stijn
źródło
5
Dziękuję za to pytanie! Wydajność sterty Matlab (OOP / zamknięcia) niepokoi mnie od lat, patrz stackoverflow.com/questions/1446281/matlabs-garbage-collector . Jestem naprawdę ciekawy, co MatlabDoug / Loren / MikeKatz odpowie na Twój post.
Michaił
1
^ to była ciekawa lektura.
stijn
1
@MatlabDoug: może twój kolega Mike Karr może skomentować OP?
Michaił
4
Czytelnicy powinni również sprawdzić ten ostatni post na blogu (autorstwa Dave'a Foti) omawiający wydajność OOP w najnowszej wersji R2012a: Rozważanie wydajności w
Amro
1
Prosty przykład wrażliwości na strukturę kodu, w którym wywołanie metod podelementów jest wyjęte z pętli. for i = 1:this.get_n_quantities() if(strcmp(id,this.get_quantity_rlz(i).get_id())) ix = i; end endzajmuje 2,2 sekundy, podczas gdy 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 endzajmuje 0,01, dwa zamówienia magazynka
Jose Ospina

Odpowiedzi:

223

Pracuję z OO MATLAB już od jakiegoś czasu i ostatecznie przyjrzałem się podobnym problemom z wydajnością.

Krótka odpowiedź brzmi: tak, OOP MATLABA jest trochę powolne. Istnieje znaczny narzut wywołań metod, wyższy niż w przypadku popularnych języków obiektowych i niewiele można z tym zrobić. Jednym z powodów może być to, że idiomatyczny MATLAB używa kodu „wektoryzowanego” w celu zmniejszenia liczby wywołań metod, a narzut na wywołanie nie ma wysokiego priorytetu.

Testowałem wydajność, pisząc funkcje „nic nie rób” jako różne typy funkcji i metod. Oto kilka typowych wyników.

>> call_nops
Komputer: PCWIN Wydanie: 2009b
Wywołanie każdej funkcji / metody 100000 razy
Funkcja nop (): 0,02261 s 0,23 usek na wywołanie
Funkcje nop1-5 (): 0,02182 s 0,22 usek na wywołanie
Podfunkcja nop (): 0,02244 s 0,22 usek na wywołanie
@ () [] funkcja anonimowa: 0,08461 s 0,85 usek na połączenie
nop (obj): 0,24664 s 2,47 usek na wywołanie
metody nop1-5 (obj): 0,23469 s 2,35 usek na wywołanie
funkcja prywatna nop (): 0,02197 s 0,22 usek na wywołanie
classdef nop (obj): 0,90547 s 9,05 usec na wywołanie
classdef obj.nop (): 1,75522 s 17,55 usek na wywołanie
classdef private_nop (obj): 0,84738 s 8,47 usek na połączenie
classdef nop (obj) (plik m): 0,90560 s 9,06 usek na wywołanie
classdef class.staticnop (): 1,16361 s 11,64 usec na wywołanie
Java nop (): 2,43035 s 24,30 usek na połączenie
Java static_nop (): 0,87682 s 8,77 usec na wywołanie
Java nop () z języka Java: 0,00014 s 0,00 usec na wywołanie
MEX mexnop (): 0,11409 sek. 1,14 usek na połączenie
C nop (): 0,00001 s 0,00 usec na połączenie

Podobne wyniki dla R2008a do R2009b. Dotyczy to systemu Windows XP x64 z 32-bitowym programem MATLAB.

Metoda „Java nop ()” jest metodą Java, która nie robi nic, wywoływana z pętli kodu M i zawiera narzut z MATLAB-do-Java na każde wywołanie. „Java nop () z języka Java” to to samo, co wywoływane w pętli for () języka Java i nie powoduje takiej kary za granicę. Spójrz na czasy Java i C z przymrużeniem oka; sprytny kompilator mógłby całkowicie zoptymalizować wywołania.

Mechanizm określania zakresu pakietów jest nowy, wprowadzony mniej więcej w tym samym czasie co klasy classdef. Jego zachowanie może być powiązane.

Kilka wstępnych wniosków:

  • Metody są wolniejsze niż funkcje.
  • Metody nowego stylu (classdef) są wolniejsze niż metody starego stylu.
  • Nowa obj.nop()składnia jest wolniejsza niż nop(obj)składnia, nawet dla tej samej metody w obiekcie classdef. To samo dotyczy obiektów Java (nie pokazano). Jeśli chcesz jechać szybko, zadzwoń nop(obj).
  • Narzut wywołania metody jest wyższy (około 2x) w 64-bitowej wersji MATLAB w systemie Windows. (Nie pokazany.)
  • Wysyłanie metody MATLAB jest wolniejsze niż w przypadku niektórych innych języków.

Mówienie, dlaczego tak jest, byłoby z mojej strony tylko spekulacją. Wewnętrzne elementy OO silnika MATLAB nie są publiczne. Nie jest to problem interpretowany i skompilowany per se - MATLAB ma JIT - ale luźniejsze pisanie i składnia MATLAB-a może oznaczać więcej pracy w czasie wykonywania. (Np. Nie można stwierdzić na podstawie samej składni, czy „f (x)” jest wywołaniem funkcji czy indeksem tablicy; zależy to od stanu obszaru roboczego w czasie wykonywania). Może to być spowodowane tym, że definicje klas MATLAB-a są powiązane do stanu systemu plików w sposób, w jaki nie ma wielu innych języków.

Więc co robić?

Idiomatycznym podejściem MATLAB do tego jest „wektoryzacja” kodu poprzez strukturyzację definicji klas w taki sposób, że instancja obiektu opakowuje tablicę; to znaczy, że każde z jego pól zawiera tablice równoległe (zwane organizacją „planarną” w dokumentacji MATLAB). Zamiast mieć tablicę obiektów, z których każdy ma pola przechowujące wartości skalarne, zdefiniuj obiekty, które same są tablicami, i niech metody przyjmują tablice jako dane wejściowe i wykonują wektoryzowane wywołania pól i danych wejściowych. Zmniejsza to liczbę wywołań metod, miejmy nadzieję, że narzut wysyłania nie jest wąskim gardłem.

Naśladowanie klasy C ++ lub Java w MATLAB-u prawdopodobnie nie będzie optymalne. Klasy Java / C ++ są zwykle budowane w taki sposób, że obiekty są najmniejszymi blokami konstrukcyjnymi, tak specyficznymi, jak to tylko możliwe (to jest wiele różnych klas), i tworzysz je w tablicach, obiektach kolekcji itp. I iterujesz po nich za pomocą pętli. Aby tworzyć szybkie zajęcia MATLAB, odwróć to podejście na lewą stronę. Miej większe klasy, których pola są tablicami, i wywołuj metody wektoryzowane na tych tablicach.

Chodzi o to, aby zaaranżować kod tak, aby wykorzystywał mocne strony języka - obsługę tablic, matematykę wektorową - i unikać słabych punktów.

EDYCJA: Od czasu oryginalnego postu pojawiły się R2010b i R2011a. Ogólny obraz jest taki sam, przy czym wywołania MCOS stają się nieco szybsze, a wywołania Java i metody starego typu stają się wolniejsze .

EDYCJA: Kiedyś miałem tutaj kilka uwag na temat „czułości ścieżki” z dodatkową tabelą czasów wywołań funkcji, gdzie na czasy funkcji wpływał sposób konfiguracji ścieżki Matlab, ale wydaje się, że była to aberracja mojej konkretnej konfiguracji sieci w czas. Powyższy wykres odzwierciedla czasy typowe dla przeważania moich testów w czasie.

Aktualizacja: R2011b

EDYCJA (13.02.2012): R2011b jest niedostępny, a obraz wydajności zmienił się na tyle, aby to zaktualizować.

Arch: PCWIN Wydanie: 2011b 
Maszyna: R2011b, Windows XP, 8x Core i7-2600 @ 3,40 GHz, 3 GB RAM, NVIDIA NVS 300
Wykonywanie każdej operacji 100000 razy
styl łącznie w µs na połączenie
Funkcja nop (): 0,01578 0,16
nop (), 10x rozwijanie pętli: 0,01477 0,15
nop (), rozwinięcie pętli 100x: 0,01518 0,15
Podfunkcja nop (): 0,01559 0,16
@ () [] funkcja anonimowa: 0,06400 0,64
nop (obj), metoda: 0,28482 2,85
funkcja prywatna 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
stała classdef: 1,51890 15,19
właściwość classdef: 0,12992 1,30
Właściwość classdef z funkcją getter: 1.39912 13.99
Funkcja + pkg.nop (): 0,87345 8,73
+ pkg.nop () od wewnątrz + 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 () z Java: 0,00010 0,00
MEX mexnop (): 0,08709 0,87
C nop (): 0,00001 0,00
j () (wbudowany): 0,00251 0,03

Myślę, że rezultatem tego jest to, że:

  • Metody MCOS / classdef są szybsze. Koszt jest teraz porównywalny z klasami w starym stylu, o ile używasz foo(obj)składni. Dlatego w większości przypadków szybkość metody nie jest już powodem do trzymania się klas starego stylu. (Uznanie, MathWorks!)
  • Umieszczanie funkcji w przestrzeniach nazw powoduje ich spowolnienie. (Nie nowy w R2011b, tylko nowy w moim teście).

Aktualizacja: R2014a

Zrekonstruowałem kod testowy i uruchomiłem go na R2014a.

Matlab R2014a na PCWIN64  
Matlab 8.3.0.532 (R2014a) / Java 1.7.0_11 na PCWIN64 Windows 7 6.1 (eilonwy-win7) 
Maszyna: procesor Core i7-3615QM @ 2,30 GHz, 4 GB pamięci RAM (platforma wirtualna VMware)
nIters = 100000 

Czas pracy (µs)  
Funkcja nop (): 0,14 
Podfunkcja nop (): 0.14 
@ () [] funkcja anonimowa: 0,69 
nop (obj), metoda: 3.28 
nop () prywatny fcn na @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 
stała classdef: 4.18 
właściwość classdef: 1.18 
Właściwość classdef z funkcją getter: 19.26 
+ funkcja pkg.nop (): 4.03 
+ pkg.nop () od wewnątrz + pkg: 4.16 
feval ('nop'): 2,31 
feval (@nop): 0,22 
eval ('nop'): 59,46 
Java obj.nop (): 26.07.2016 
Java nop (obj): 3.72 
Java feval ('nop', obj): 9.25 
Java Klass.staticNop (): 10.54 
Java obj.nop () z Java: 0.01 
MEX mexnop (): 0,91 
wbudowany j (): 0,02 
Dostęp do pola struct s.foo: 0.14 
isempty (trwałe): 0,00 

Aktualizacja: R2015b: obiekty stały się szybsze!

Oto wyniki R2015b, udostępnione przez @Shaked. To duża zmiana: OOP jest znacznie szybsze, a teraz obj.method()składnia jest równie szybka method(obj)i znacznie szybsza niż starsze obiekty OOP.

Matlab R2015b na PCWIN64  
Matlab 8.6.0.267246 (R2015b) / Java 1.7.0_60 na PCWIN64 Windows 8 6.2 (wstrząśnięty nanit) 
Maszyna: procesor Core i7-4720HQ @ 2,60 GHz, 16 GB RAM (20378)
nIters = 100000 

Czas pracy (µs)  
Funkcja nop (): 0,04 
Podfunkcja nop (): 0,08 
@ () [] funkcja anonimowa: 1,83 
nop (obj), metoda: 3.15 
nop () prywatny fcn na @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 stała: 0,25 
właściwość classdef: 0,25 
Właściwość classdef z funkcją pobierającą: 0.64 
Funkcja + pkg.nop (): 0,04 
+ pkg.nop () od wewnątrz + 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 () z Java: 0.01 
MEX mexnop (): 0,33 
wbudowany j (): 0,15 
Dostęp do pola struct s.foo: 0.25 
isempty (trwałe): 0,13 

Aktualizacja: R2018a

Oto wyniki R2018a. Nie jest to ogromny skok, który widzieliśmy, gdy nowy silnik wykonawczy został wprowadzony w R2015b, ale nadal jest to znaczna poprawa z roku na rok. Warto zauważyć, że anonimowe uchwyty funkcji stały się znacznie szybsze.

Matlab R2018a na MACI64  
Matlab 9.4.0.813654 (R2018a) / Java 1.8.0_144 na MACI64 Mac OS X 10.13.5 (eilonwy) 
Maszyna: procesor Core i7-3615QM @ 2,30 GHz, 16 GB pamięci RAM 
nIters = 100000 

Czas pracy (µs)  
Funkcja nop (): 0,03 
Podfunkcja nop (): 0,04 
@ () [] funkcja anonimowa: 0,16 
classdef nop (obj): 0.16 
classdef obj.nop (): 0.17 
classdef pivate_nop (obj): 0.16 
classdef class.static_nop (): 0,03 
stała classdef: 0,16 
właściwość classdef: 0.13 
Właściwość classdef z funkcją getter: 0,39 
+ Funkcja pkg.nop (): 0,02 
+ pkg.nop () od wewnątrz + 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.0 
Java feval ('nop', obj): 21,85 
Java Klass.staticNop (): 45.49 
Java obj.nop () z Java: 0.03 
MEX mexnop (): 3.54 
wbudowany j (): 0,10 
dostęp do pola struct s.foo: 0.16 
isempty (trwałe): 0,07 

Aktualizacja: R2018b i R2019a: bez zmian

Brak znaczących zmian. Nie kłopoczę się dołączaniem wyników testu.

Kod źródłowy dla testów porównawczych

Umieściłem kod źródłowy tych testów na GitHub, wydanym na licencji MIT. https://github.com/apjanke/matlab-bench

Andrew Janke
źródło
5
@AndrewJanke Czy myślisz, że mógłbyś ponownie przeprowadzić test porównawczy z R2012a? To jest naprawdę interesujące.
Dang Khoa
7
Cześć ludzie. Jeśli nadal interesuje Cię kod źródłowy, zrekonstruowałem go i otworzyłem na GitHubie. github.com/apjanke/matlab-bench
Andrew Janke
2
@Seeda: metody statyczne są wymienione w tych wynikach jako „classdef class.static_nop ()”. Są dość powolne w porównaniu z funkcjami. Jeśli nie dzwonią często, to nie ma znaczenia.
Andrew Janke
2
@AndrewJanke Oto jest: gist.github.com/ShakedDovrat/62db9e8f6883c5e28fc0
Shaked
2
Łał! Jeśli te wyniki się utrzymają, być może będę musiał zrewidować całą odpowiedź. Dodany. Dzięki!
Andrew Janke
3

Klasa handle ma dodatkowe obciążenie związane ze śledzeniem wszystkich odwołań do samej siebie w celu czyszczenia.

Spróbuj tego samego eksperymentu bez użycia klasy handle i zobacz, jakie są wyniki.

MikeEL
źródło
1
dokładnie ten sam eksperyment z String, ale teraz jako klasa wartości (chociaż na innym komputerze); atest: 0,009, btest: o.356. Zasadniczo jest to ta sama różnica, co w przypadku uchwytu, więc nie sądzę, aby śledzenie referencji było kluczową odpowiedzią. Nie wyjaśnia również narzutu funkcji w porównaniu z funkcją w pakietach.
stijn
Jakiej wersji Matlab używasz?
MikeEL
1
Przeprowadziłem kilka podobnych porównań między klasami uchwytów i wartości i nie zauważyłem różnicy w wydajności między nimi.
RjOllos
Ja też nie zauważam różnicy.
MikeEL
Ma to sens: w Matlabie wszystkie tablice, nie tylko obiekty obsługujące, są liczone jako referencje, ponieważ używają kopiowania przy zapisie i współdzielenia podstawowych danych surowych.
Andrew Janke
1

Wydajność OO zależy w dużym stopniu od używanej wersji MATLAB-a. Nie mogę komentować wszystkich wersji, ale z doświadczenia wiem, że 2012a jest znacznie ulepszona w stosunku do wersji 2010. Brak testów porównawczych, a więc żadnych liczb do przedstawienia. Mój kod, napisany wyłącznie przy użyciu klas obsługi i napisany pod 2012a, w ogóle nie będzie działał we wcześniejszych wersjach.

HG Bruce
źródło
1

Właściwie nie ma problemu z Twoim kodem, ale jest to problem z Matlabem. Myślę, że jest to rodzaj zabawy, aby wyglądać. Kompilacja kodu klasy to nic innego jak obciążenie. Zrobiłem test z prostym punktem klasy (raz jako uchwyt) i drugim (raz jako klasa wartości)

    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

oto test

%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

Wyniki t1 =

12.0212% Uchwyt

t2 =

12,0042% wartości

t3 =

0.5489  % vector

t4 =

0.0707 % structure 

Dlatego w celu zapewnienia wydajnej wydajności należy unikać używania OOP, zamiast tego struktura jest dobrym wyborem do grupowania zmiennych

Ahmad
źródło