¿MATLAB OOP es lento o estoy haciendo algo mal?

144

Estoy experimentando con MATLAB programación orientada a objetos , como un principio he imitado mi C ++ 's clases Logger y yo estoy poniendo todas mis funciones de ayuda de cuerdas en una clase String, pensando que sería genial ser capaz de hacer cosas como a + b, a == b, a.find( b )en lugar de strcat( a b ), strcmp( a, b ), recuperar el primer elemento de strfind( a, b ), etc.

El problema: desaceleración

Puse las cosas anteriores para usar e inmediatamente noté una drástica desaceleración. ¿Lo estoy haciendo mal?

Mi caso de prueba

Aquí está la prueba simple que hice para la cadena, básicamente agregando una cadena y eliminando la parte adjunta nuevamente:

Nota: ¡No escriba una clase de cadena como esta en código real! Matlab tiene un stringtipo de matriz nativo ahora, y debería usarlo en su lugar.

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

Los resultados

Tiempo total en segundos, para 1000 iteraciones:

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

atest 0.015

Los resultados para el sistema de registro también son: 0.1 segundos para 1000 llamadas a frpintf( 1, 'test\n' ), 7 (!) Segundos para 1000 llamadas a mi sistema cuando uso la clase String internamente (OK, tiene mucha más lógica, pero para comparar con C ++: la sobrecarga de mi sistema que usa std::string( "blah" )y std::couten el lado de salida vs simple std::cout << "blah"está en el orden de 1 milisegundo).

¿Es solo una sobrecarga al buscar funciones de clase / paquete?

Dado que MATLAB se interpreta, debe buscar la definición de una función / objeto en tiempo de ejecución. Entonces, me preguntaba que tal vez haya mucha más sobrecarga al buscar la función de clase o paquete frente a las funciones que están en la ruta. Traté de probar esto, y se vuelve más extraño. Para descartar la influencia de clases / objetos, comparé llamar a una función en la ruta frente a una función en un paquete:

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

Resultados, reunidos de la misma manera que arriba:

atest 0.004 sec, 0.001 sec en ctest

btest 0.060 sec, 0.014 sec en util.ctest

Entonces, ¿toda esta sobrecarga proviene de que MATLAB pasa tiempo buscando definiciones para su implementación OOP, mientras que esta sobrecarga no está allí para las funciones que están directamente en el camino?

stijn
fuente
55
¡Gracias por esta pregunta! El rendimiento del montón de Matlab (OOP / cierres) me ha preocupado durante años, consulte stackoverflow.com/questions/1446281/matlabs-garbage-collector . Tengo curiosidad por saber qué responderá MatlabDoug / Loren / MikeKatz a tu publicación.
Mikhail el
1
^ esa fue una lectura interesante.
stijn el
1
@MatlabDoug: ¿quizás tu colega Mike Karr pueda comentar OP?
Mikhail
44
Los lectores también deben consultar esta reciente publicación de blog (por Dave Foti) sobre el rendimiento de OOP en la última versión de R2012a: Consideración del rendimiento en el código MATLAB orientado a objetos
Amro
1
Un ejemplo simple de la sensibilidad en la estructura del código en la que la llamada de los métodos de subelementos se saca del bucle. for i = 1:this.get_n_quantities() if(strcmp(id,this.get_quantity_rlz(i).get_id())) ix = i; end endtoma 2.2 segundos, mientras 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 endtoma 0.01, dos órdenes de mag
Jose Ospina

Respuestas:

223

He estado trabajando con OO MATLAB durante un tiempo y terminé buscando problemas de rendimiento similares.

La respuesta corta es: sí, la POO de MATLAB es un poco lenta. Hay una sobrecarga de llamadas de método sustancial, más alta que los lenguajes OO convencionales, y no hay mucho que pueda hacer al respecto. Parte de la razón puede ser que MATLAB idiomático usa código "vectorizado" para reducir el número de llamadas a métodos, y la sobrecarga por llamada no es una prioridad alta.

Comparé el rendimiento escribiendo funciones "nop" de no hacer nada como los diversos tipos de funciones y métodos. Aquí hay algunos resultados típicos.

>> call_nops
Computadora: PCWIN Lanzamiento: 2009b
Llamar a cada función / método 100000 veces
función nop (): 0.02261 sec 0.23 usec por llamada
funciones nop1-5 (): 0.02182 sec 0.22 usec por llamada
subfunción nop (): 0.02244 sec 0.22 usec por llamada
@ () [] función anónima: 0.08461 sec 0.85 usec por llamada
método nop (obj): 0.24664 sec 2.47 usec por llamada
métodos nop1-5 (obj): 0.23469 sec 2.35 usec por llamada
función privada nop (): 0.02197 sec 0.22 usec por llamada
classdef nop (obj): 0.90547 sec 9.05 usec por llamada
classdef obj.nop (): 1.75522 sec 17.55 usec por llamada
classdef private_nop (obj): 0.84738 sec 8.47 usec por llamada
classdef nop (obj) (archivo-m): 0.90560 sec 9.06 usec por llamada
classdef class.staticnop (): 1.16361 sec 11.64 usec por llamada
Java nop (): 2.43035 sec 24.30 usec por llamada
Java static_nop (): 0.87682 sec 8.77 usec por llamada
Java nop () de Java: 0.00014 sec 0.00 usec por llamada
MEX mexnop (): 0.11409 sec 1.14 usec por llamada
C nop (): 0.00001 sec 0.00 usec por llamada

Resultados similares en R2008a a R2009b. Esto está en Windows XP x64 ejecutando MATLAB de 32 bits.

El "Java nop ()" es un método Java que no se hace nada que se llama desde un bucle de código M e incluye la sobrecarga de despacho de MATLAB a Java con cada llamada. "Java nop () from Java" es lo mismo que se llama en un bucle Java for () y no incurre en esa penalización de límite. Tome los tiempos de Java y C con un grano de sal; Un compilador inteligente podría optimizar las llamadas por completo.

El mecanismo de definición del paquete es nuevo, introducido aproximadamente al mismo tiempo que las clases classdef. Su comportamiento puede estar relacionado.

Algunas conclusiones tentativas:

  • Los métodos son más lentos que las funciones.
  • Los nuevos métodos de estilo (classdef) son más lentos que los métodos de estilo antiguo.
  • La nueva obj.nop()sintaxis es más lenta que la nop(obj)sintaxis, incluso para el mismo método en un objeto classdef. Lo mismo para los objetos Java (no se muestran). Si quieres ir rápido, llama nop(obj).
  • La sobrecarga de la llamada al método es mayor (aproximadamente 2 veces) en MATLAB de 64 bits en Windows. (No mostrado.)
  • El envío del método MATLAB es más lento que en otros idiomas.

Decir por qué esto es así sería especulación de mi parte. Los componentes internos OO del motor MATLAB no son públicos. No es un problema interpretado vs compilado per se - MATLAB tiene un JIT - pero la sintaxis y la escritura más flexible de MATLAB pueden significar más trabajo en tiempo de ejecución. (Por ejemplo, no puede distinguir solo de la sintaxis si "f (x)" es una llamada de función o un índice en una matriz; depende del estado del espacio de trabajo en tiempo de ejecución). Puede ser porque las definiciones de clase de MATLAB están vinculadas al estado del sistema de archivos de una manera que muchos otros idiomas no lo son.

¿Entonces lo que hay que hacer?

Un enfoque idiomático de MATLAB para esto es "vectorizar" su código estructurando sus definiciones de clase de modo que una instancia de objeto envuelva una matriz; es decir, cada uno de sus campos contiene matrices paralelas (denominadas organización "planar" en la documentación de MATLAB). En lugar de tener una matriz de objetos, cada uno con campos que contienen valores escalares, define objetos que son en sí mismos matrices, y hace que los métodos tomen matrices como entradas y hagan llamadas vectorizadas en los campos y entradas. Esto reduce la cantidad de llamadas a métodos realizadas, es de esperar que la sobrecarga del envío no sea un cuello de botella.

Imitar una clase C ++ o Java en MATLAB probablemente no será óptimo. Las clases Java / C ++ generalmente se crean de manera que los objetos son los bloques de construcción más pequeños, tan específicos como sea posible (es decir, muchas clases diferentes), y los compones en matrices, objetos de colección, etc., y los repites con bucles. Para hacer clases rápidas de MATLAB, cambie ese enfoque al revés. Tenga clases más grandes cuyos campos sean matrices y llame a métodos vectorizados en esas matrices.

El punto es organizar su código para jugar con las fortalezas del lenguaje (manejo de matriz, matemática vectorizada) y evitar los puntos débiles.

EDITAR: Desde la publicación original, han salido R2010b y R2011a. La imagen general es la misma, con las llamadas MCOS cada vez más rápidas, y las llamadas a métodos antiguos y Java cada vez más lentas .

EDITAR: Solía ​​tener algunas notas aquí sobre "sensibilidad de ruta" con una tabla adicional de temporizaciones de llamadas de función, donde los tiempos de función se vieron afectados por cómo se configuró la ruta de Matlab, pero eso parece haber sido una aberración de mi configuración de red particular en el tiempo. El cuadro anterior refleja los tiempos típicos de la preponderancia de mis pruebas a lo largo del tiempo.

Actualización: R2011b

EDITAR (13/02/2012): R2011b está fuera, y la imagen de rendimiento ha cambiado lo suficiente como para actualizar esto.

Arco: PCWIN Lanzamiento: 2011b 
Máquina: R2011b, Windows XP, 8x Core i7-2600 @ 3.40GHz, 3 GB de RAM, NVIDIA NVS 300
Haciendo cada operación 100000 veces
estilo total µseg por llamada
función nop (): 0.01578 0.16
nop (), desenrollado de bucle 10x: 0.01477 0.15
nop (), desenrollado de bucle 100x: 0.01518 0.15
subfunción nop (): 0.01559 0.16
@ () [] función anónima: 0.06400 0.64
método nop (obj): 0.28482 2.85
función privada 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
propiedad classdef: 0.12992 1.30
propiedad classdef con getter: 1.39912 13.99
+ función pkg.nop (): 0.87345 8.73
+ pkg.nop () desde adentro + 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 () de Java: 0.00010 0.00
MEX mexnop (): 0.08709 0.87
C nop (): 0.00001 0.00
j () (incorporado): 0.00251 0.03

Creo que el resultado de esto es que:

  • Los métodos MCOS / classdef son más rápidos. El costo ahora está a la par con las clases de estilo antiguo, siempre que use la foo(obj)sintaxis. Entonces, la velocidad del método ya no es una razón para seguir con las clases de estilo antiguo en la mayoría de los casos. (¡Felicitaciones, MathWorks!)
  • Poner funciones en espacios de nombres los hace lentos. (No es nuevo en R2011b, solo nuevo en mi prueba).

Actualización: R2014a

Reconstruí el código de evaluación comparativa y lo ejecuté en R2014a.

Matlab R2014a en PCWIN64  
Matlab 8.3.0.532 (R2014a) / Java 1.7.0_11 en PCWIN64 Windows 7 6.1 (eilonwy-win7) 
Máquina: CPU Core i7-3615QM a 2.30 GHz, 4 GB de RAM (plataforma virtual VMware)
nIters = 100000 

Tiempo de operación (µseg)  
función nop (): 0.14 
subfunción nop (): 0.14 
@ () [] función anónima: 0,69 
método nop (obj): 3.28 
nop () fcn privado en @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 
propiedad classdef: 1.18 
propiedad classdef con getter: 19.26 
+ función pkg.nop (): 4.03 
+ pkg.nop () desde adentro + 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 () de Java: 0.01 
MEX mexnop (): 0.91 
j integrado (): 0.02 
struct s.foo field access: 0.14 
vacío (persistente): 0.00 

Actualización: R2015b: ¡Los objetos se volvieron más rápidos!

Aquí están los resultados de R2015b, amablemente proporcionados por @Shaked. Este es un gran cambio: OOP es significativamente más rápido, y ahora la obj.method()sintaxis es tan rápida method(obj)y mucho más rápida que los objetos OOP heredados.

Matlab R2015b en PCWIN64  
Matlab 8.6.0.267246 (R2015b) / Java 1.7.0_60 en PCWIN64 Windows 8 6.2 (nanit-shaked) 
Máquina: CPU Core i7-4720HQ a 2.60 GHz, 16 GB de RAM (20378)
nIters = 100000 

Tiempo de operación (µseg)  
función nop (): 0.04 
subfunción nop (): 0.08 
@ () [] función anónima: 1.83 
método nop (obj): 3.15 
nop () fcn privado en @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 
propiedad classdef: 0.25 
propiedad classdef con getter: 0.64 
Función + pkg.nop (): 0.04 
+ pkg.nop () desde adentro + 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 () de Java: 0.01 
MEX mexnop (): 0.33 
Construido j (): 0.15 
struct s.foo field access: 0.25 
vacío (persistente): 0.13 

Actualización: R2018a

Aquí están los resultados de R2018a. No es el gran salto que vimos cuando se introdujo el nuevo motor de ejecución en R2015b, pero sigue siendo una mejora apreciable año tras año. En particular, los manejadores de funciones anónimas se volvieron mucho más rápidos.

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

Tiempo de operación (µseg)  
función nop (): 0.03 
subfunción nop (): 0.04 
@ () [] función anónima: 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 
propiedad classdef: 0.13 
propiedad classdef con getter: 0.39 
Función + pkg.nop (): 0.02 
+ pkg.nop () desde adentro + 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 () de Java: 0.03 
MEX mexnop (): 3.54 
incorporado j (): 0.10 
struct s.foo field access: 0.16 
vacío (persistente): 0.07 

Actualización: R2018b y R2019a: sin cambios

No hay cambios significativos. No me estoy molestando en incluir los resultados de la prueba.

Código fuente para puntos de referencia

Puse el código fuente de estos puntos de referencia en GitHub, publicado bajo la Licencia MIT. https://github.com/apjanke/matlab-bench

Andrew Janke
fuente
55
@AndrewJanke ¿Crees que podrías ejecutar el benchmark nuevamente con R2012a? Esto es realmente interesante
Dang Khoa
77
Hola amigos. Si todavía está interesado en el código fuente, lo he reconstruido y lo he abierto en GitHub. github.com/apjanke/matlab-bench
Andrew Janke
2
@Seeda: los métodos estáticos se enumeran como "classdef class.static_nop ()" en estos resultados. Son bastante lentos en comparación con las funciones. Si no se les llama con frecuencia, eso no importa.
Andrew Janke
2
@AndrewJanke Aquí está: gist.github.com/ShakedDovrat/62db9e8f6883c5e28fc0
Shaked
2
¡Guauu! Si esos resultados se mantienen, podría necesitar revisar esta respuesta completa. Adicional. ¡Gracias!
Andrew Janke
3

La clase de identificador tiene una sobrecarga adicional por el seguimiento de todas las referencias a sí misma para fines de limpieza.

Pruebe el mismo experimento sin usar la clase de identificador y vea cuáles son sus resultados.

MikeEL
fuente
1
exactamente el mismo experimento con String, pero ahora como una clase de valor (aunque en otra máquina); atest: 0.009, btest: o.356. Esa es básicamente la misma diferencia que con el identificador, por lo que no creo que el seguimiento de las referencias sea la respuesta clave. Tampoco explica la sobrecarga en funciones vs función en paquetes.
stijn
¿Qué versión de matlab estás usando?
MikeEL
1
He realizado algunas comparaciones similares entre las clases de identificador y valor y no he notado una diferencia de rendimiento entre los dos.
RjOllos
Ya no noto una diferencia tampoco.
MikeEL
Tiene sentido: en Matlab, todos los arreglos, no solo los objetos de manejo, son contados por referencia, porque usan copia en escritura y datos brutos subyacentes compartidos.
Andrew Janke el
1

El rendimiento de OO depende significativamente de la versión de MATLAB utilizada. No puedo comentar sobre todas las versiones, pero sé por experiencia que 2012a ha mejorado mucho más que las versiones de 2010. No hay puntos de referencia y, por lo tanto, no hay números para presentar. Mi código, escrito exclusivamente usando clases de identificador y escrito bajo 2012a, no se ejecutará en absoluto en versiones anteriores.

HG Bruce
fuente
1

En realidad no hay problema con su código pero es un problema con Matlab. Creo que es una especie de juego para parecer. No es más que una sobrecarga para compilar el código de clase. He realizado la prueba con un punto de clase simple (una vez como identificador) y el otro (una vez como clase de valor)

    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

aqui esta la prueba

%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

Los resultados t1 =

12.0212% Mango

t2 =

Valor 12.0042%

t3 =

0.5489  % vector

t4 =

0.0707 % structure 

Por lo tanto, para un rendimiento eficiente, evite usar OOP, en cambio la estructura es una buena opción para agrupar variables

Ahmad
fuente