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&&vsTpor valor; alguns veemT&&(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.