¿MATLAB OOP es lento o estoy haciendo algo mal?

Estoy experimentando con MATLAB OOP , como principio imité las clases de Logger de C ++ y estoy poniendo todas mis funciones de ayudante de cuerdas en una clase String, pensando que sería genial poder hacer cosas como a + b , a == b , a.find( b ) lugar de strcat( ab ) , strcmp( a, b ) , recupera el primer elemento de strfind( a, b ) , etc.

El problema: desaceleración

Puse las cosas de arriba para usar e inmediatamente noté una ralentización drástica . ¿Lo estoy haciendo mal (lo cual es ciertamente posible ya que tengo bastante limitada la experiencia de MATLAB), o el OOP de MATLAB solo introduce un montón de sobrecarga?

Mi caso de prueba

Esta es la prueba simple que hice para la cadena, básicamente solo añadiendo una cadena y eliminando la parte añadida de nuevo:

 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 = [ ab ]; 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 logger son igualmente: 0.1 segundos para 1000 llamadas a frpintf( 1, 'test\n' ) , 7 (!) Segundos para 1000 llamadas a mi sistema cuando use la clase String internamente (OK, tiene mucho más lógica en ella, pero para comparar con C ++: la sobrecarga de mi sistema que usa std::string( "blah" ) y std::cout en el lado de salida vs plain std::cout << "blah" está en la orden de 1 milisegundo.)

¿Se trata solo de gastos generales cuando se buscan funciones de clase / paquete?

Desde que se interpreta MATLAB, tiene que buscar la definición de una función / objeto en tiempo de ejecución. Así que me preguntaba si quizás se necesita mucha más sobrecarga para buscar funciones de clase o paquete frente a funciones que están en la ruta. Traté de probar esto, y se vuelve más extraño. Para descartar la influencia de las clases / objetos, comparé la invocación de 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 seg, 0.001 seg en ctest

btest 0.060 seg, 0.014 seg en util.ctest

Entonces, ¿todos estos gastos generales provienen de MATLAB pasando tiempo buscando definiciones para su implementación de OOP, mientras que esta sobrecarga no está ahí para las funciones que están directamente en la ruta?

He estado trabajando con OO MATLAB por un tiempo, y terminé viendo problemas de rendimiento similares.

La respuesta corta es: sí, el OOP de MATLAB es un poco lento. Hay una sobrecarga considerable de llamadas a métodos, 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 el idiomático MATLAB usa código “vectorizado” para reducir el número de llamadas a métodos, y la sobrecarga por llamada no es una alta prioridad.

Hice una evaluación comparativa del rendimiento escribiendo funciones “nop” sin función como los diversos tipos de funciones y métodos. Aquí hay algunos resultados típicos.

 >> call_nops
 Computadora: Lanzamiento de PCWIN: 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 seg 1.14 usec por llamada
 C nop (): 0.00001 sec 0.00 usec por llamada

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

El “Java nop ()” es un método de Java do-nothing llamado desde dentro de un bucle de M-código, e incluye la sobrecarga de despacho de MATLAB a Java con cada llamada. “Java nop () de 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 comstackdor inteligente podría optimizar las llamadas completamente.

El mecanismo de determinación del scope del paquete es nuevo, se introdujo 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 syntax obj.nop() es más lenta que la syntax nop(obj) , incluso para el mismo método en un objeto classdef. Lo mismo para objetos Java (no se muestra). Si quieres ir rápido, llama a nop(obj) .
  • La sobrecarga de llamada del método es mayor (aproximadamente 2x) en MATLAB de 64 bits en Windows. (No mostrada.)
  • El envío del método MATLAB es más lento que en otros lenguajes.

Decir por qué esto es así sería solo una especulación de mi parte. Los componentes internos OO del motor MATLAB no son públicos. No se trata de un problema interpretado versus comstackdo per se: MATLAB tiene un JIT, pero la syntax y el tipado más flexible de MATLAB pueden significar más trabajo en tiempo de ejecución. (Por ejemplo, no se puede decir solo por syntax 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 ligadas al estado del sistema de archivos de una manera que muchos otros lenguajes 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 (denominada 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, definen objetos que a su vez son matrices, y hacen que los métodos tomen matrices como entradas y realicen llamadas vectorizadas en los campos y las entradas. Esto reduce la cantidad de llamadas a métodos realizadas, con la esperanza de que la sobrecarga del envío no sea un cuello de botella.

Imitar una clase C ++ o Java en MATLAB probablemente no sea óptimo. Las clases de Java / C ++ normalmente se crean de tal manera que los objetos son los componentes más pequeños, tan específicos como se pueda (es decir, muchas clases diferentes), y se componen en matrices, objetos de colección, etc., y se iteran sobre ellos con bucles. Para hacer clases rápidas de MATLAB, cambia ese enfoque de adentro hacia afuera. Tenga clases más grandes cuyos campos sean matrices, y llame a métodos vectorizados en esas matrices.

El punto es organizar tu código para jugar con los puntos fuertes del lenguaje (manejo de matrices, matemáticas vectorizadas) y evitar los puntos débiles.

EDIT: desde la publicación original, R2010b y R2011a han salido. La imagen general es la misma, las llamadas a MCOS se vuelven un poco más rápidas, y las llamadas a métodos antiguos y de Java se vuelven más lentas .

EDITAR: Solía ​​tener algunas notas aquí sobre “sensibilidad de ruta” con una tabla adicional de tiempos de llamada de función, donde los tiempos de función se veían afectados por la configuración de la ruta de acceso de Matlab, pero parece haber sido una aberración de la 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á desactivado, y la imagen de rendimiento ha cambiado lo suficiente como para actualizar esto.

 Arch: PCWIN Versión: 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 μsec por llamada
 Función nop (): 0.01578 0.16
 nop (), desenrollamiento de bucle 10x: 0.01477 0.15
 nop (), desenrollamiento 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 de clase constante: 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 + paquete: 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 anteriores, siempre que use la syntax foo(obj) . Así que 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 (VMware Virtual Platform)
 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 de clase constante: 4.18 
 propiedad classdef: 1.18 
 propiedad classdef con getter: 19.26 
 Función + pkg.nop (): 4.03 
 + pkg.nop () desde el interior + paquete: 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 
 builtin j (): 0.02 
 estructura en el campo de acceso: 0.14 
 isempty (persistente): 0.00 

Actualización: R2015b: ¡Los objetos se aceleraron!

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 syntax obj.method() es tan rápida como el 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: Core i7-4720HQ CPU a 2.60GHz, 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 de clase constante: 0.25 
 propiedad classdef: 0.25 
 propiedad classdef con getter: 0.64 
 Función + pkg.nop (): 0.04 
 + pkg.nop () desde el interior + paquete: 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 
 builtin j (): 0.15 
 acceso de campo struct s.foo: 0.25 
 isempty (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. Notablemente, los identificadores de funciones anónimas obtuvieron mucho más rápido.

 Matlab R2018a en MACI64  
 Matlab 9.4.0.813654 (R2018a) / Java 1.8.0_144 en Mac OS X 10.13.5 (eilonwy) 
 Máquina: Core i7-3615QM CPU a 2.30 GHz, 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 de clase constante: 0.16 
 propiedad classdef: 0.13 
 propiedad classdef con getter: 0.39 
 Función + pkg.nop (): 0.02 
 + pkg.nop () desde el interior + paquete: 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 
 builtin j (): 0.10 
 estructura en el campo de acceso: 0.16 
 isempty (persistente): 0.07 

Código fuente para los puntos de referencia

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

La clase de control tiene una sobrecarga adicional al rastrear todas las referencias a sí mismo para fines de limpieza.

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

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

En realidad, no hay problema con tu código, pero es un problema con Matlab. Creo que es una especie de juego para verse. No es más que una sobrecarga comstackr el código de clase. He hecho la prueba con un punto de clase simple (una vez como manejador) y la otra (una vez como clase de valor)

  classdef Pointh < handle properties X Y end methods function p = Pointh (x,y) pX = x; pY = y; end function d = dist(p,p1) d = (pX - p1.X)^2 + (pY - p1.Y)^2 ; end end end 

aquí está 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% Manija

t2 =

12.0042% de valor

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