1 pontos por GN⁺ 2024-01-06 | 1 comentários | Compartilhar no WhatsApp

Acelerando o código: não passe structs maiores que 16 bytes no AMD64

  • Para melhorar o desempenho da linguagem Neat, foi alterada a forma de passar arrays: em vez de um único parâmetro de struct, passaram a ser usados três parâmetros de ponteiro.
  • O motivo de os arrays de Neat serem mais lentos que os arrays da linguagem D era que o array, com 24 bytes, excedia 16 bytes e por isso os parâmetros eram passados de outra forma.
  • Segundo a especificação da ABI SystemV AMD64, toda struct com mais de 16 bytes é passada por ponteiro.

Confirmando o problema com benchmarks

  • Os benchmarks confirmaram a diferença de desempenho entre passar uma struct e passar campos individuais.
  • Ao passar uma struct, é necessário alocar na pilha e copiar; ao passar campos individuais, eles são enviados diretamente por registradores SSE.
  • Passar campos individuais mostrou desempenho cerca de 2 vezes melhor do que passar uma struct.

A escolha do projetista da linguagem

  • Ao chamar uma API em C, é preciso seguir a ABI de C, mas tipos de alto nível usados internamente não precisam ser representados como structs.
  • O projetista da linguagem pode decidir como arrays, tuplas e tipos soma serão passados.
  • Passar separadamente os campos de tipos com mais de 16 bytes pode ajudar a melhorar o desempenho.

Opinião do GN⁺

  • Este artigo é muito útil para desenvolvedores interessados em otimização de software.
  • Ele mostra, especialmente ao desenvolver aplicações sensíveis a desempenho, que o tamanho da struct e a forma de passá-la podem ter impacto importante.
  • Projetistas de linguagem e desenvolvedores de API podem usar essa informação para encontrar oportunidades de melhorar o desempenho.

1 comentários

 
GN⁺ 2024-01-06
Comentário do Hacker News
  • Em relação ao problema da ABI SysV amd64, é possível definir a ABI interna da linguagem como algo diferente de SysV. Desde que não seja exposta a chamadores C SysV, é possível usar a convenção de chamada que quiser. A diferença do NeatLang parece ser muito mais complexa do que simplesmente mudar a convenção de chamada do LLVM, e o autor talvez queira expor tipos a programas em C com uma convenção de chamada consistente.
  • Muitas vezes falta compreensão sobre o custo da passagem de argumentos, e o texto escrito sobre isso é útil. Por exemplo, no Google a prática de passar objetos de 24 bytes por valor não aparece no profiler, mas gera custo em todas as funções.
  • Ao migrar para x64, houve preocupação com a expansão de objetos vec3 (3xfloat) de 12 bytes para 16 bytes, então o motor gráfico foi submetido a benchmark. Descobriu-se que usar 16 bytes era mais rápido por se alinhar melhor a leituras de 8 bytes. Como resultado, vec3 passou a ser usado como vec4. Recomenda-se sempre fazer benchmark do sistema como um todo.
  • Argumentos pré-carregados em registradores têm desempenho melhor do que gravações na pilha, e manipulação de pilha é mais rápida do que algo alocado no heap. Isso explica por que código complexo com muitas variáveis globais pode executar rápido, enquanto funções recursivas elegantes ou argumentos em tuplas/structs/listas tendem a ser lentos. O primeiro caso é mais fácil de otimizar em loops compactos de assembly.
  • No MSVC, structs com mais de 8 bytes são passadas pela pilha. Esse é um detalhe de ABI do qual código portável não deveria depender. Mas, no caso de funções chamadas com pouca frequência, não vale a pena se estressar demais; para funções pequenas chamadas com frequência, permita que o compilador faça inline do código, ativando otimizações mais úteis do que apenas passar argumentos por registradores.
  • No Windows, ao usar a convenção de chamada cdecl padrão, structs maiores que 8 bytes não são passadas em registradores.
  • No amd64, passar e retornar structs maiores que 16 bytes por valor usando a ABI sysv amd64 é lento, mas muitas vezes vale a pena para deixar o código mais claro. Claro que isso não se aplica aqui, mas, por exemplo, cada compilador C++, Golang, OCaml e SBCL pode usar uma ABI personalizada dentro da própria linguagem.
  • Em C++, existe a regra prática de que tipos não primitivos devem ser passados por referência (ou, se necessário, por ponteiro), a menos que haja um bom motivo para não fazer isso. Isso também se deve à ABI e ao desejo de evitar construtores de cópia ou de movimento. Se você quer otimizar desempenho, esse é um daqueles detalhes chatos de baixo nível aos quais é preciso prestar atenção em C++.
  • O artigo fornece um link para um benchmark muito específico, no qual Java (JIT) é mais rápido que C++ e até mais rápido que Scala. Isso levanta dúvidas sobre o que é Julia HO e por que é tão rápido, por que há uma diferença tão grande de velocidade entre Python e Pypy, se existe motivo para não usar Pypy e se ele deveria se tornar o padrão.
  • No exemplo fornecido, isso pode ser corrigido sem afetar o chamador, mudando o tipo do parâmetro struct Vector para uma referência const struct Vector &. Muito código C++ com bugs de ponteiro usou ponteiros sem necessidade, quando passar por referência teria sido mais fácil e mais seguro.