MATLAB OOP è lento o sto facendo qualcosa di sbagliato?


144

Sto sperimentando con MATLAB OOP , come inizio ho imitato 's classi Logger mia C ++ e sto mettendo tutte le mie funzioni di supporto di stringa in una classe String, pensando che sarebbe stato bello essere in grado di fare cose come a + b, a == b, a.find( b )invece di strcat( a b ), strcmp( a, b ), recuperare il primo elemento di strfind( a, b ), ecc.

Il problema: rallentamento

Ho messo le cose sopra da usare e ho notato immediatamente un drastico rallentamento. Sto sbagliando (il che è certamente possibile dato che ho un'esperienza MATLAB piuttosto limitata), o OOP di MATLAB introduce solo un sacco di spese generali?

Il mio caso di prova

Ecco il semplice test che ho fatto per la stringa, fondamentalmente semplicemente aggiungendo una stringa e rimuovendo di nuovo la parte aggiunta:

Nota: in realtà non scrivere una classe String come questa nel vero codice! Matlab ha ora un stringtipo di array nativo e dovresti usarlo invece.

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

I risultati

Tempo totale in secondi, per 1000 iterazioni:

btest 0,550 (con String.SetLength 0.138, String.plus 0.065, String.Length 0.057)

atest 0,015

I risultati per il sistema di registrazione sono ugualmente: 0,1 secondi per 1000 chiamate a frpintf( 1, 'test\n' ), 7 (!) Secondi per 1000 chiamate al mio sistema quando uso internamente la classe String (OK, ha molta più logica in essa, ma da confrontare con C ++: l'overhead del mio sistema che utilizza std::string( "blah" )e std::coutsul lato output rispetto a quello normale std::cout << "blah"è dell'ordine di 1 millisecondo.)

È solo sovraccarico quando si cercano le funzioni di classe / pacchetto?

Poiché MATLAB viene interpretato, deve cercare la definizione di una funzione / oggetto in fase di esecuzione. Quindi mi chiedevo che forse un sovraccarico maggiore fosse coinvolto nella ricerca di funzioni di classe o pacchetto rispetto a funzioni che si trovano nel percorso. Ho provato a provare questo, e diventa solo più strano. Per escludere l'influenza di classi / oggetti, ho confrontato la chiamata di una funzione nel percorso rispetto a una funzione in un pacchetto:

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

Risultati, raccolti come sopra:

atest 0,004 sec, 0,001 sec in ctest

tra 0,060 sec, 0,014 sec in util.ctest

Quindi, tutto questo overhead proviene solo dal MATLAB che sta trascorrendo del tempo a cercare definizioni per la sua implementazione OOP, mentre questo overhead non è presente per le funzioni che sono direttamente nel percorso?


5
Grazie per questa domanda! Le prestazioni dell'heap di Matlab (OOP / chiusure) mi hanno turbato per anni, vedi stackoverflow.com/questions/1446281/matlabs-garbage-collector . Sono davvero curioso di sapere cosa risponderà MatlabDoug / Loren / MikeKatz al tuo post.
Mikhail,

1
^ quella era una lettura interessante.
stijn

1
@MatlabDoug: forse il tuo collega Mike Karr può commentare OP?
Mikhail,

4
I lettori dovrebbero anche controllare questo recente post sul blog (di Dave Foti) che discute delle prestazioni OOP nell'ultima versione R2012a: Considerare le prestazioni nel codice MATLAB orientato agli oggetti
Amro,

1
Un semplice esempio della sensibilità sulla struttura del codice in cui il richiamo dei metodi dei sottoelementi viene rimosso dal ciclo. for i = 1:this.get_n_quantities() if(strcmp(id,this.get_quantity_rlz(i).get_id())) ix = i; end endrichiede 2,2 secondi, mentre 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 endrichiede 0,01, due ordini di riviste
Jose Ospina,

Risposte:


223

Ho lavorato con OO MATLAB per un po 'e ho finito per esaminare problemi di prestazioni simili.

La risposta breve è: sì, OOP di MATLAB è un po 'lento. Esiste un sostanziale overhead di chiamata del metodo, superiore alle lingue OO tradizionali e non c'è molto che puoi fare al riguardo. Parte del motivo potrebbe essere che MATLAB idiomatico utilizza un codice "vettorializzato" per ridurre il numero di chiamate al metodo e l'overhead per chiamata non è una priorità assoluta.

Ho confrontato le prestazioni scrivendo funzioni "nop" do-nothing come vari tipi di funzioni e metodi. Ecco alcuni risultati tipici.

>> call_nops
Computer: versione PCWIN: 2009b
Chiamando ogni funzione / metodo 100000 volte
Funzione nop (): 0,02261 sec 0,23 usec per chiamata
nop1-5 () funzioni: 0,02182 sec 0,22 usec per chiamata
Sotto funzione nop (): 0,02244 sec 0,22 usec per chiamata
@ () [] funzione anonima: 0,08461 sec 0,85 usec per chiamata
Metodo nop (obj): 0,24664 sec 2,47 usec per chiamata
nop1-5 (obj) metodi: 0,23469 sec 2,35 usec per chiamata
nop () funzione privata: 0,02197 sec 0,22 usec per chiamata
classdef nop (obj): 0.90547 sec 9.05 usec per chiamata
classdef obj.nop (): 1.75522 sec 17.55 usec per chiamata
classdef private_nop (obj): 0,84738 sec 8,47 usec per chiamata
classdef nop (obj) (m-file): 0.90560 sec 9.06 usec per chiamata
classdef class.staticnop (): 1.16361 sec 11.64 usec per chiamata
Java nop (): 2.43035 sec 24.30 usec per chiamata
Java static_nop (): 0,87682 sec 8,77 usec per chiamata
Java nop () da Java: 0,00014 sec 0,00 usec per chiamata
MEX mexnop (): 0,11409 sec 1,14 usec per chiamata
C nop (): 0,00001 sec 0,00 usec per chiamata

Risultati simili su R2008a fino a R2009b. Questo è su Windows XP x64 con MATLAB a 32 bit.

"Java nop ()" è un metodo Java do-nothing chiamato dall'interno di un ciclo M-code e include l'overhead di invio MATLAB-Java ad ogni chiamata. "Java nop () da Java" è la stessa cosa chiamata in un ciclo Java for () e non comporta questa penalità al contorno. Prendi i tempi Java e C con un granello di sale; un compilatore intelligente potrebbe ottimizzare completamente le chiamate.

Il meccanismo di scoping del pacchetto è nuovo, introdotto più o meno contemporaneamente alle classi classdef. Il suo comportamento può essere correlato.

Alcune conclusioni provvisorie:

  • I metodi sono più lenti delle funzioni.
  • I nuovi metodi di stile (classdef) sono più lenti dei metodi di vecchio stile.
  • La nuova obj.nop()sintassi è più lenta della nop(obj)sintassi, anche per lo stesso metodo su un oggetto classdef. Lo stesso vale per gli oggetti Java (non mostrati). Se vuoi andare veloce, chiama nop(obj).
  • Il sovraccarico della chiamata del metodo è maggiore (circa 2x) in MATLAB a 64 bit su Windows. (Non mostrato.)
  • L'invio del metodo MATLAB è più lento di alcune altre lingue.

Dire perché è così sarebbe solo una speculazione da parte mia. Gli interni OO del motore MATLAB non sono pubblici. Non è un problema interpretato vs compilato di per sé - MATLAB ha una JIT - ma la digitazione e la sintassi più lente di MATLAB possono significare più lavoro in fase di esecuzione. (Ad esempio, dalla sintassi non si può dire da soli se "f (x)" è una chiamata di funzione o un indice in un array; dipende dallo stato dell'area di lavoro in fase di esecuzione.) Potrebbe essere perché le definizioni di classe di MATLAB sono legate allo stato del filesystem in un modo in cui molte altre lingue non lo sono.

Quindi che si fa?

Un approccio MATLAB idiomatico a questo consiste nel "vettorializzare" il codice strutturando le definizioni delle classi in modo tale che un'istanza di oggetto avvolga un array; vale a dire, ciascuno dei suoi campi contiene matrici parallele (chiamate organizzazione "planare" nella documentazione MATLAB). Piuttosto che avere una matrice di oggetti, ognuno con campi che contengono valori scalari, definiscono oggetti che sono essi stessi matrici e fanno in modo che i metodi prendano le matrici come input ed effettuino chiamate vettorializzate sui campi e sugli input. Ciò riduce il numero di chiamate di metodo effettuate, si spera abbastanza che l'overhead di spedizione non sia un collo di bottiglia.

Imitare una classe C ++ o Java in MATLAB probabilmente non sarà ottimale. Le classi Java / C ++ sono in genere costruite in modo tale che gli oggetti siano i più piccoli blocchi predefiniti, il più specifici possibile (vale a dire molte classi diverse) e li componi in matrici, oggetti di raccolta, ecc. E li esegui ripetutamente con loop. Per rendere veloci le classi MATLAB, capovolgi questo approccio. Hanno classi più grandi i cui campi sono matrici e chiamano metodi vettorializzati su tali matrici.

Il punto è organizzare il tuo codice affinché giochi ai punti di forza del linguaggio - gestione degli array, matematica vettoriale - ed evita i punti deboli.

EDIT: Dal post originale, R2010b e R2011a sono usciti. Il quadro generale è lo stesso, con le chiamate MCOS un po 'più veloci e le chiamate con metodi Java e vecchio stile diventano più lente .

EDIT: Avevo qui alcune note sulla "sensibilità del percorso" con una tabella aggiuntiva dei tempi delle chiamate di funzione, in cui i tempi di funzione erano influenzati dalla configurazione del percorso di Matlab, ma sembra essere stata un'aberrazione della mia particolare configurazione di rete in il tempo. La tabella sopra riflette i tempi tipici della preponderanza dei miei test nel tempo.

Aggiornamento: R2011b

EDIT (13/02/2012): R2011b è uscito e l'immagine delle prestazioni è cambiata abbastanza per aggiornarla.

Arch: PCWIN Rilascio: 2011b 
Macchina: R2011b, Windows XP, 8x Core i7-2600 a 3,40 GHz, 3 GB di RAM, NVIDIA NVS 300
Ogni operazione viene eseguita 100000 volte
stile totale µsec per chiamata
Funzione nop (): 0,01578 0,16
nop (), 10x loop svolgersi: 0,01477 0,15
nop (), 100x loop unroll: 0.01518 0.15
Sottofunzione nop (): 0,01559 0,16
@ () [] funzione anonima: 0.06400 0.64
Metodo nop (obj): 0,28482 2,85
nop () funzione privata: 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
costante classdef: 1.51890 15.19
proprietà classdef: 0.12992 1.30
proprietà classdef con getter: 1.39912 13.99
+ Funzione pkg.nop (): 0.87345 8.73
+ pkg.nop () dall'interno + 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 () da Java: 0.00010 0.00
MEX mexnop (): 0,08709 0,87
C nop (): 0,00001 0,00
j () (incorporato): 0,00251 0,03

Penso che il risultato di questo sia che:

  • I metodi MCOS / classdef sono più veloci. Il costo è ora alla pari con le classi di vecchio stile, purché si utilizzi la foo(obj)sintassi. Quindi la velocità del metodo non è più un motivo per restare fedeli alle classi di vecchio stile nella maggior parte dei casi. (Complimenti, MathWorks!)
  • Inserire le funzioni negli spazi dei nomi le rallenta. (Non nuovo in R2011b, solo nuovo nel mio test.)

Aggiornamento: R2014a

Ho ricostruito il codice di benchmarking ed eseguito su R2014a.

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

Tempo operativo (µsec)  
Funzione nop (): 0.14 
Sottofunzione nop (): 0.14 
@ () [] funzione anonima: 0.69 
Metodo nop (obj): 3.28 
nop () fcn privato su @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 
costante classdef: 4.18 
proprietà classdef: 1.18 
proprietà classdef con getter: 19.26 
+ Funzione pkg.nop (): 4.03 
+ pkg.nop () dall'interno + 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 () da Java: 0.01 
MEX mexnop (): 0.91 
builtin j (): 0.02 
accesso al campo struct s.foo: 0.14 
isempty (persistente): 0.00 

Aggiornamento: R2015b: gli oggetti sono diventati più veloci!

Ecco i risultati di R2015b, gentilmente forniti da @Shaked. Questo è un grande cambiamento: OOP è significativamente più veloce e ora la obj.method()sintassi è più veloce method(obj)e molto più veloce degli oggetti OOP legacy.

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

Tempo operativo (µsec)  
Funzione nop (): 0.04 
Sottofunzione nop (): 0.08 
@ () [] funzione anonima: 1.83 
Metodo nop (obj): 3.15 
nop () fcn privato su @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 
costante classdef: 0,25 
proprietà classdef: 0,25 
proprietà classdef con getter: 0.64 
+ Funzione pkg.nop (): 0.04 
+ pkg.nop () dall'interno + 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 () da Java: 0.01 
MEX mexnop (): 0,33 
builtin j (): 0.15 
accesso al campo struct s.foo: 0.25 
isempty (persistente): 0.13 

Aggiornamento: R2018a

Ecco i risultati di R2018a. Non è stato il grande salto che abbiamo visto quando è stato introdotto il nuovo motore di esecuzione in R2015b, ma è comunque un miglioramento apprezzabile anno dopo anno. In particolare, gli handle di funzioni anonimi sono diventati molto più veloci.

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

Tempo operativo (µsec)  
Funzione nop (): 0.03 
Sotto funzione nop (): 0.04 
@ () [] funzione anonima: 0.16 
classdef nop (obj): 0.16 
classdef obj.nop (): 0.17 
classdef pivate_nop (obj): 0.16 
classdef class.static_nop (): 0.03 
costante classdef: 0,16 
proprietà classdef: 0.13 
proprietà classdef con getter: 0,39 
+ Funzione pkg.nop (): 0.02 
+ pkg.nop () dall'interno + 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 () da Java: 0.03 
MEX mexnop (): 3.54 
builtin j (): 0.10 
accesso al campo struct s.foo: 0.16 
isempty (persistente): 0.07 

Aggiornamento: R2018b e R2019a: nessuna modifica

Nessun cambiamento significativo. Non mi preoccupo di includere i risultati del test.

Codice sorgente per benchmark

Ho inserito il codice sorgente di questi benchmark su GitHub, rilasciato sotto licenza MIT. https://github.com/apjanke/matlab-bench


5
@AndrewJanke Pensi di poter eseguire nuovamente il benchmark con R2012a? Questo è davvero interessante
Dang Khoa,

7
Ciao gente. Se sei ancora interessato al codice sorgente, l'ho ricostruito e aperto da GitHub. github.com/apjanke/matlab-bench
Andrew Janke

2
@Seeda: i metodi statici sono elencati come "classdef class.static_nop ()" in questi risultati. Sono piuttosto lenti rispetto alle funzioni. Se non vengono chiamati frequentemente, non importa.
Andrew Janke,


2
Wow! Se quei risultati reggono, potrei aver bisogno di rivedere l'intera risposta. Aggiunto. Grazie!
Andrew Janke,

3

La classe handle ha un sovraccarico aggiuntivo dal tracciamento di tutti i riferimenti a se stesso per scopi di pulizia.

Prova lo stesso esperimento senza utilizzare la classe handle e vedi quali sono i tuoi risultati.


1
esattamente lo stesso esperimento con String, ma ora come una classe di valore (su un altro computer); atest: 0,009, btest: o.356. Questa è sostanzialmente la stessa differenza con l'handle, quindi non credo che il rintracciamento dei riferimenti sia la risposta chiave. Inoltre, non spiega il sovraccarico nelle funzioni rispetto alla funzione nei pacchetti.
stijn

Quale versione di Matlab stai usando?
MikeEL,

1
Ho eseguito alcuni confronti simili tra le classi handle e value e non ho notato una differenza di prestazioni tra i due.
RjOllos,

Neanche più noto una differenza.
MikeEL,

Ha un senso: in Matlab, tutti gli array, non solo la gestione degli oggetti, vengono conteggiati come riferimento, poiché utilizzano i dati grezzi sottostanti condivisi da copia a scrittura.
Andrew Janke,

1

Le prestazioni OO dipendono in modo significativo dalla versione MATLAB utilizzata. Non posso commentare tutte le versioni, ma so per esperienza che 2012a è molto migliorato rispetto alle versioni 2010. Nessun benchmark e quindi nessun numero da presentare. Il mio codice, scritto esclusivamente utilizzando le classi handle e scritto sotto 2012a, non funzionerà affatto nelle versioni precedenti.


1

In realtà nessun problema con il tuo codice ma è un problema con Matlab. Penso che sia una specie di gioco in giro per assomigliare. Compilare il codice di classe non è altro che sovraccarico. Ho fatto il test con un semplice punto di classe (una volta come handle) e l'altro (una volta come classe di valore)

    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

ecco il 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

I risultati t1 =

12.0212% Impugnatura

t2 =

Valore del 12,0042%

t3 =

0.5489  % vector

t4 =

0.0707 % structure 

Pertanto per prestazioni efficienti evitare l'uso di OOP, invece la struttura è una buona scelta per raggruppare le variabili

Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.