Não passe structs maiores que 16 bytes no AMD64

Passar structs maiores que 16 bytes por valor no AMD64 pode degradar o desempenho silenciosamente porque as ABIs System V e MSVC x64 movem esses argumentos para a stack em vez de usar registradores. Os comentários debatem quando preferir passagem por valor versus por referência em C, C++, Rust, Zig e outras linguagens, equilibrando APIs mais claras e semântica de movimento contra custos ocultos em caminhos críticos e convenções de chamada. Vários observam que essas sobrecargas são difíceis de ver em profiladores, defendem otimização de programa inteiro ou ABIs internas personalizadas e enfatizam perfilação e contexto em vez de regras absolutas.

Restrições de ABI e de Convenção de Chamadas

  • O principal problema é a ABI SysV AMD64: structs com mais de 16 bytes são passadas via memória, não por registradores, o que pode adicionar sobrecarga oculta em código crítico.
  • No MSVC/x64 o limite é ainda menor (8 bytes). Plataformas diferentes têm limiares diferentes, então isso é um detalhe de ABI, não uma regra universal.
  • Alguns observam que retornos grandes são tratados de forma eficiente por meio de argumentos ocultos de “ponteiro de retorno”, então a sobrecarga no valor de retorno costuma ser menos problemática do que a passagem de parâmetros.

Passagem por Valor vs Passagem por Referência em C/C++

  • Muitas bases de código C++ adotam por padrão passar tipos não triviais por ponteiro ou referência, com tipos de visão (string_view, span, FunctionRef) como exceções comuns.
  • Alguns argumentam que passar por valor muitas vezes é aceitável ou até preferível quando isso habilita semântica de movimento e APIs mais simples, especialmente para tipos como std::string.
  • Outros apontam que referências constantes não evitam os spills de stack impostos pela ABI para structs grandes; apenas decompor em parâmetros escalares separados pode manter tudo em registradores.
  • Há debate sobre usar T&& vs T por valor; alguns veem T&& (sem templates) como um sinal de mau cheiro em código, em comparação com passar por valor e mover.

Notas Específicas de Linguagem

  • Zig pode escolher pass-by-value ou pass-by-reference de forma transparente para structs, mas isso levou a bugs confusos; há propostas (por exemplo, noalias-by-default) para mitigar.
  • A ABI interna de Rust é independente da SysV; FFI é onde essas questões de tamanho importam. Tipos emprestados (&str, slices) são naturalmente “fat pointers” e baratos de passar.
  • Desenvolvedores .NET também se preocupam em passar structs grandes; a orientação implícita é evitar structs maiores do que algumas referências.

Trade-offs de Desempenho e Perfilação

  • Copiar um objeto de 24 bytes versus passar um ponteiro nem sempre é obviamente melhor; há um trade-off entre cópias extras e localidade de cache versus pointer chasing e possíveis misses de cache.
  • Alguns participantes enfatizam que tais otimizações são de microescala e, em geral, devem seguir a perfilação e a otimização de programa inteiro (LTO), mas outros observam que os profiladores atuais têm dificuldade para destacar a sobrecarga difusa da convenção de chamadas.
  • Um benchmark real citado mostrou uma aceleração de ~2× (um grande salto no ranking) ao evitar parâmetros grandes passados por valor, sugerindo que isso pode importar em loops apertados.