Estou usando o Tatu para fazer multiplicações de matriz muito intensivas com comprimentos laterais , onde pode ser de até 20 ou mais. Estou usando o Armadillo com o OpenBLAS para multiplicação de matrizes, o que parece estar fazendo um trabalho muito bom em núcleos paralelos, exceto que tenho um problema com o formalismo da multiplicação no Armadillo para super otimização de desempenho. n
Digamos que eu tenho um loop no seguinte formato:
arma::cx_mat stateMatrix, evolutionMatrix; //armadillo complex matrix type
for(double t = t0; t < t1; t += 1/sampleRate)
{
...
stateMatrix = evolutionMatrix*stateMatrix;
...
}
No C ++ fundamental, acho que o problema aqui é que o C ++ alocará um novo objeto de cx_mat
para armazenar evolutionMatrix*stateMatrix
e, em seguida, copie o novo objeto para stateMatrix
with operator=()
. Isso é muito, muito ineficiente. É sabido que retornar classes complexas de grandes tipos de dados é uma má ideia, certo?
A maneira como vejo isso indo de maneira mais eficiente é com uma função que faz a multiplicação na forma:
void multiply(const cx_mat& mat1, const cx_mat& mat2, cx_mat& output)
{
... //multiplication of mat1 and mat2 and then store it in output
}
Dessa forma, não é necessário copiar objetos enormes com valor de retorno, e a saída não precisa ser realocada a cada multiplicação.
A pergunta : como posso encontrar um compromisso, no qual eu possa usar o Armadillo para multiplicação com sua bela interface do BLAS, e fazer isso de forma eficiente sem precisar recriar objetos de matriz e copiá-los a cada operação?
Isso não é um problema de implementação no Tatu?
fonte
stateMatrix = evolutionMatrix*stateMatrix
não fará nenhuma cópia. Em vez disso, o Armadillo faz uma alteração elegante no ponteiro da memória. A nova memória para o resultado ainda será alocada (não há como contornar isso), mas, em vez de copiar, astateMatrix
matriz simplesmente usará a nova memória e descartará a memória antiga.Respostas:
Acho que você está certo ao criar temporários, o que é muito lento, mas acho que a razão pela qual está fazendo isso está errada.
O Tatu, como qualquer boa biblioteca de álgebra linear C ++, usa modelos de expressão para implementar a avaliação atrasada de expressões. Quando você anota um produto da matriz como
A*B
, nenhum temporário é criado. Em vez disso, o Armadillo cria um objeto temporário (x
) que mantém referênciasA
eB
, posteriormente, dada uma expressão comoC = x
, calcula o produto da matriz armazenando o resultado diretamenteC
, sem criar nenhum temporários.Ele também usa essa otimização para lidar com expressões como
A*B*C*D
, onde, dependendo do tamanho da matriz, certas ordens de multiplicação são mais eficientes que outras.Se o Armadillo não estiver executando essa otimização, isso seria um bug no Armadillo que deveria ser relatado aos desenvolvedores.
No entanto, no seu caso, há outro problema que é mais importante. Em uma expressão como
A=B*C
o armazenamento deA
não contém nenhum dado de entrada, seA
não houver aliasB
ouC
. No seu caso,A = A*B
escrever qualquer coisa na matriz de saída também modificaria uma das matrizes de entrada.Mesmo com a função sugerida
como exatamente essa função ajudaria na expressão
multiply(A, B, A)
? Para implementações mais comuns dessa função, isso levaria a um bug. Seria necessário usar algum armazenamento temporário por conta própria, para garantir que seus dados de entrada não estejam corrompidos. Sua sugestão é basicamente como o Armadillo já implementa a multiplicação de matrizes, mas acho que provavelmente tomamos o cuidado de evitar situações como amultiply(A, B, A)
alocação temporária.A explicação mais provável do motivo pelo qual o Armadillo não está fazendo essa otimização é que seria incorreto fazer isso.
Por fim, existe uma maneira muito mais simples de fazer o que você deseja, assim:
Isso é idêntico ao
mas aloca uma matriz temporária, em vez de uma matriz temporária por iteração.
fonte
new
-inicializarAtemp
- você não ganha nada: ainda envolve gerar uma nova matriz temporária(*A)*B
e copiá-la*Atemp
, a menos que o RVO a impeça.(*A)*B
é uma matriz temporária, mas um objeto de expressão que controla a expressão e suas entradas. Tentei explicar por que essa otimização não é acionada no exemplo original e não tem nada a ver com o RVO (ou mover a semântica como em outra resposta). Eu pulei todo o código de inicialização, não é importante no exemplo, apenas mostrei os tipos.swap
para que você não precise fazer esse tipo de malabarismo com ponteiros.O @BillGreene aponta para a "otimização do valor de retorno" como uma maneira de contornar o problema fundamental, mas isso na verdade ajuda apenas a metade dele. Suponha que você tenha um código deste formulário:
Um compilador ingênuo irá
A otimização do valor de retorno pode unificar apenas o objeto 'tmp' e o slot 'result', mas não remove a necessidade de uma cópia. Portanto, você ainda fica com a criação de um temporário, a operação de cópia e a destruição de um temporário.
A única maneira de contornar isso é o operador + não retorna um objeto, mas um objeto de alguma classe intermediária que, quando atribuído a um
ExpensiveObject
, realiza a operação de adição e cópia. Essa é a abordagem típica usada nas bibliotecas de modelos de expressão .fonte
Stackoverflow ( https://stackoverflow.com/ ) é provavelmente um fórum de discussão melhor para esta pergunta. No entanto, aqui está uma resposta curta.
Duvido que o compilador C ++ esteja gerando código para esta expressão como você descreveu acima. Todos os compiladores C ++ modernos implementam uma otimização chamada "otimização do valor de retorno" ( http://en.wikipedia.org/wiki/Return_value_optimization ). Com a otimização do valor de retorno, o resultado de
evolutionMatrix*stateMatrix
é armazenado diretamentestateMatrix
; nenhuma cópia é feita.Obviamente, há uma considerável confusão sobre esse tópico e esse é um dos motivos pelos quais sugeri que o Stackoverflow poderia ser um fórum melhor. Existem muitos "advogados de linguagem" em C ++ lá, enquanto a maioria de nós aqui prefere gastar nosso tempo no CSE. ;-)
Criei o seguinte exemplo simples com base no post do professor Bangerth:
Parece mais complicado do que realmente é porque eu queria remover completamente todo o código da impressão ao compilar no modo otimizado. Quando executo a versão compilada com uma opção de depuração, obtenho a seguinte saída:
A primeira coisa a notar é que nenhum temporário é construído - apenas a, bec. O construtor padrão e o operador de atribuição nunca são chamados porque não são necessários neste exemplo.
O professor Bangerth mencionou modelos de expressão. De fato, essa técnica de otimização é muito importante para obter um bom desempenho em uma biblioteca de classes matriciais. Mas é importante apenas quando as expressões de objeto são mais complicadas do que simplesmente a + b. Se, por exemplo, meu teste fosse:
Eu obteria a seguinte saída:
Este caso mostra a construção indesejável de um temporário (5 chamadas para o construtor e duas chamadas do operador +). O uso adequado de modelos de expressão (um tópico muito além do escopo deste fórum) impediria isso temporariamente. (Para os altamente motivados, uma discussão particularmente legível dos modelos de expressão pode ser encontrada no capítulo 18 de http://www.amazon.com/C-Templates-The-Complete-Guide/dp/0201734842 ).
Finalmente, a verdadeira "prova" do que o compilador está realmente fazendo vem do exame do código de montagem gerado pelo compilador. Para o primeiro exemplo, quando compilado no modo otimizado, esse código é surpreendentemente simples. Todas as chamadas de funções foram otimizadas e o código de montagem carrega essencialmente 2 em um registro, 3 em um segundo e as adiciona.
fonte
malloc
efree
e o compilador não pode otimizar afastado pares deles sem tropeçar até monitores de memória etc.Ou seja, a menos que você incorra em uma constante enorme na cópia - o que na verdade não é tão exagerado, porque a versão com cópia é muito mais cara em outro aspecto: ela precisa de muito mais memória. Portanto, se você precisar trocar de e para o disco rígido, a cópia poderá se tornar um gargalo. No entanto, mesmo se você não copiar nada, um algoritmo fortemente paralelizado poderá fazer algumas cópias por conta própria. Realmente, a única maneira de garantir que não seja usada muita memória em cada etapa é dividir a multiplicação em colunas de
stateMatrix
, para que apenas pequenas multiplicações sejam feitas por vez. Por exemplo, você pode definirVocê também deve considerar se precisa evoluir isso
stateMatrix
como um em primeiro lugar. Se você basicamente quer apenas uma evolução no tempo independente dosn
kets de estado, pode evoluí-los um por um, o que é muito menos dispendioso em termos de memória. Em particular, seevolutionMatrix
for escasso , você definitivamente deve conferir! Pois isso é basicamente apenas um hamiltoniano, não é? Os hamiltonianos são frequentemente esparsos ou aproximadamente esparsos.fonte
O C ++ moderno tem uma solução para o problema usando "mover construtores" e "referências de valor".
Um "mover construtor" é um construtor para uma classe, por exemplo, uma classe matricial, que pega outra instância da mesma classe e move os dados da outra instância para a nova instância, deixando a instância original vazia. Normalmente, um objeto de matriz terá dois números para o tamanho e um ponteiro para os dados. Onde um construtor normal duplicaria os dados, um construtor de movimentação copiará apenas os dois números e o ponteiro, portanto isso é muito rápido.
Uma "referência de valor", escrita por exemplo como "matriz &&" em vez da "matriz &" usual é usada para variáveis temporárias. Você declararia uma multiplicação de matriz como retornando uma matriz &&. Ao fazer isso, o compilador garantirá que um construtor de movimentação muito barato seja usado para obter o resultado da função que o chama. Portanto, uma expressão como resultado = (a + b) * (c + d) onde a, b, c, d são enormes objetos matriciais, ocorrerá sem nenhuma cópia.
Ao pesquisar no Google "referências de rvalue e mover construtores", você encontrará exemplos e tutoriais.
fonte
Então, novamente, entendo que o OpenBLAS tem uma coleção maior de otimizações específicas da arquitetura, portanto o Eigen pode ou não ser uma vitória para você. Infelizmente, não existe uma biblioteca de álgebra linear tão impressionante que você nem precise considerar os outros ao lutar pelos "últimos 10%" do desempenho. Os invólucros não são uma solução 100%; a maioria (todos?) deles não pode tirar proveito da capacidade da eigen de mesclar cálculos dessa maneira.
fonte