Ist MATLAB OOP langsam oder mache ich etwas falsch?

144

Ich bin mit experimentiert MATLAB OOP , als beginne ich meine C ++ s Klassen Logger 'nachgeahmt und ich bin alle Hilfsfunktionen in einem String - Klasse meine Zeichenfolge setzen, dachte , es wäre toll , der Lage sein , Dinge zu tun , wie a + b, a == b, a.find( b )statt strcat( a b ), strcmp( a, b ), Erstes Element von strfind( a, b )usw. abrufen .

Das Problem: Verlangsamung

Ich nutzte die oben genannten Dinge und bemerkte sofort eine drastische Verlangsamung. Mache ich es falsch (was sicherlich möglich ist, da ich nur begrenzte MATLAB-Erfahrung habe), oder führt MATLABs OOP nur viel Overhead ein?

Mein Testfall

Hier ist der einfache Test, den ich für einen String durchgeführt habe. Im Grunde genommen habe ich nur einen String angehängt und den angehängten Teil wieder entfernt:

Hinweis: Schreiben Sie eine solche String-Klasse nicht in echtem Code! Matlab hat stringjetzt einen nativen Array-Typ, den Sie stattdessen verwenden sollten.

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

Die Ergebnisse

Gesamtzeit in Sekunden für 1000 Iterationen:

btest 0.550 (mit String.SetLength 0.138, String.plus 0.065, String.Length 0.057)

mindestens 0,015

Die Ergebnisse für das Loggersystem sind ebenfalls: 0,1 Sekunden für 1000 Aufrufe an frpintf( 1, 'test\n' ), 7 (!) Sekunden für 1000 Aufrufe an mein System, wenn die String-Klasse intern verwendet wird (OK, sie enthält viel mehr Logik, aber im Vergleich zu C ++: Der Overhead meines Systems, das verwendet std::string( "blah" )und std::coutauf der Ausgabeseite gegenüber der Ebene std::cout << "blah"liegt, liegt in der Größenordnung von 1 Millisekunde.)

Ist es nur Overhead beim Nachschlagen von Klassen- / Paketfunktionen?

Da MATLAB interpretiert wird, muss es zur Laufzeit die Definition einer Funktion / eines Objekts nachschlagen. Ich habe mich also gefragt, ob das Nachschlagen von Klassen- oder Paketfunktionen möglicherweise viel mehr Aufwand erfordert als Funktionen, die sich im Pfad befinden. Ich habe versucht, dies zu testen, und es wird nur seltsamer. Um den Einfluss von Klassen / Objekten auszuschließen, habe ich den Aufruf einer Funktion im Pfad mit einer Funktion in einem Paket verglichen:

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

Ergebnisse, wie oben gesammelt:

mindestens 0,004 s, 0,001 s im ctest

btest 0,060 s, 0,014 s im Gebrauchstest

Kommt all dieser Aufwand nur von MATLAB, der Zeit damit verbringt, Definitionen für seine OOP-Implementierung nachzuschlagen, während dieser Aufwand nicht für Funktionen vorhanden ist, die sich direkt im Pfad befinden?

stijn
quelle
5
Vielen Dank für diese Frage! Die Leistung des Matlab-Heaps (OOP / Closures) hat mich jahrelang beunruhigt, siehe stackoverflow.com/questions/1446281/matlabs-garbage-collector . Ich bin wirklich gespannt, was MatlabDoug / Loren / MikeKatz auf Ihren Beitrag antworten wird.
Mikhail
1
^ das war eine interessante Lektüre.
Stijn
1
@MatlabDoug: Vielleicht kann Ihr Kollege Mike Karr OP kommentieren?
Mikhail
4
Leser sollten auch diesen kürzlich veröffentlichten Blog-Beitrag (von Dave Foti) lesen, in dem die OOP-Leistung in der neuesten R2012a-Version erläutert wird: Berücksichtigung der Leistung in objektorientiertem MATLAB-Code
Amro
1
Ein einfaches Beispiel für die Empfindlichkeit der Codestruktur, bei der der Aufruf von Methoden von Unterelementen aus der Schleife genommen wird. for i = 1:this.get_n_quantities() if(strcmp(id,this.get_quantity_rlz(i).get_id())) ix = i; end enddauert 2,2 Sek., während 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 end0,01 dauert, zwei Bestellungen von Mag
Jose Ospina

Antworten:

223

Ich habe eine Weile mit OO MATLAB gearbeitet und mich am Ende mit ähnlichen Leistungsproblemen befasst.

Die kurze Antwort lautet: Ja, MATLABs OOP ist ziemlich langsam. Es gibt einen erheblichen Aufwand für Methodenaufrufe, der höher ist als bei herkömmlichen OO-Sprachen, und Sie können nicht viel dagegen tun. Ein Grund dafür kann sein, dass das idiomatische MATLAB "vektorisierten" Code verwendet, um die Anzahl der Methodenaufrufe zu reduzieren, und dass der Overhead pro Anruf keine hohe Priorität hat.

Ich habe die Leistung durch Schreiben von "NOP" -Funktionen als verschiedene Arten von Funktionen und Methoden bewertet. Hier sind einige typische Ergebnisse.

>> call_nops
Computer: PCWIN Release: 2009b
Aufruf jeder Funktion / Methode 100000 Mal
nop () Funktion: 0.02261 sec 0.23 usec pro Aufruf
nop1-5 () -Funktionen: 0,02182 Sek. 0,22 usec pro Aufruf
Unterfunktion nop (): 0,02244 Sek. 0,22 usec pro Aufruf
@ () [] anonyme Funktion: 0,08461 Sek. 0,85 usec pro Anruf
nop (obj) -Methode: 0,24664 Sek. 2,47 usec pro Aufruf
nop1-5 (obj) -Methoden: 0,23469 Sek. 2,35 usec pro Aufruf
private Funktion nop (): 0,02197 Sek. 0,22 usec pro Aufruf
classdef nop (obj): 0,90547 Sek. 9,05 usec pro Anruf
classdef obj.nop (): 1.75522 Sek. 17.55 usec pro Aufruf
classdef private_nop (obj): 0,84738 Sek. 8,47 usec pro Aufruf
classdef nop (obj) (m-Datei): 0,90560 Sek. 9,06 usec pro Aufruf
classdef class.staticnop (): 1.16361 Sek. 11.64 usec pro Aufruf
Java nop (): 2.43035 Sek. 24.30 usec pro Aufruf
Java static_nop (): 0,87682 Sek. 8,77 usec pro Aufruf
Java nop () von Java: 0,00014 Sek. 0,00 usec pro Aufruf
MEX mexnop (): 0,11409 Sek. 1,14 usec pro Anruf
C nop (): 0,00001 Sek. 0,00 usec pro Anruf

Ähnliche Ergebnisse für R2008a bis R2009b. Dies ist unter Windows XP x64 mit 32-Bit-MATLAB möglich.

"Java nop ()" ist eine Nichtstun-Java-Methode, die aus einer M-Code-Schleife heraus aufgerufen wird und den Versandaufwand von MATLAB zu Java bei jedem Aufruf enthält. "Java nop () from Java" ist dasselbe, was in einer Java for () - Schleife aufgerufen wird, und verursacht keine Grenzstrafe. Nehmen Sie die Java- und C-Timings mit einem Körnchen Salz; Ein cleverer Compiler könnte die Aufrufe komplett optimieren.

Der Paket-Scoping-Mechanismus ist neu und wird ungefähr zur gleichen Zeit wie die classdef-Klassen eingeführt. Sein Verhalten kann zusammenhängen.

Einige vorläufige Schlussfolgerungen:

  • Methoden sind langsamer als Funktionen.
  • Neue Stilmethoden (classdef) sind langsamer als alte Stilmethoden.
  • Die neue obj.nop()Syntax ist langsamer als die nop(obj)Syntax, selbst für dieselbe Methode für ein classdef-Objekt. Gleiches gilt für Java-Objekte (nicht gezeigt). Wenn Sie schnell gehen möchten, rufen Sie an nop(obj).
  • Der Aufwand für Methodenaufrufe ist in 64-Bit-MATLAB unter Windows höher (ca. 2x). (Nicht gezeigt.)
  • Der Versand der MATLAB-Methode ist langsamer als in einigen anderen Sprachen.

Zu sagen, warum das so ist, wäre nur Spekulation meinerseits. Die OO-Interna der MATLAB-Engine sind nicht öffentlich. Es ist an sich kein interpretiertes oder kompiliertes Problem - MATLAB hat eine JIT -, aber die lockerere Eingabe und Syntax von MATLAB kann zur Laufzeit mehr Arbeit bedeuten. (Zum Beispiel können Sie nicht allein anhand der Syntax erkennen, ob "f (x)" ein Funktionsaufruf oder ein Index in einem Array ist. Dies hängt vom Status des Arbeitsbereichs zur Laufzeit ab.) Dies kann daran liegen, dass die Klassendefinitionen von MATLAB verknüpft sind zum Dateisystemstatus in einer Weise, wie es viele andere Sprachen nicht sind.

Also, was tun?

Ein idiomatischer MATLAB-Ansatz besteht darin, Ihren Code zu "vektorisieren", indem Sie Ihre Klassendefinitionen so strukturieren, dass eine Objektinstanz ein Array umschließt. Das heißt, jedes seiner Felder enthält parallele Arrays (in der MATLAB-Dokumentation als "planare" Organisation bezeichnet). Anstatt ein Array von Objekten mit Feldern zu haben, die skalare Werte enthalten, definieren Sie Objekte, die selbst Arrays sind, und lassen Sie die Methoden Arrays als Eingaben verwenden und vektorisierte Aufrufe für die Felder und Eingaben ausführen. Dies reduziert die Anzahl der durchgeführten Methodenaufrufe, hoffentlich genug, dass der Versandaufwand kein Engpass ist.

Die Nachahmung einer C ++ - oder Java-Klasse in MATLAB ist wahrscheinlich nicht optimal. Java / C ++ - Klassen werden normalerweise so erstellt, dass Objekte die kleinsten Bausteine ​​sind, so spezifisch wie möglich (dh viele verschiedene Klassen), und Sie komponieren sie in Arrays, Sammlungsobjekten usw. und durchlaufen sie mit Schleifen. Um schnelle MATLAB-Klassen zu erstellen, drehen Sie diesen Ansatz um. Haben Sie größere Klassen, deren Felder Arrays sind, und rufen Sie vektorisierte Methoden für diese Arrays auf.

Der Punkt ist, Ihren Code so anzuordnen, dass er die Stärken der Sprache ausspielt - Array-Handhabung, vektorisierte Mathematik - und die Schwachstellen vermeidet.

EDIT: Seit dem ursprünglichen Beitrag sind R2010b und R2011a herausgekommen. Das Gesamtbild ist das gleiche: MCOS-Aufrufe werden etwas schneller und Java- und Methodenaufrufe im alten Stil werden langsamer .

BEARBEITEN: Früher hatte ich hier einige Hinweise zur "Pfadempfindlichkeit" mit einer zusätzlichen Tabelle mit Funktionsaufrufzeiten, in der die Funktionszeiten durch die Konfiguration des Matlab-Pfads beeinflusst wurden. Dies scheint jedoch eine Abweichung von meinem speziellen Netzwerk-Setup bei gewesen zu sein die Zeit. Die obige Tabelle zeigt die Zeiten, die für das Überwiegen meiner Tests im Laufe der Zeit typisch sind.

Update: R2011b

BEARBEITEN (13.02.2012): R2011b ist nicht verfügbar, und das Leistungsbild hat sich ausreichend geändert, um dies zu aktualisieren.

Arch: PCWIN Veröffentlichung: 2011b 
Maschine: R2011b, Windows XP, 8x Core i7-2600 bei 3,40 GHz, 3 GB RAM, NVIDIA NVS 300
Jede Operation 100000 Mal ausführen
Stil insgesamt µsec pro Anruf
nop () -Funktion: 0,01578 0,16
nop (), 10x Loop Unroll: 0.01477 0.15
nop (), 100x Loop Unroll: 0.01518 0.15
Unterfunktion nop (): 0.01559 0.16
@ () [] anonyme Funktion: 0.06400 0.64
nop (obj) -Methode: 0,28482 2,85
nop () private Funktion: 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 Konstante: 1.51890 15.19
classdef-Eigenschaft: 0.12992 1.30
classdef-Eigenschaft mit Getter: 1.39912 13.99
+ pkg.nop () Funktion: 0.87345 8.73
+ pkg.nop () von innen + 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 () von Java: 0,00010 0,00
MEX mexnop (): 0,08709 0,87
C nop (): 0,00001 0,00
j () (eingebaut): 0,00251 0,03

Ich denke, das Ergebnis ist:

  • MCOS / classdef-Methoden sind schneller. Die Kosten sind jetzt ungefähr gleich hoch wie bei alten Stilklassen, solange Sie die foo(obj)Syntax verwenden. Daher ist die Methodengeschwindigkeit in den meisten Fällen kein Grund mehr, sich an alte Stilklassen zu halten. (Kudos, MathWorks!)
  • Das Einfügen von Funktionen in Namespaces verlangsamt diese. (Nicht neu in R2011b, nur neu in meinem Test.)

Update: R2014a

Ich habe den Benchmarking-Code rekonstruiert und auf R2014a ausgeführt.

Matlab R2014a auf PCWIN64  
Matlab 8.3.0.532 (R2014a) / Java 1.7.0_11 unter PCWIN64 Windows 7 6.1 (eilonwy-win7) 
Maschine: Core i7-3615QM CPU bei 2,30 GHz, 4 GB RAM (VMware Virtual Platform)
nIters = 100000 

Betriebszeit (µsec)  
nop () Funktion: 0.14 
Unterfunktion nop (): 0.14 
@ () [] anonyme Funktion: 0.69 
nop (obj) -Methode: 3.28 
nop () private fcn auf @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 Konstante: 4.18 
classdef-Eigenschaft: 1.18 
classdef-Eigenschaft mit Getter: 19.26 
+ Funktion pkg.nop (): 4.03 
+ pkg.nop () von innen + 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 () von Java: 0.01 
MEX mexnop (): 0,91 
eingebautes j (): 0,02 
struct s.foo field access: 0.14 
isempty (persistent): 0,00 

Update: R2015b: Objekte wurden schneller!

Hier sind die Ergebnisse von R2015b, freundlicherweise zur Verfügung gestellt von @Shaked. Dies ist eine große Änderung: OOP ist erheblich schneller, und jetzt ist die obj.method()Syntax so schnell wie method(obj)und viel schneller als bei älteren OOP-Objekten.

Matlab R2015b auf PCWIN64  
Matlab 8.6.0.267246 (R2015b) / Java 1.7.0_60 unter PCWIN64 Windows 8 6.2 (nanit-shaked) 
Maschine: Core i7-4720HQ-CPU bei 2,60 GHz, 16 GB RAM (20378)
nIters = 100000 

Betriebszeit (µsec)  
nop () Funktion: 0.04 
Unterfunktion nop (): 0.08 
@ () [] anonyme Funktion: 1.83 
nop (obj) -Methode: 3.15 
nop () private fcn auf @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 Konstante: 0,25 
classdef-Eigenschaft: 0,25 
classdef-Eigenschaft mit Getter: 0.64 
+ pkg.nop () Funktion: 0.04 
+ pkg.nop () von innen + 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 Uhr 
Java Klass.staticNop (): 24.48 
Java obj.nop () von Java: 0.01 
MEX mexnop (): 0,33 
eingebautes j (): 0,15 
struct s.foo field access: 0.25 
isempty (persistent): 0,13 

Update: R2018a

Hier sind die Ergebnisse von R2018a. Es ist nicht der große Sprung, den wir gesehen haben, als die neue Ausführungs-Engine in R2015b eingeführt wurde, aber es ist immer noch eine spürbare Verbesserung gegenüber dem Vorjahr. Insbesondere anonyme Funktionshandles wurden viel schneller.

Matlab R2018a auf MACI64  
Matlab 9.4.0.813654 (R2018a) / Java 1.8.0_144 unter MACI64 Mac OS X 10.13.5 (eilonwy) 
Maschine: Core i7-3615QM CPU bei 2,30 GHz, 16 GB RAM 
nIters = 100000 

Betriebszeit (µsec)  
nop () Funktion: 0.03 
Unterfunktion nop (): 0.04 
@ () [] anonyme Funktion: 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-Konstante: 0,16 
classdef-Eigenschaft: 0.13 
classdef-Eigenschaft mit Getter: 0.39 
+ pkg.nop () Funktion: 0.02 
+ pkg.nop () von innen + 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 () von Java: 0.03 
MEX mexnop (): 3,54 
eingebautes j (): 0,10 
struct s.foo field access: 0.16 
isempty (persistent): 0,07 

Update: R2018b und R2019a: Keine Änderung

Keine wesentlichen Änderungen. Ich mache mir nicht die Mühe, die Testergebnisse einzubeziehen.

Quellcode für Benchmarks

Ich habe den Quellcode für diese Benchmarks auf GitHub veröffentlicht, der unter der MIT-Lizenz veröffentlicht wurde. https://github.com/apjanke/matlab-bench

Andrew Janke
quelle
5
@AndrewJanke Glaubst du, du könntest den Benchmark mit R2012a wieder laufen lassen? Das ist wirklich interessant.
Dang Khoa
7
Hallo Leute. Wenn Sie immer noch am Quellcode interessiert sind, habe ich ihn rekonstruiert und auf GitHub als Open-Source-Version bereitgestellt. github.com/apjanke/matlab-bench
Andrew Janke
2
@Seeda: Statische Methoden werden in diesen Ergebnissen als "classdef class.static_nop ()" aufgeführt. Sie sind im Vergleich zu Funktionen ziemlich langsam. Wenn sie nicht häufig angerufen werden, spielt das keine Rolle.
Andrew Janke
2
@AndrewJanke Hier ist es: gist.github.com/ShakedDovrat/62db9e8f6883c5e28fc0
Shaked
2
Beeindruckend! Wenn diese Ergebnisse stimmen, muss ich möglicherweise diese ganze Antwort überarbeiten. Hinzugefügt. Vielen Dank!
Andrew Janke
3

Die Handle-Klasse hat einen zusätzlichen Aufwand, wenn alle Verweise zu Bereinigungszwecken auf sich selbst verfolgt werden.

Versuchen Sie dasselbe Experiment ohne Verwendung der Handle-Klasse und sehen Sie, was Ihre Ergebnisse sind.

MikeEL
quelle
1
genau das gleiche Experiment mit String, aber jetzt als Wertklasse (allerdings auf einem anderen Computer); atest: 0,009, btest: o.356. Das ist im Grunde der gleiche Unterschied wie beim Handle, daher denke ich nicht, dass das Verfolgen von Referenzen die Schlüsselantwort ist. Es erklärt auch nicht den Overhead in Funktionen gegenüber Funktionen in Paketen.
Stijn
Welche Version von Matlab verwenden Sie?
MikeEL
1
Ich habe einige ähnliche Vergleiche zwischen Handle- und Wertklassen durchgeführt und keinen Leistungsunterschied zwischen den beiden festgestellt.
RjOllos
Ich merke auch keinen Unterschied mehr.
Mikeel
Sinnvoll: In Matlab werden alle Arrays, nicht nur Objekte, mit Referenzzählungen versehen, da sie Copy-on-Write- und gemeinsame zugrunde liegende Rohdaten verwenden.
Andrew Janke
1

Die OO-Leistung hängt wesentlich von der verwendeten MATLAB-Version ab. Ich kann nicht alle Versionen kommentieren, weiß aber aus Erfahrung, dass 2012a gegenüber 2010-Versionen stark verbessert wurde. Keine Benchmarks und daher keine Zahlen zu präsentieren. Mein Code, der ausschließlich mit Handle-Klassen geschrieben und unter 2012a geschrieben wurde, wird unter früheren Versionen überhaupt nicht ausgeführt.

HG Bruce
quelle
1

Eigentlich kein Problem mit Ihrem Code, aber es ist ein Problem mit Matlab. Ich denke, es ist eine Art Herumspielen, um so auszusehen. Das Kompilieren des Klassencodes ist nichts anderes als Overhead. Ich habe den Test mit einem einfachen Klassenpunkt (einmal als Handle) und dem anderen (einmal als Werteklasse) durchgeführt.

    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

Hier ist der 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

Die Ergebnisse t1 =

12,0212% Griff

t2 =

12.0042% Wert

t3 =

0.5489  % vector

t4 =

0.0707 % structure 

Vermeiden Sie daher für eine effiziente Leistung die Verwendung von OOP. Stattdessen ist die Struktur eine gute Wahl, um Variablen zu gruppieren

Ahmad
quelle