MATLAB OOP est-il lent ou est-ce que je fais quelque chose de mal?

144

J'expérimente avec Matlab POO , comme un début j'imité ma classes C ++ Logger s et je suis en train de toutes mes fonctions d'aide à cordes dans une classe String, pensant que ce serait génial de pouvoir faire des choses comme a + b, a == b, au a.find( b )lieu de strcat( a b ), strcmp( a, b ), récupérer le premier élément de strfind( a, b ), etc.

Le problème: le ralentissement

J'ai mis les choses ci-dessus à profit et j'ai immédiatement remarqué un ralentissement drastique . Est-ce que je fais mal (ce qui est certainement possible car j'ai une expérience MATLAB plutôt limitée), ou est-ce que la POO de MATLAB introduit simplement beaucoup de frais généraux?

Mon cas de test

Voici le test simple que j'ai fait pour la chaîne, en ajoutant simplement une chaîne et en supprimant à nouveau la partie ajoutée:

Remarque: n'écrivez pas réellement une classe String comme celle-ci dans du code réel! Matlab a stringmaintenant un type de tableau natif , et vous devriez l'utiliser à la place.

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

Les resultats

Temps total en secondes, pour 1000 itérations:

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

au plus 0,015

Les résultats pour le système de journalisation sont les mêmes: 0,1 seconde pour 1000 appels à frpintf( 1, 'test\n' ), 7 (!) Secondes pour 1000 appels à mon système lors de l'utilisation de la classe String en interne (OK, il y a beaucoup plus de logique, mais à comparer avec C ++: la surcharge de mon système qui utilise std::string( "blah" )et std::coutdu côté sortie vs plain std::cout << "blah"est de l'ordre de 1 milliseconde.)

Est-ce juste une surcharge lors de la recherche de fonctions de classe / package?

Puisque MATLAB est interprété, il doit rechercher la définition d'une fonction / d'un objet au moment de l'exécution. Je me demandais donc que peut-être beaucoup plus de frais généraux sont impliqués dans la recherche de la fonction de classe ou de package par rapport aux fonctions qui se trouvent dans le chemin. J'ai essayé de tester ça, et ça devient plus étrange. Pour exclure l'influence des classes / objets, j'ai comparé l'appel d'une fonction dans le chemin à une fonction dans un package:

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

Résultats, rassemblés de la même manière que ci-dessus:

au plus 0,004 s, 0,001 s au test

btest 0,060 sec, 0,014 sec dans le test util.

Alors, est-ce que toute cette surcharge vient juste du temps passé par MATLAB à rechercher des définitions pour son implémentation POO, alors que cette surcharge n'est pas là pour les fonctions qui sont directement dans le chemin?

stijn
la source
5
Merci pour cette question! Les performances du tas Matlab (POO / fermetures) me gênent depuis des années, voir stackoverflow.com/questions/1446281/matlabs-garbage-collector . Je suis vraiment curieux de savoir ce que MatlabDoug / Loren / MikeKatz répondra à votre message.
Mikhail
1
^ c'était une lecture intéressante.
stijn
1
@MatlabDoug: peut-être que votre collègue Mike Karr peut commenter OP?
Mikhail
4
Les lecteurs devraient également consulter ce récent article de blog (par Dave Foti) sur les performances de la POO dans la dernière version de R2012a: Considérer les performances dans le code MATLAB orienté objet
Amro
1
Un exemple simple de la sensibilité sur la structure du code dans lequel l'appel de méthodes de sous-éléments est sorti de la boucle. for i = 1:this.get_n_quantities() if(strcmp(id,this.get_quantity_rlz(i).get_id())) ix = i; end endprend 2,2 sec, tandis que 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 endprend 0,01, deux ordres de mag
Jose Ospina

Réponses:

223

Je travaille avec OO MATLAB depuis un moment et j'ai fini par examiner des problèmes de performances similaires.

La réponse courte est: oui, la POO de MATLAB est plutôt lente. Il y a une surcharge d'appel de méthode substantielle, plus élevée que les langages OO traditionnels, et vous ne pouvez pas faire grand-chose à ce sujet. Une partie de la raison peut être que MATLAB idiomatique utilise du code "vectorisé" pour réduire le nombre d'appels de méthode, et le surdébit par appel n'est pas une priorité élevée.

J'ai comparé les performances en écrivant des fonctions «nop» à ne rien faire comme les différents types de fonctions et de méthodes. Voici quelques résultats typiques.

>> call_nops
Ordinateur: PCWIN Version: 2009b
Appel de chaque fonction / méthode 100000 fois
Fonction nop (): 0,02261 sec 0,23 usec par appel
Fonctions nop1-5 (): 0,02182 sec 0,22 usec par appel
Sous-fonction nop (): 0,02244 sec 0,22 usec par appel
@ () [] fonction anonyme: 0,08461 s 0,85 usec par appel
Méthode nop (obj): 0,24664 sec 2,47 usec par appel
méthodes nop1-5 (obj): 0,23469 sec 2,35 usec par appel
nop () fonction privée: 0,02197 sec 0,22 usec par appel
classdef nop (obj): 0,90547 sec 9,05 usec par appel
classdef obj.nop (): 1,75522 sec 17,55 usec par appel
classdef private_nop (obj): 0,84738 s 8,47 usec par appel
classdef nop (obj) (m-file): 0,90560 sec 9,06 usec par appel
classdef class.staticnop (): 1,16361 sec 11,64 usec par appel
Java nop (): 2,43035 sec 24,30 usec par appel
Java static_nop (): 0,87682 sec 8,77 usec par appel
Java nop () depuis Java: 0,00014 sec 0,00 usec par appel
MEX mexnop (): 0,11409 s 1,14 usec par appel
C nop (): 0,00001 s 0,00 usec par appel

Résultats similaires sur R2008a à R2009b. Ceci est sur Windows XP x64 exécutant MATLAB 32 bits.

Le "Java nop ()" est une méthode Java à ne rien faire appelée à partir d'une boucle de code M, et inclut la surcharge de répartition MATLAB vers Java à chaque appel. "Java nop () de Java" est la même chose appelée dans une boucle Java for () et n'entraîne pas cette pénalité de limite. Prenez les timings Java et C avec un grain de sel; un compilateur intelligent pourrait optimiser complètement les appels.

Le mécanisme de portée des packages est nouveau, introduit à peu près en même temps que les classes classdef. Son comportement peut être lié.

Quelques conclusions provisoires:

  • Les méthodes sont plus lentes que les fonctions.
  • Les méthodes de style nouveau (classdef) sont plus lentes que les méthodes de style ancien.
  • La nouvelle obj.nop()syntaxe est plus lente que la nop(obj)syntaxe, même pour la même méthode sur un objet classdef. Idem pour les objets Java (non représentés). Si vous voulez aller vite, appelez nop(obj).
  • La surcharge d'appel de méthode est plus élevée (environ 2x) dans MATLAB 64 bits sous Windows. (Pas montré.)
  • La répartition des méthodes MATLAB est plus lente que dans certains autres langages.

Dire pourquoi il en est ainsi ne serait que spéculation de ma part. Les internes OO du moteur MATLAB ne sont pas publics. Ce n'est pas un problème interprété ou compilé en soi - MATLAB a un JIT - mais le typage et la syntaxe plus lâches de MATLAB peuvent signifier plus de travail au moment de l'exécution. (Par exemple, vous ne pouvez pas dire à partir de la syntaxe seule si "f (x)" est un appel de fonction ou un index dans un tableau; cela dépend de l'état de l'espace de travail au moment de l'exécution.) Il se peut que les définitions de classe de MATLAB soient liées à l'état du système de fichiers d'une manière que de nombreuses autres langues ne le sont pas.

Alors que faire?

Une approche MATLAB idiomatique consiste à "vectoriser" votre code en structurant vos définitions de classe de telle sorte qu'une instance d'objet enveloppe un tableau; c'est-à-dire que chacun de ses champs contient des tableaux parallèles (appelés organisation "planaire" dans la documentation MATLAB). Plutôt que d'avoir un tableau d'objets, chacun avec des champs contenant des valeurs scalaires, définissez des objets qui sont eux-mêmes des tableaux, et demandez aux méthodes de prendre des tableaux comme entrées et d'effectuer des appels vectorisés sur les champs et les entrées. Cela réduit le nombre d'appels de méthode effectués, suffisamment, espérons-le, que la surcharge de répartition ne constitue pas un goulot d'étranglement.

Imiter une classe C ++ ou Java dans MATLAB ne sera probablement pas optimal. Les classes Java / C ++ sont généralement construites de manière à ce que les objets soient les plus petits blocs de construction, aussi spécifiques que possible (c'est-à-dire, de nombreuses classes différentes), et vous les composez dans des tableaux, des objets de collection, etc., et les parcourez avec des boucles. Pour créer des classes MATLAB rapides, retournez cette approche à l'envers. Avoir des classes plus grandes dont les champs sont des tableaux et appeler des méthodes vectorisées sur ces tableaux.

Le but est d'arranger votre code pour qu'il joue sur les forces du langage - gestion des tableaux, mathématiques vectorisées - et éviter les points faibles.

EDIT: Depuis le post original, R2010b et R2011a sont sortis. L'image globale est la même, les appels MCOS devenant un peu plus rapides, et les appels aux méthodes Java et à l'ancienne devenant plus lents .

EDIT: J'avais l'habitude d'avoir quelques notes ici sur la "sensibilité du chemin" avec un tableau supplémentaire des horaires des appels de fonction, où les temps de fonction étaient affectés par la façon dont le chemin Matlab était configuré, mais cela semble avoir été une aberration de ma configuration réseau particulière à le temps. Le graphique ci-dessus reflète les temps typiques de la prépondérance de mes tests dans le temps.

Mise à jour: R2011b

EDIT (13/02/2012): R2011b est sorti, et l'image des performances a suffisamment changé pour mettre à jour cela.

Arch: PCWIN Sortie: 2011b 
Machine: R2011b, Windows XP, 8x Core i7-2600 à 3,40 GHz, 3 Go de RAM, NVIDIA NVS 300
Faire chaque opération 100000 fois
style total en µs par appel
Fonction nop (): 0,01578 0,16
nop (), 10x déroulement de la boucle: 0,01477 0,15
nop (), déroulement de boucle 100x: 0,01518 0,15
Sous-fonction nop (): 0,01559 0,16
@ () [] fonction anonyme: 0,06400 0,64
Méthode nop (obj): 0,28482 2,85
fonction privée 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
constante classdef: 1,51890 15,19
Propriété classdef: 0.12992 1.30
propriété classdef avec getter: 1.39912 13.99
+ fonction pkg.nop (): 0,87345 8,73
+ pkg.nop () de l'intérieur + 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 () depuis Java: 0,00010 0,00
MEX mexnop (): 0,08709 0,87
C nop (): 0,00001 0,00
j () (intégré): 0,00251 0,03

Je pense que le résultat est que:

  • Les méthodes MCOS / classdef sont plus rapides. Le coût est désormais comparable à celui des classes de style ancien, tant que vous utilisez la foo(obj)syntaxe. La vitesse des méthodes n'est donc plus une raison de s'en tenir aux anciennes classes dans la plupart des cas. (Félicitations, MathWorks!)
  • Mettre des fonctions dans des espaces de noms les ralentit. (Pas nouveau dans R2011b, juste nouveau dans mon test.)

Mise à jour: R2014a

J'ai reconstruit le code d'analyse comparative et l'ai exécuté sur R2014a.

Matlab R2014a sur PCWIN64  
Matlab 8.3.0.532 (R2014a) / Java 1.7.0_11 sur PCWIN64 Windows 7 6.1 (eilonwy-win7) 
Machine: processeur Core i7-3615QM à 2,30 GHz, 4 Go de RAM (plate-forme virtuelle VMware)
nItres = 100000 

Temps de fonctionnement (µsec)  
Fonction nop (): 0,14 
Sous-fonction nop (): 0,14 
@ () [] fonction anonyme: 0,69 
Méthode nop (obj): 3.28 
nop () fcn privé sur @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 
constante classdef: 4,18 
Propriété classdef: 1.18 
propriété classdef avec getter: 19.26 
+ Fonction pkg.nop (): 4.03 
+ pkg.nop () de l'intérieur + pkg: 4.16 
féval ('nop'): 2,31 
féval (@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 () depuis Java: 0.01 
MEX mexnop (): 0,91 
intégré j (): 0,02 
accès au champ struct s.foo: 0.14 
isempty (persistant): 0,00 

Mise à jour: R2015b: les objets sont devenus plus rapides!

Voici les résultats de R2015b, aimablement fournis par @Shaked. C'est un grand changement: la POO est beaucoup plus rapide, et maintenant la obj.method()syntaxe est aussi rapide method(obj)et beaucoup plus rapide que les objets OOP hérités.

Matlab R2015b sur PCWIN64  
Matlab 8.6.0.267246 (R2015b) / Java 1.7.0_60 sur PCWIN64 Windows 8 6.2 (nanit-shaked) 
Machine: Processeur Core i7-4720HQ à 2,60 GHz, 16 Go de RAM (20378)
nItres = 100000 

Temps de fonctionnement (µsec)  
Fonction nop (): 0,04 
Sous-fonction nop (): 0,08 
@ () [] fonction anonyme: 1,83 
méthode nop (obj): 3.15 
nop () fcn privé sur @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 
constante classdef: 0,25 
propriété classdef: 0.25 
propriété classdef avec getter: 0.64 
+ fonction pkg.nop (): 0,04 
+ pkg.nop () de l'intérieur + pkg: 0,04 
féval ('nop'): 8,26 
féval (@nop): 0,63 
eval ('nop'): 21,22 
Java obj.nop (): 14.15 
Java nop (obj): 2.50 
Java feval ('nop', obj): 10h30 
Java Klass.staticNop (): 24,48 
Java obj.nop () depuis Java: 0.01 
MEX mexnop (): 0,33 
intégré j (): 0,15 
accès au champ struct s.foo: 0,25 
isempty (persistant): 0,13 

Mise à jour: R2018a

Voici les résultats de R2018a. Ce n'est pas l'énorme saut que nous avons vu lorsque le nouveau moteur d'exécution a été introduit dans R2015b, mais c'est toujours une amélioration appréciable d'une année à l'autre. Notamment, les poignées de fonctions anonymes sont devenues beaucoup plus rapides.

Matlab R2018a sur MACI64  
Matlab 9.4.0.813654 (R2018a) / Java 1.8.0_144 sur MACI64 Mac OS X 10.13.5 (eilonwy) 
Machine: processeur Core i7-3615QM à 2,30 GHz, 16 Go de RAM 
nItres = 100000 

Temps de fonctionnement (µsec)  
Fonction nop (): 0,03 
Sous-fonction nop (): 0,04 
@ () [] fonction anonyme: 0,16 
classdef nop (obj): 0,16 
classdef obj.nop (): 0,17 
classdef pivate_nop (obj): 0.16 
classdef class.static_nop (): 0,03 
constante classdef: 0,16 
propriété classdef: 0.13 
propriété classdef avec getter: 0.39 
+ fonction pkg.nop (): 0,02 
+ pkg.nop () de l'intérieur + pkg: 0,02 
féval ('nop'): 15,62 
féval (@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 () depuis Java: 0.03 
MEX mexnop (): 3,54 
intégré j (): 0,10 
accès au champ struct s.foo: 0.16 
isempty (persistant): 0,07 

Mise à jour: R2018b et R2019a: aucun changement

Aucun changement significatif. Je ne prends pas la peine d'inclure les résultats des tests.

Code source pour les benchmarks

J'ai mis le code source de ces benchmarks sur GitHub, publié sous la licence MIT. https://github.com/apjanke/matlab-bench

Andrew Janke
la source
5
@AndrewJanke Pensez-vous que vous pourriez à nouveau exécuter le benchmark avec R2012a? C'est vraiment intéressant.
Dang Khoa
7
Salut les gens. Si vous êtes toujours intéressé par le code source, je l'ai reconstruit et open-source sur GitHub. github.com/apjanke/matlab-bench
Andrew Janke
2
@Seeda: Les méthodes statiques sont répertoriées comme "classdef class.static_nop ()" dans ces résultats. Ils sont assez lents par rapport aux fonctions. S'ils ne sont pas appelés fréquemment, cela n'a pas d'importance.
Andrew Janke
2
@AndrewJanke Le voici: gist.github.com/ShakedDovrat/62db9e8f6883c5e28fc0
Shaked le
2
Hou la la! Si ces résultats tiennent, je devrai peut-être revoir toute cette réponse. Ajoutée. Merci!
Andrew Janke
3

La classe handle a une surcharge supplémentaire du suivi de toutes les références à elle-même à des fins de nettoyage.

Essayez la même expérience sans utiliser la classe handle et voyez quels sont vos résultats.

MikeEL
la source
1
exactement la même expérience avec String, mais maintenant en tant que classe de valeur (sur une autre machine cependant); au plus: 0,009, meilleur: o.356. C'est fondamentalement la même différence qu'avec la poignée, donc je ne pense pas que le suivi des références soit la réponse clé. Cela n'explique pas non plus la surcharge des fonctions par rapport à la fonction dans les packages.
stijn
Quelle version de matlab utilisez-vous?
MikeEL
1
J'ai effectué des comparaisons similaires entre les classes de descripteur et de valeur et je n'ai pas remarqué de différence de performances entre les deux.
RjOllos
Je ne remarque plus de différence non plus.
MikeEL
Cela a du sens: dans Matlab, tous les tableaux, et pas seulement les objets de gestion, sont comptés par référence, car ils utilisent des données brutes sous-jacentes de copie sur écriture et partagées.
Andrew Janke
1

Les performances OO dépendent de manière significative de la version MATLAB utilisée. Je ne peux pas faire de commentaires sur toutes les versions, mais je sais par expérience que 2012a est bien améliorée par rapport aux versions 2010. Pas de benchmarks et donc pas de chiffres à présenter. Mon code, exclusivement écrit à l'aide de classes handle et écrit sous 2012a ne fonctionnera pas du tout sous les versions antérieures.

HG Bruce
la source
1

En fait pas de problème avec votre code mais c'est un problème avec Matlab. Je pense que c'est une sorte de jeu pour ressembler. La compilation du code de classe n'est rien qu'une surcharge. J'ai fait le test avec un point de classe simple (une fois comme poignée) et l'autre (une fois comme classe de valeur)

    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

voici le 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

Les résultats t1 =

12.0212% Poignée

t2 =

12,0042% valeur

t3 =

0.5489  % vector

t4 =

0.0707 % structure 

Par conséquent, pour des performances efficaces, évitez d'utiliser la POO à la place, la structure est un bon choix pour regrouper les variables

Ahmad
la source