O MATLAB OOP está lento ou estou fazendo algo errado?

144

Eu estou experimentando com MATLAB OOP , como um começo eu imitava o meu C ++ 's aulas Logger e eu estou colocando todas as minhas funções auxiliares de corda em uma classe String, pensando que seria ótimo ser capaz de fazer coisas como a + b, a == b, a.find( b )em vez de strcat( a b ), strcmp( a, b ), recuperar o primeiro elemento de strfind( a, b )etc.

O problema: desaceleração

Coloquei as coisas acima em uso e imediatamente notei uma desaceleração drástica . Estou fazendo errado (o que é certamente possível, pois tenho uma experiência bastante limitada com o MATLAB) ou o POO do MATLAB apenas introduz muita sobrecarga?

Meu caso de teste

Aqui está o teste simples que fiz para a string, basicamente apenas acrescentando uma string e removendo a parte anexada novamente:

Nota: Na verdade, não escreva uma classe String como essa em código real! O Matlab agora tem um stringtipo de matriz nativa , e você deve usá-lo.

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

Os resultados

Tempo total em segundos, para 1000 iterações:

btest 0,550 (com String.SetLength 0,138, String.plus 0,065, String.Length 0,057)

atest 0,015

Os resultados para o sistema de logger também são: 0,1 segundos para 1000 chamadas para frpintf( 1, 'test\n' ), 7 (!) Segundos para 1000 chamadas para o meu sistema ao usar a classe String internamente (OK, ele possui muito mais lógica, mas para comparar com C ++: a sobrecarga do meu sistema que usa std::string( "blah" )e std::coutno lado da saída vs simples std::cout << "blah"é da ordem de 1 milissegundo.)

É uma sobrecarga ao procurar funções de classe / pacote?

Como o MATLAB é interpretado, ele deve procurar a definição de uma função / objeto em tempo de execução. Então, eu queria saber que talvez haja muito mais sobrecarga na procura de funções de classe ou pacote vs funções que estão no caminho. Eu tentei testar isso, e isso fica mais estranho. Para descartar a influência de classes / objetos, comparei a chamada de uma função no caminho versus uma função em um pacote:

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 da mesma forma que acima:

atest 0,004 s, 0,001 s no ctest

btest 0,060 s, 0,014 s em util.ctest

Então, toda essa sobrecarga é proveniente do MATLAB gastando tempo procurando definições para sua implementação OOP, enquanto essa sobrecarga não existe para funções diretamente no caminho?

stijn
fonte
5
Obrigado por esta pergunta! O desempenho do heap do Matlab (OOP / closures) me incomoda há anos, consulte stackoverflow.com/questions/1446281/matlabs-garbage-collector . Estou realmente curioso para saber o que MatlabDoug / Loren / MikeKatz responderá à sua postagem.
11559 Mikhail
1
Essa foi uma leitura interessante.
Stijn
1
@ MatlabDoug: talvez o seu colega Mike Karr possa comentar OP?
10169 Mikhail
4
Os leitores devem também verificar este post recente (por Dave Foti) discutindo desempenho OOP na última versão R2012a: Considerando o desempenho no código MATLAB Orientada a Objetos
Amro
1
Um exemplo simples da sensibilidade na estrutura de código na qual a chamada de métodos de subelementos é retirada do loop. for i = 1:this.get_n_quantities() if(strcmp(id,this.get_quantity_rlz(i).get_id())) ix = i; end endleva 2.2 segundos, enquanto 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, duas ordens de mag
José Ospina

Respostas:

223

Trabalho com o OO MATLAB há um tempo e acabei analisando problemas de desempenho semelhantes.

A resposta curta é: sim, o POO do MATLAB é meio lento. Existe uma sobrecarga substancial de chamadas de método, mais alta que as linguagens OO convencionais, e não há muito o que fazer sobre isso. Parte do motivo pode ser que o MATLAB idiomático use código "vetorizado" para reduzir o número de chamadas de método, e a sobrecarga por chamada não é uma alta prioridade.

Avaliei o desempenho escrevendo funções "nop" do-nothing como os vários tipos de funções e métodos. Aqui estão alguns resultados típicos.

>> call_nops
Computador: PCWIN Lançamento: 2009b
Chamando cada função / método 100000 vezes
Função nop (): 0.02261 seg 0.23 usec por chamada
Funções nop1-5 (): 0,02182 seg 0,22 usec por chamada
subfunção nop (): 0.02244 seg 0.22 usec por chamada
@ () [] função anônima: 0,08461 seg 0,85 usec por chamada
método nop (obj): 0,24664 seg 2,47 usec por chamada
métodos nop1-5 (obj): 0.23469 seg. 2,35 usec por chamada
Função privada nop (): 0.02197 seg 0.22 usec por chamada
classdef nop (obj): 0.90547 seg 9.05 usec por chamada
classdef obj.nop (): 1.75522 seg 17,55 usec por chamada
classdef private_nop (obj): 0.84738 seg 8.47 usec por chamada
classdef nop (obj) (arquivo m): 0.90560 seg 9.06 usec por chamada
classdef class.staticnop (): 1.16361 seg 11,64 usec por chamada
Java nop (): 2.43035 seg 24,30 usec por chamada
Java static_nop (): 0,87682 s 8,77 usec por chamada
Java nop () de Java: 0.00014 seg 0.00 usec por chamada
MEX mexnop (): 0.11409 seg 1,14 usec por chamada
C nop (): 0.00001 seg 0.00 usec por chamada

Resultados semelhantes no R2008a a R2009b. Este é no Windows XP x64 executando o MATLAB de 32 bits.

O "Java nop ()" é um método Java do-nothing chamado de dentro de um loop de código M e inclui a sobrecarga de despacho MATLAB para Java a cada chamada. "Java nop () de Java" é a mesma coisa chamada em um loop Java for () e não incorre nessa penalidade de limite. Faça os intervalos de Java e C com um grão de sal; um compilador inteligente pode otimizar completamente as chamadas.

O mecanismo de escopo do pacote é novo, introduzido aproximadamente ao mesmo tempo que as classes classdef. Seu comportamento pode estar relacionado.

Algumas conclusões provisórias:

  • Os métodos são mais lentos que as funções.
  • Os novos métodos de estilo (classdef) são mais lentos que os métodos de estilo antigo.
  • A nova obj.nop()sintaxe é mais lenta que a nop(obj)sintaxe, mesmo para o mesmo método em um objeto classdef. O mesmo para objetos Java (não mostrado). Se você quiser ir mais rápido, ligue nop(obj).
  • A sobrecarga de chamada de método é maior (cerca de 2x) no MATLAB de 64 bits no Windows. (Não mostrado.)
  • O envio do método MATLAB é mais lento que em outros idiomas.

Dizer por que isso é assim seria apenas especulação da minha parte. Os internos de OO do mecanismo MATLAB não são públicos. Não é um problema interpretado versus compilado em si - o MATLAB possui um JIT -, mas a tipagem e sintaxe mais frouxas do MATLAB podem significar mais trabalho em tempo de execução. (Por exemplo, você não pode distinguir apenas da sintaxe "f (x)" é uma chamada de função ou um índice em uma matriz; depende do estado da área de trabalho no tempo de execução.) Pode ser porque as definições de classe do MATLAB estão vinculadas ao estado do sistema de arquivos de uma maneira que muitas outras línguas não o são.

Então o que fazer?

Uma abordagem idiomática do MATLAB para isso é "vetorizar" seu código estruturando suas definições de classe de modo que uma instância de objeto envolva uma matriz; isto é, cada um de seus campos contém matrizes paralelas (denominadas organização "planar" na documentação do MATLAB). Em vez de ter uma matriz de objetos, cada um com campos contendo valores escalares, define objetos que são matrizes e os métodos tomam matrizes como entradas e fazem chamadas vetorizadas nos campos e entradas. Isso reduz o número de chamadas de método feitas, espero que o gasto adicional da expedição não seja um gargalo.

Imitar uma classe C ++ ou Java no MATLAB provavelmente não será o ideal. As classes Java / C ++ são tipicamente construídas de modo que os objetos sejam os menores blocos de construção, o mais específico possível (ou seja, muitas classes diferentes), e você os compõe em matrizes, objetos de coleção etc. e itera sobre eles com loops. Para fazer aulas rápidas do MATLAB, vire essa abordagem do avesso. Tenha classes maiores cujos campos são matrizes e chame métodos vetorizados nessas matrizes.

O objetivo é organizar seu código para que ele atenda aos pontos fortes da linguagem - manipulação de array, matemática vetorizada - e evite os pontos fracos.

EDIT: Desde o post original, R2010b e R2011a foram lançados. A imagem geral é a mesma, com as chamadas do MCOS ficando um pouco mais rápidas e as chamadas do Java e do método antigo ficando mais lentas .

EDIT: Eu costumava fazer algumas anotações aqui sobre "sensibilidade do caminho" com uma tabela adicional de tempos de chamada de função, onde os tempos da função eram afetados pela forma como o caminho do Matlab foi configurado, mas isso parece ter sido uma aberração da minha configuração de rede específica em A Hora. O gráfico acima reflete os horários típicos da preponderância dos meus testes ao longo do tempo.

Atualização: R2011b

EDIT (13/2/2012): O R2011b foi lançado e a imagem de desempenho mudou o suficiente para atualizar isso.

Arco: PCWIN Versão: 2011b 
Máquina: R2011b, Windows XP, 8x Core i7-2600 a 3.40GHz, 3 GB de RAM, NVIDIA NVS 300
Fazendo cada operação 100000 vezes
estilo µsec total por chamada
Função nop (): 0,01578 0,16
nop (), desenrolamento de loop 10x: 0,01477 0,15
nop (), desenrolamento de loop de 100x: 0,01518 0,15
subfunção nop (): 0,01559 0,16
@ () [] função anônima: 0,06400 0,64
método nop (obj): 0,28482 2,85
Função 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
propriedade classdef: 0.12992 1.30
propriedade classdef com getter: 1.39912 13.99
+ função pkg.nop (): 0.87345 8.73
+ pkg.nop () de dentro + 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 () (interno): 0,00251 0,03

Eu acho que o resultado disso é o seguinte:

  • Os métodos MCOS / classdef são mais rápidos. O custo agora está em pé de igualdade com as classes de estilo antigo, desde que você use a foo(obj)sintaxe. Portanto, a velocidade do método não é mais um motivo para seguir as classes de estilo antigo na maioria dos casos. (Parabéns, MathWorks!)
  • Colocar funções nos espaços para nome os torna lentos. (Não é novo no R2011b, apenas é novo no meu teste.)

Atualização: R2014a

Eu reconstruí o código de benchmarking e o executei no R2014a.

Matlab R2014a em PCWIN64  
Matlab 8.3.0.532 (R2014a) / Java 1.7.0_11 no 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 

Tempo de operação (µsec)  
Função nop (): 0.14 
subfunção nop (): 0,14 
@ () [] função anônima: 0,69 
método nop (obj): 3,28 
nop () private fcn em @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 
propriedade classdef: 1,18 
propriedade classdef com getter: 19.26 
+ função pkg.nop (): 4.03 
+ pkg.nop () de dentro + 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 
Construído em j (): 0.02 
acesso ao campo struct s.foo: 0,14 
isempty (persistente): 0,00 

Atualização: R2015b: os objetos ficaram mais rápidos!

Aqui estão os resultados do R2015b, gentilmente fornecidos por @Shaked. Essa é uma grande mudança: OOP é significativamente mais rápido, e agora a obj.method()sintaxe é tão rápida quanto method(obj)e muito mais rápida que os objetos OOP herdados.

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

Tempo de operação (µsec)  
Função nop (): 0.04 
subfunção nop (): 0,08 
@ () [] função anônima: 1,83 
método nop (obj): 3.15 
nop () private fcn em @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 
propriedade classdef: 0.25 
propriedade classdef com getter: 0,64 
+ função pkg.nop (): 0.04 
+ pkg.nop () de dentro + 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 
Construído em j (): 0.15 
acesso ao campo struct s.foo: 0,25 
isempty (persistente): 0,13 

Atualização: R2018a

Aqui estão os resultados do R2018a. Não é o grande salto que vimos quando o novo mecanismo de execução foi introduzido no R2015b, mas ainda é uma melhoria apreciável ano após ano. Notavelmente, identificadores de funções anônimas ficaram muito mais rápidos.

Matlab R2018a em MACI64  
Matlab 9.4.0.813654 (R2018a) / Java 1.8.0_144 no MACI64 Mac OS X 10.13.5 (eilonwy) 
Máquina: CPU Core i7-3615QM a 2,30 GHz, 16 GB RAM 
nIters = 100000 

Tempo de operação (µsec)  
Função nop (): 0.03 
subfunção nop (): 0,04 
@ () [] função 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 
propriedade classdef: 0.13 
propriedade classdef com getter: 0,39 
+ função pkg.nop (): 0.02 
+ pkg.nop () de dentro + 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 
Construído em j (): 0.10 
acesso ao campo struct s.foo: 0,16 
isempty (persistente): 0,07 

Atualização: R2018b e R2019a: nenhuma alteração

Sem alterações significativas. Não estou me incomodando em incluir os resultados do teste.

Código Fonte para Benchmarks

Coloquei o código-fonte desses benchmarks no GitHub, lançado sob a licença MIT. https://github.com/apjanke/matlab-bench

Andrew Janke
fonte
5
@AndrewJanke Você acha que poderia executar o benchmark novamente com o R2012a? Isso é realmente interessante.
Dang Khoa
7
Oi pessoal. Se você ainda está interessado no código-fonte, eu o reconstruí e o código-fonte aberto no GitHub. github.com/apjanke/matlab-bench
Andrew Janke
2
@Seeda: métodos estáticos são listados como "classdef class.static_nop ()" nesses resultados. Eles são bastante lentos se comparados às funções. Se não forem chamados com frequência, isso não importa.
Andrew Janke
2
@AndrewJanke Aqui está: gist.github.com/ShakedDovrat/62db9e8f6883c5e28fc0
Shaked
2
Uau! Se esses resultados persistirem, talvez seja necessário revisar toda a resposta. Adicionado. Obrigado!
Andrew Janke 24/09
3

A classe handle tem uma sobrecarga adicional ao rastrear todas as referências a si mesma para fins de limpeza.

Experimente o mesmo experimento sem usar a classe handle e veja quais são seus resultados.

MikeEL
fonte
1
exatamente o mesmo experimento com String, mas agora como uma classe de valor (em outra máquina); atest: 0,009, btest: o.356. Essa é basicamente a mesma diferença que com o identificador, então não acho que rastrear referências seja a resposta principal. Também não explica a sobrecarga em funções versus função em pacotes.
Stijn
Qual versão do matlab você está usando?
MikeEL
1
Fiz algumas comparações semelhantes entre identificador e classes de valor e não notei uma diferença de desempenho entre os dois.
RjOllos
Também não percebo diferença.
MikeEL
Faz sentido: no Matlab, todas as matrizes, e não apenas os objetos, são contadas como referência, porque usam cópia em gravação e dados brutos subjacentes compartilhados.
Andrew Janke
1

O desempenho do OO depende significativamente da versão do MATLAB usada. Não posso comentar em todas as versões, mas sei por experiência que 2012a foi muito aprimorado em relação às versões de 2010. Sem referências e, portanto, sem números para apresentar. Meu código, escrito exclusivamente usando classes de identificador e escrito em 2012a, não será executado nas versões anteriores.

HG Bruce
fonte
1

Na verdade, não há problema com o seu código, mas é um problema com o Matlab. Eu acho que é uma espécie de brincadeira para parecer. Não é nada além de sobrecarga para compilar o código da classe. Eu fiz o teste com ponto de classe simples (uma vez como identificador) e o outro (uma vez como classe 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 está o teste

%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

Os resultados t1 =

12.0212% Alça

t2 =

12.0042% valor

t3 =

0.5489  % vector

t4 =

0.0707 % structure 

Portanto, para obter um desempenho eficiente, evite usar OOP. A estrutura é uma boa opção para agrupar variáveis

Ahmad
fonte