¿MATLAB OOP es lento o estoy haciendo algo mal?
profiling benchmarking (4)
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.
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 = [ 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 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?
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 compilar 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)
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
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
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 Computer: PCWIN Release: 2009b Calling each function/method 100000 times nop() function: 0.02261 sec 0.23 usec per call nop1-5() functions: 0.02182 sec 0.22 usec per call nop() subfunction: 0.02244 sec 0.22 usec per call @()[] anonymous function: 0.08461 sec 0.85 usec per call nop(obj) method: 0.24664 sec 2.47 usec per call nop1-5(obj) methods: 0.23469 sec 2.35 usec per call nop() private function: 0.02197 sec 0.22 usec per call classdef nop(obj): 0.90547 sec 9.05 usec per call classdef obj.nop(): 1.75522 sec 17.55 usec per call classdef private_nop(obj): 0.84738 sec 8.47 usec per call classdef nop(obj) (m-file): 0.90560 sec 9.06 usec per call classdef class.staticnop(): 1.16361 sec 11.64 usec per call Java nop(): 2.43035 sec 24.30 usec per call Java static_nop(): 0.87682 sec 8.77 usec per call Java nop() from Java: 0.00014 sec 0.00 usec per call MEX mexnop(): 0.11409 sec 1.14 usec per call C nop(): 0.00001 sec 0.00 usec per call
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 compilador inteligente podría optimizar las llamadas completamente.
El mecanismo de determinación del alcance 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 sintaxis
obj.nop()
es más lenta que la sintaxisnop(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 anop(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 compilado per se: MATLAB tiene un JIT, pero la sintaxis 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 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 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 Release: 2011b Machine: R2011b, Windows XP, 8x Core i7-2600 @ 3.40GHz, 3 GB RAM, NVIDIA NVS 300 Doing each operation 100000 times style total µsec per call nop() function: 0.01578 0.16 nop(), 10x loop unroll: 0.01477 0.15 nop(), 100x loop unroll: 0.01518 0.15 nop() subfunction: 0.01559 0.16 @()[] anonymous function: 0.06400 0.64 nop(obj) method: 0.28482 2.85 nop() private function: 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 constant: 1.51890 15.19 classdef property: 0.12992 1.30 classdef property with getter: 1.39912 13.99 +pkg.nop() function: 0.87345 8.73 +pkg.nop() from inside +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() from Java: 0.00010 0.00 MEX mexnop(): 0.08709 0.87 C nop(): 0.00001 0.00 j() (builtin): 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 sintaxis
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 on PCWIN64 Matlab 8.3.0.532 (R2014a) / Java 1.7.0_11 on PCWIN64 Windows 7 6.1 (eilonwy-win7) Machine: Core i7-3615QM CPU @ 2.30GHz, 4 GB RAM (VMware Virtual Platform) nIters = 100000 Operation Time (µsec) nop() function: 0.14 nop() subfunction: 0.14 @()[] anonymous function: 0.69 nop(obj) method: 3.28 nop() private fcn on @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 constant: 4.18 classdef property: 1.18 classdef property with getter: 19.26 +pkg.nop() function: 4.03 +pkg.nop() from inside +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() from Java: 0.01 MEX mexnop(): 0.91 builtin j(): 0.02 struct s.foo field access: 0.14 isempty(persistent): 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 sintaxis obj.method()
es tan rápida como el method(obj)
y mucho más rápida que los objetos OOP heredados.
Matlab R2015b on PCWIN64 Matlab 8.6.0.267246 (R2015b) / Java 1.7.0_60 on PCWIN64 Windows 8 6.2 (nanit-shaked) Machine: Core i7-4720HQ CPU @ 2.60GHz, 16 GB RAM (20378) nIters = 100000 Operation Time (µsec) nop() function: 0.04 nop() subfunction: 0.08 @()[] anonymous function: 1.83 nop(obj) method: 3.15 nop() private fcn on @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 constant: 0.25 classdef property: 0.25 classdef property with getter: 0.64 +pkg.nop() function: 0.04 +pkg.nop() from inside +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() from Java: 0.01 MEX mexnop(): 0.33 builtin j(): 0.15 struct s.foo field access: 0.25 isempty(persistent): 0.13
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.