Estou muito interessado em otimizar a solução do sistema linear para matrizes pequenas (10x10), às vezes chamadas de matrizes minúsculas . Existe uma solução pronta para isso? A matriz pode ser considerada não singular.
Esse solucionador deve ser executado em excesso de 1 000 000 vezes em microssegundos em uma CPU Intel. Estou falando do nível de otimização usado em jogos de computador. Independentemente de codificá-lo em montagem e arquitetura específica, ou estudar reduções de trocas de precisão ou confiabilidade e usar hacks de ponto flutuante (eu uso o sinalizador de compilação -ffast-math, não há problema). A solução pode até falhar por cerca de 20% do tempo!
O parcialPivLu da Eigen é o mais rápido no meu benchmark atual, superando o LAPACK quando otimizado com -O3 e um bom compilador. Mas agora estou no ponto de criar manualmente um solucionador linear personalizado. Qualquer conselho seria muito apreciado. Farei minha solução de código aberto e conheço informações importantes em publicações, etc.
Relacionado: Velocidade de resolução de sistemas lineares com matriz diagonal de blocos Qual é o método mais rápido para inverter milhões de matrizes? https://stackoverflow.com/q/50909385/1489510
Respostas:
O uso de um tipo de matriz Eigen em que o número de linhas e colunas é codificado no tipo em tempo de compilação fornece uma vantagem sobre o LAPACK, onde o tamanho da matriz é conhecido apenas em tempo de execução. Essas informações extras permitem ao compilador desenrolar total ou parcialmente o loop, eliminando muitas instruções de ramificação. Se você estiver olhando para usar uma biblioteca existente em vez de escrever seus próprios kernels, provavelmente será essencial ter um tipo de dados em que o tamanho da matriz possa ser incluído como parâmetros do modelo C ++. A única outra biblioteca que conheço que faz isso é a chama , de modo que talvez valha a pena comparar o Eigen.
Se você decidir implementar sua própria implementação, poderá descobrir o que o PETSc faz para o formato CSR de bloco ser um exemplo útil, embora o próprio PETSc provavelmente não seja a ferramenta certa para o que você tem em mente. Em vez de escrever um loop, eles escrevem todas as operações para pequenos vetores matriciais explicitamente (veja este arquivo em seu repositório). Isso garante que não há instruções de ramificação, como você pode obter com um loop. As versões do código com instruções AVX são um bom exemplo de como realmente usar extensões de vetor. Por exemplo, esta função usa o
__m256d
tipo de dados para operar simultaneamente em quatro duplos ao mesmo tempo. Você pode obter um aumento considerável no desempenho escrevendo explicitamente todas as operações usando extensões de vetor, apenas para fatoração LU em vez de multiplicar vetor matriz. Em vez de realmente escrever o código C manualmente, seria melhor usar um script para gerá-lo. Também pode ser divertido verificar se há uma diferença significativa de desempenho quando você reordena algumas das operações para aproveitar melhor o pipelining de instruções.Você também pode obter alguma milhagem da ferramenta STOKE , que explorará aleatoriamente o espaço de possíveis transformações de programas para encontrar uma versão mais rápida.
fonte
Outra idéia poderia ser usar uma abordagem generativa (um programa escrevendo um programa). Crie um (meta) programa que cite a sequência de instruções C / C ++ para executar LU ** não dinâmica em um sistema 10x10. Basicamente, pegue o ninho de loop k / i / j e aplaná-lo em O (1000) ou mais linhas de aritmética escalar. Em seguida, alimente esse programa gerado para qualquer compilador otimizador. O que eu acho interessante aqui, é remover os loops que expõem todas as dependências de dados e subexpressão redundante e oferece ao compilador a oportunidade máxima de reordenar as instruções para que elas mapeiem bem o hardware real (por exemplo, número de unidades de execução, perigos / paradas, portanto em).
Se você conhecer todas as matrizes (ou apenas algumas delas), poderá melhorar a taxa de transferência chamando as funções / intrínsecas do SIMD (SSE / AVX) em vez do código escalar. Aqui você exploraria o paralelismo embaraçoso entre as instâncias, em vez de perseguir qualquer paralelismo dentro de uma única instância. Por exemplo, você pode executar quatro LUs de precisão dupla simultaneamente usando intrínsecas AVX256, agrupando 4 matrizes "em todo o registro" e executando as mesmas operações ** em todas elas.
** Daí o foco na LU não dinâmica. A articulação estraga essa abordagem de duas maneiras. Primeiro, ele introduz ramificações devido à seleção dinâmica, o que significa que suas dependências de dados não são tão conhecidas. Segundo, significa que diferentes "slots" do SIMD teriam que fazer coisas diferentes, porque a instância A pode girar de maneira diferente da instância B. Portanto, se você prosseguir com isso, sugiro rodar estaticamente suas matrizes antes do cálculo (permita a maior entrada de cada coluna para diagonal).
fonte
Sua pergunta leva a duas considerações diferentes.
Primeiro, você precisa escolher o algoritmo certo. Portanto, a questão de saber se as matrizes têm alguma estrutura deve ser considerada. Por exemplo, quando as matrizes são simétricas, uma decomposição de Cholesky é mais eficiente que a LU. Quando você precisa apenas de uma quantidade limitada de precisão, um método iterativo pode ser mais rápido.
Segundo, você precisa implementar o algoritmo com eficiência. Para fazer isso, você precisa conhecer o gargalo do seu algoritmo. A sua implementação está vinculada à velocidade da transferência de memória ou à velocidade da computação. Como você considera apenas10 × 10 matrizes, sua matriz deve caber completamente no cache da CPU. Portanto, você deve usar as unidades SIMD (SSE, AVX etc.) e os núcleos do seu processador para fazer o maior número possível de cálculos por ciclo.
Ao todo, a resposta à sua pergunta depende muito do hardware e das matrizes que você considera. Provavelmente não existe uma resposta definitiva e você deve tentar algumas coisas para encontrar um método ideal.
fonte
Eu tentaria inversão em blocos.
https://en.wikipedia.org/wiki/Invertible_matrix#Blockwise_inversion
Eigen usa uma rotina otimizada para calcular o inverso de uma matriz 4x4, que é provavelmente o melhor que você obterá. Tente usar isso o máximo possível.
http://www.eigen.tuxfamily.org/dox/Inverse__SSE_8h_source.html
Superior esquerdo: 8x8. Superior direito: 8x2. Parte inferior esquerda: 2x8. Em baixo à direita: 2x2. Inverta o 8x8 usando o código de inversão 4x4 otimizado. O resto são produtos matriciais.
EDIT: O uso de blocos 6x6, 6x4, 4x6 e 4x4 mostrou ser um pouco mais rápido do que o que descrevi acima.
Aqui estão os resultados de uma execução de benchmark usando um milhão de
Eigen::Matrix<double,10,10>::Random()
matrizes eEigen::Matrix<double,10,1>::Random()
vetores. Em todos os meus testes, meu inverso é sempre mais rápido. Minha rotina de resolução envolve calcular o inverso e depois multiplicá-lo por um vetor. Às vezes é mais rápido que Eigen, às vezes não. Meu método de marcação de bancada pode ter falhas (não desativou o turbo boost, etc.). Além disso, as funções aleatórias de Eigen podem não representar dados reais.Estou muito interessado em ver se alguém pode otimizar isso ainda mais, pois tenho um aplicativo de elementos finitos que inverte um zilhão de matrizes 10x10 (e sim, preciso de coeficientes individuais do inverso, portanto, resolver diretamente um sistema linear nem sempre é uma opção) .
fonte