Eu tenho a representação da coluna esparsa compactada (csc) da matriz triangular inferior nxn A com zeros na diagonal principal e gostaria de resolver b em
(A + I)' * x = b
Esta é a rotina que tenho para calcular isso:
void backsolve(const int*__restrict__ Lp,
const int*__restrict__ Li,
const double*__restrict__ Lx,
const int n,
double*__restrict__ x) {
for (int i=n-1; i>=0; --i) {
for (int j=Lp[i]; j<Lp[i+1]; ++j) {
x[i] -= Lx[j] * x[Li[j]];
}
}
}
Assim, b
é passado através do argumento x
e é substituído pela solução. Lp
, Li
, Lx
São, respectivamente, a linha, índices e ponteiros de dados na representação csc padrão de matrizes esparsas. Esta função é o principal ponto de acesso do programa, com a linha
x[i] -= Lx[j] * x[Li[j]];
sendo a maior parte do tempo gasto. Compilar com gcc-8.3 -O3 -mfma -mavx -mavx512f
dá
backsolve(int const*, int const*, double const*, int, double*):
lea eax, [rcx-1]
movsx r11, eax
lea r9, [r8+r11*8]
test eax, eax
js .L9
.L5:
movsx rax, DWORD PTR [rdi+r11*4]
mov r10d, DWORD PTR [rdi+4+r11*4]
cmp eax, r10d
jge .L6
vmovsd xmm0, QWORD PTR [r9]
.L7:
movsx rcx, DWORD PTR [rsi+rax*4]
vmovsd xmm1, QWORD PTR [rdx+rax*8]
add rax, 1
vfnmadd231sd xmm0, xmm1, QWORD PTR [r8+rcx*8]
vmovsd QWORD PTR [r9], xmm0
cmp r10d, eax
jg .L7
.L6:
sub r11, 1
sub r9, 8
test r11d, r11d
jns .L5
ret
.L9:
ret
De acordo com vtune,
vmovsd QWORD PTR [r9], xmm0
é a parte mais lenta. Quase não tenho experiência com montagem e não sei como diagnosticar ou otimizar ainda mais esta operação. Eu tentei compilar com diferentes sinalizadores para ativar / desativar SSE, FMA, etc, mas nada funcionou.
Processador: Xeon Skylake
Pergunta O que posso fazer para otimizar esta função?
fonte
i >= Li[j]
para todosj
no circuito interno?Li[j]
sejam separadasi
, verifique a suposição com instruções de detecção de conflitos, verifique se todas asi
s são independentes, calcule, armazene os resultados. Se algum conflito for detectado, volte para a implementação escalar.i < Li[j] < n
. Atualizado a pergunta para mencionar a natureza triangular inferior de A.Respostas:
Isso deve depender um pouco do padrão exato de dispersão da matriz e da plataforma que está sendo usada. Testei algumas coisas com
gcc 8.3.0
sinalizadores de compilador-O3 -march=native
(que estão-march=skylake
na minha CPU) no triângulo inferior dessa matriz de dimensão 3006 com entradas 19554 diferentes de zero. Espero que isso esteja um pouco próximo da sua configuração, mas, em qualquer caso, espero que eles possam lhe dar uma idéia de por onde começar.Por tempo, usei google / benchmark com este arquivo de origem . Ele define
benchBacksolveBaseline
quais benchmarks a implementação dada na pergunta ebenchBacksolveOptimized
quais benchmarks as implementações "otimizadas" propostas. Também hábenchFillRhs
quais benchmarks separadamente da função usada em ambos para gerar alguns valores não completamente triviais para o lado direito. Para obter o tempo dos backsolves "puros", o tempo quebenchFillRhs
leva deve ser subtraído.1. Iterando estritamente para trás
O loop externo em sua implementação percorre as colunas para trás, enquanto o loop interno percorre a coluna atual para a frente. Parece que seria mais consistente iterar através de cada coluna também:
Isso mal muda a montagem ( https://godbolt.org/z/CBZAT5 ), mas os tempos de referência mostram uma melhoria mensurável:
Suponho que isso seja causado por um acesso de cache de alguma forma mais previsível, mas não o examinei muito mais.
2. Menos cargas / lojas no loop interno
Como A é triangular inferior, temos
i < Li[j]
. Portanto, sabemos quex[Li[j]]
isso não será alterado devido às alteraçõesx[i]
no loop interno. Podemos colocar esse conhecimento em nossa implementação usando uma variável temporária:Isso faz com que
gcc 8.3.0
o armazenamento seja transferido para a memória de dentro do loop interno para diretamente após seu término ( https://godbolt.org/z/vM4gPD ). A referência para a matriz de teste no meu sistema mostra uma pequena melhoria:3. Desenrole o loop
Embora
clang
já comece a desenrolar o loop após a primeira alteração de código sugerida,gcc 8.3.0
ainda não o fez. Então, vamos tentar, passando adicionalmente-funroll-loops
.Observe que a linha de base também melhora, pois o loop nessa implementação também é desenrolado. Nossa versão otimizada também se beneficia um pouco do desenrolar do loop, mas talvez não tanto quanto gostaríamos. Olhando para o assembly gerado ( https://godbolt.org/z/_LJC5f ), parece que
gcc
pode ter ido um pouco longe com 8 desenrolamentos. Para minha configuração, na verdade, posso melhorar um pouco com apenas um desenrolar manual simples. Então solte a bandeira-funroll-loops
novamente e implemente o desenrolamento com algo como isto:Com isso eu meço:
Outros algoritmos
Todas essas versões ainda usam a mesma implementação simples da solução reversa na estrutura da matriz esparsa. Inerentemente, operar em estruturas de matriz esparsas como estas pode ter problemas significativos com o tráfego de memória. Pelo menos para fatorações matriciais, existem métodos mais sofisticados, que operam em submatrizes densas que são montadas a partir da estrutura esparsa. Exemplos são métodos supernodais e multifrontais. Estou um pouco confuso com isso, mas acho que esses métodos também aplicarão essa idéia ao layout e usarão operações de matriz densa para soluções triangulares inferiores para trás (por exemplo, para fatorações do tipo Cholesky). Portanto, pode valer a pena examinar esse tipo de método, se você não for forçado a seguir o método simples que funciona diretamente na estrutura esparsa. Veja, por exemplo, esta pesquisapor Davis.
fonte
-ffast-math
) clang usará vários acumuladores. O GCC desenrolará, mas não se preocupará em criar várias cadeias de dependência para ocultar a latência da ULA, derrotando a maior parte do objetivo de loops de redução comoxi_temp -=
. Embora a melhoria 2. tenha compilado da maneira que esperávamos, tirando a latência de encaminhamento de loja do caminho crítico, mas acelerado por muito menos do que um fator de 2, parece que a latência de FP não é um grande gargalo (em vez disso, falta de memória / cache) ou que as cadeias dep sejam curtas o suficiente para que o exec OoO oculte.Você pode cortar alguns ciclos usando, em
unsigned
vez de,int
os tipos de índice, que devem ser>= 0
assim:A compilação com o explorador de compilador da Godbolt mostra um código ligeiramente diferente para o loop interno, potencialmente fazendo melhor uso do pipeline da CPU. Não posso testar, mas você pode tentar.
Aqui está o código gerado para o loop interno:
fonte
unsigned
vez desize_t
ainda evita a extensão de sinal sem impacto no cache, e o código é um pouco diferente, potencialmente permitindo um melhor uso do pipeline.unsigned
, mas não estou vendo nada que pareça melhor com ela. Para mim, parece um pouco pior que o códigoint
ousize_t
. De qualquer forma, também parece quegcc
está tentando desperdiçar memória usandoincq %rax
withgcc-9.2.1 -march=skylake-avx512
para os casosint
eunsigned
, ondeincl %rax
salvaria um rex-byte. Hoje não estou impressionado com o gcc.