Não zombe do feliz preditor de desvios
- Tenho escrito bastante assembly AArch64 recentemente
- Uma ideia "inteligente" para remover um salto no loop acabou piorando o desempenho
- Explico esse erro para que outras pessoas não cometam o mesmo engano
Exemplo de código
float run(const float* data, size_t n) {
float g = 0.0;
while (n) {
n--;
const float f = *data++;
foo(f, &g);
}
return g;
}
static void foo(float f, float* g) {
// g를 수정하는 작업
}
Traduzido para assembly AArch64
// x0: const float* data
// x1: size_t n
// s0: float de retorno
stp x29, x30, [sp, #-16]!
mov s0, #0.0
loop:
cmp x1, #0
b.eq exit
sub x1, x1, #1
ldr s1, [x0], #4
bl foo
b loop
foo:
// s1에서 읽고 s0에 누적
// ...
ret
exit:
ldp x29, x30, [sp], #16
ret
Tentativa de otimização
- Tentativa de melhorar o desempenho reduzindo a instrução
bl
- Mas o desempenho acabou piorando
Comparação de desempenho
- Código original: 969 ns
- Código otimizado: 3.85 µs
Análise da causa
- O preditor de desvios fica confuso porque os pares
bl e ret não correspondem
- Segundo a documentação da ARM, a instrução
ret ajuda a prever retornos de função
Como resolver
- Usar
br x30 em vez de ret
- Desempenho recuperado: 913 ns
Otimização adicional
- Fazer inline de
foo para melhorar o desempenho
- Usar unrolling de loop e instruções SIMD
Desempenho final
- SIMD + unrolling manual de loop: 94 ns
Conclusão
- Não confunda o preditor de desvios
- O código SIMD é mais rápido, mas como a soma de ponto flutuante não segue a propriedade associativa, o resultado pode ser diferente
Opinião do GN⁺
- Este texto mostra bem a importância da otimização em assembly AArch64
- Entender como o preditor de desvios funciona é essencial para otimizar desempenho
- A otimização com instruções SIMD é muito eficaz, mas é preciso considerar questões de precisão
- Usar uma linguagem de alto nível como Rust pode facilitar ganhos de desempenho via otimizações do compilador
- Um projeto com funcionalidade semelhante é o guia de otimização em assembly de Agner Fog
1 comentários
Opiniões no Hacker News
Resumiu o artigo junto com amigos da época do Apple II
Raymond Chen abordou o mesmo tema há quase 20 anos
Em código SIMD, a soma de ponto flutuante pode ser feita em outra ordem porque a adição de ponto flutuante não segue a propriedade associativa
Desde o Rust 1.78, o compilador usa unrolling de loop mais agressivo e um pouco de SIMD
Houve confusão sobre como x0 é incrementado em assembly ARM/ARM64
ldr s1, [x0], #4faz o carregamento enquanto incrementa x0 em 4Surpreendeu que não tenham tentado um método menos complexo para otimizar o código assembly
Houve quem desejasse que o autor não ficasse mudando de unidade o tempo todo