1 pontos por GN⁺ 2025-05-23 | 1 comentários | Compartilhar no WhatsApp
  • Foi observado que o decodificador AV1 rav1d, escrito em Rust, era cerca de 9% mais lento que o dav1d baseado em C
  • Foi confirmado que a otimização da inicialização de buffers e a melhoria da lógica de comparação de structs trouxeram ganhos de velocidade de 1,5% e 0,7%, respectivamente
  • Com a ferramenta de profiling samply, foi possível identificar de forma concreta a causa da diferença de desempenho entre as duas versões
  • Em vez da implementação padrão de PartialEq do Rust, foi adotada uma comparação em nível de bytes para aumentar a eficiência
  • Com esta otimização, cerca de 30% da diferença total de desempenho foi reduzida, embora ainda haja espaço para mais otimizações

Contexto e abordagem

  • rav1d é um projeto que porta o decodificador AV1 dav1d para Rust com c2rust, incorporando funções otimizadas em asm e melhorias de segurança próprias da linguagem Rust
  • Um critério público de desempenho básico foi estabelecido, e o rav1d em Rust estava cerca de 5% mais lento que o dav1d em C
  • Em vez de analisar a estrutura geral de um decodificador de vídeo complexo, a análise se concentrou na diferença de tempo de execução entre binários com a mesma entrada
  • A comparação foi feita de forma sistemática com a ferramenta de medição de desempenho (hyperfine) e o profiler (samply)
  • O ambiente-alvo foi um chip macOS M3, simplificado com execução em thread única

Medição de desempenho: comparação do padrão

  • Cada versão foi compilada e testada com benchmark usando o mesmo arquivo de teste (Chimera-AV1-8bit-1920x1080-6736kbps.ivf)
  • rav1d: cerca de 73,9 segundos, dav1d: cerca de 67,9 segundos, confirmando uma diferença de aproximadamente 6 segundos (9%) no tempo de execução
  • Cada compilador (Clang, Rustc) usava praticamente a mesma versão do LLVM

Análise de profiling

  • Com o profiler samply, foi feita uma comparação da contagem de amostras por função em cada executável
  • Foi dada atenção especial aos caminhos de chamada e à distribuição de amostras das funções em assembly baseadas em NEON (ARM SIMD)
  • O dav1d separa em funções de filtro distintas para chamar condicionalmente as funções asm, enquanto o rav1d gerencia tudo em uma única função de dispatch
  • Na função cdef_filter_neon_erased, o número de amostras Self foi cerca de 270 maior que a soma de duas funções equivalentes no dav1d (cerca de 1% do total)
  • A análise identificou um trecho em que um buffer temporário (zero-initialized buffer) estava sendo inicializado de forma desnecessariamente grande

Otimização com remoção da inicialização de buffers

  • Por segurança, o Rust faz zeroing automático com abordagens como [0u16; LEN]
  • Porém, em C (dav1d), o buffer não é zerado explicitamente; apenas a região realmente usada recebe valores
  • No Rust, foi usado std::mem::MaybeUninit para eliminar o custo desnecessário dessa inicialização
  • As amostras Self da função cdef_filter_neon_erased caíram drasticamente de 670 para 274
  • Outro buffer grande Align16 também teve a inicialização movida para fora do loop, reduzindo o custo de inicialização para apenas uma vez
  • Após a otimização, o benchmark caiu para cerca de 72,6 segundos, uma melhora de 1,2 segundo (1,5%)

Otimização da comparação de structs

  • Na análise de pilha invertida do profiling, foi descoberto que a função add_temporal_candidate estava funcionando de maneira mais ineficiente do que o esperado
  • A comparação de campos da struct Mv nessa função (implementação automática de PartialEq) gerava código desnecessariamente lento
  • Em C, uma union é usada para realizar uma comparação eficiente em unidades de uint32_t
  • Em Rust, sem recorrer a unsafe, a comparação em fatias de bytes foi implementada com a trait zerocopy::AsBytes
  • Essa otimização trouxe mais 0,5 segundo de ganho (cerca de 0,7%)

Resultado e resumo

  • Duas otimizações simples (remoção da inicialização de buffers e comparação de structs por bytes) reduziram o tempo de execução em mais de 2%
  • Ainda resta uma diferença de desempenho de cerca de 6%, com bastante espaço para otimizações adicionais
  • Foi confirmado que o método de comparação entre snapshots do profiler é eficaz
  • Há grande potencial para novas otimizações no rav1d e no dav1d com base na análise desses snapshots
  • Com feedback ativo e colaboração dos mantenedores do projeto, foi possível melhorar sem comprometer a segurança

Resumo

  • Com as ferramentas profiler (samply) e benchmark (hyperfine), foi feita uma análise precisa da diferença de 6 segundos (9%) no tempo de execução entre rav1d e dav1d
  • Duas otimizações principais:
    • remoção de zeroing desnecessário de buffer em código específico para ARM (1,2 segundo, -1,6%)
    • troca da implementação de PartialEq de uma struct numérica pequena por uma comparação rápida em nível de bytes (0,5 segundo, -0,7%)
  • Cada otimização é concisa, com apenas algumas dezenas de linhas, sem novo código unsafe
  • A colaboração com os mantenedores e a revisão de PRs permitiram melhorar confiabilidade e qualidade ao mesmo tempo
  • Ainda resta uma diferença de cerca de 6% no desempenho, deixando amplo espaço para mais estudos de otimização baseados em comparação com profiler

Go ahead and give this a try! Maybe rav1d can eventually become faster than dav1d 👀🦀.

1 comentários

 
GN⁺ 2025-05-23
Opiniões no Hacker News
  • Compartilha a opinião de que a questão de comparar dois u16 é um tema interessante, e fornece o link da issue relacionada https://github.com/rust-lang/rust/issues/140167
    • Expressa surpresa por store forwarding não ter sido mencionado na discussão; o resultado da geração de código em -O3 é excessivo, mas em -O2 parece uma decisão razoável. Explica concretamente que, se uma das structs tiver acabado de passar por uma operação, tentar um load de 32 bits pode falhar em store forwarding, tornando o ganho de desempenho irrelevante. Aponta que, em cenários sem inline e sem PGO, o compilador não tem informações suficientes para julgar se a otimização é adequada.
    • Comenta que foi bom a discussão da issue não ter sido preenchida por comentários do tipo “aconteceu comigo também” ou “quando isso vai ser corrigido”, e compartilha de forma sincera que, como desenvolvedor web, considera as issues do GitHub insatisfatórias.
    • Opina que este caso mostra o quão complexo é o desenvolvimento de compiladores, e expressa convicção de que compiladores da família C também não lidariam muito melhor com esse tipo de issue.
  • Demonstra curiosidade sobre como os resultados do profiler foram inseridos no post do blog, perguntando se os nós HTML foram simplesmente copiados.
  • Acha interessante que um texto sobre a vantagem de desempenho de omitir a inicialização do buffer (zeroing) tenha saído poucos dias depois de um texto relacionado, e compartilha o link do texto anterior https://news.ycombinator.com/item?id=44032680
  • Aponta que o título do texto principal foi modesto demais em comparação com o resultado real, destacando que na prática houve um ganho de 2,3% graças a duas boas otimizações.
    • Opina que a melhoria de 1,5% vale apenas para aarch64, então mencioná-la como número geral não é totalmente justo; considerando a proporção entre ARM/x86, o correto seria tratar como algo em torno da metade.
  • Avalia que o post foi útil e que foi impressionante encontrar código ineficiente na comparação de pares de inteiros de 16 bits.
    • Tem curiosidade se os desenvolvedores de Rust/LLVM poderão aplicar essa otimização automaticamente quando possível, mencionando que, no Rust, as informações relacionadas à inicialização de memória são muito mais precisas.
  • Acredita que, em igualdade de condições, esse tipo de codec deveria ser tratado em uma linguagem como WUFFS ou outra linguagem especializada, e compartilha a percepção de que converter código complexo como o dav1d para WUFFS é muito mais difícil do que apenas traduzir/organizar código C existente. Ainda assim, considera uma tentativa valiosa e defende que vale a pena investir nisso em termos civilizatórios.
    • Explica que WUFFS serve para parsing de contêineres como Matroska, webm e mp4, mas não é nada adequada para decodificadores de vídeo. Como não há alocação dinâmica de memória, lidar com dados dinâmicos se torna desafiador, e enfatiza que codecs de vídeo não fazem apenas parsing de arquivos, mas exigem gerenciamento de estados dinâmicos muito variados.
  • Pergunta, como quem fala sozinho, sobre o andamento da recompensa do rav1d, e expressa identificação ao ver que há outras pessoas com a mesma dúvida.
  • Comenta que textos que começam com um meme divertido costumam ser bons posts, e menciona a relação com a discussão recente “Rav1d AV1 Decoder Rust optimization $20k bounty”, adicionando o link relacionado https://news.ycombinator.com/item?id=43982238
    • Brinca dizendo que este caso é um exemplo claro de Nominative determinism.
  • Diz que, honestamente, a primeira otimização foi um pouco surpreendente por ser do tipo comum que se encontra facilmente só usando perf; achava que a questão do zeroing já tinha sido discutida no primeiro post. A segunda otimização foi mais complexa e interessante, mas ainda assim foi guiada pelo perf, e aconselha a não subestimar a utilidade da ferramenta.
    • Esclarece que não foi apenas uso de perf, mas também profiling diferencial entre a versão em C e a versão em Rust, além de um processo manual de correspondência. Aponta como limitação que existe o recurso perf diff, mas os nomes dos símbolos são diferentes, o que dificulta o pareamento automático.
    • Menciona que a abordagem foi a partir da perspectiva de dispositivos Apple baseados em aarch64, e ressalta pela experiência que pessoas com históricos diferentes conseguem captar rapidamente coisas que, olhando em retrospecto, parecem “óbvias”.
  • Especula que este caso esteja por trás do motivo de a conta do Twitter do ffmpeg ter se pronunciado sobre questões relacionadas a Rust, e compartilha o link do tweet https://x.com/ffmpeg/status/1924137645988356437?s=46
    • Compartilha sinceramente que, ao ler a conta do Twitter do ffmpeg, passou a ter dúvidas sobre usar ffmpeg; lamenta a falta de alternativas e critica a toxicidade da comunidade de desenvolvedores. Observa que desempenho máximo pode ser importante, mas em ambientes que trocam dados com o mundo externo, o ffmpeg pode ter várias vulnerabilidades remotas (CVEs) por ano, e enfatiza a necessidade de um sandbox bem restritivo do ponto de vista de segurança. Defende que é preciso um meio-termo para construir soluções que sejam ao mesmo tempo rápidas e seguras, e compartilha o link relacionado https://ffmpeg.org/security.html
    • Sugere que uma resposta melhor seria reagir melhorando o desempenho do dav1d; usa a metáfora de recordes esportivos para dizer que melhorar apenas os números não empolga tanto quanto bater um recorde de verdade, e explica de forma bem-humorada que a solução real é produzir resultados concretamente rápidos e inovadores.