No pases structs más grandes que 16 bytes en AMD64
Pasar structs de más de 16 bytes por valor en AMD64 puede degradar el rendimiento de forma silenciosa porque los ABI System V y MSVC x64 vuelcan esos argumentos a la pila en lugar de usar registros. Los comentaristas debaten cuándo conviene pasar por valor frente a por referencia en C, C++, Rust, Zig y otros lenguajes, sopesando APIs más claras y semántica de movimiento frente a costes ocultos en caminos calientes y convenciones de llamada. Varios señalan que estas sobrecargas son difíciles de ver en los perfiles, defienden la optimización de programa completo o ABI internas personalizadas, y subrayan el perfilado y el contexto por encima de reglas generales absolutas.
Restricciones de ABI y convención de llamadas
- El problema principal es el ABI SysV AMD64: los structs de más de 16 bytes se pasan por memoria, no por registros, lo que puede añadir sobrecarga oculta en código caliente.
- En MSVC/x64 el umbral es aún menor (8 bytes). Distintas plataformas tienen distintos límites, así que esto es un detalle del ABI, no una regla universal.
- Algunos señalan que las devoluciones grandes se manejan de forma eficiente mediante argumentos ocultos de “puntero de retorno”, por lo que la sobrecarga del valor de retorno suele ser menos problemática que el paso de parámetros.
Paso por valor vs paso por referencia en C/C++
- Muchas bases de código C++ usan por defecto punteros o referencias para tipos no triviales, con tipos de vista (string_view, span, FunctionRef) como excepciones comunes.
- Algunos argumentan que pasar por valor suele estar bien o incluso ser preferible cuando permite semántica de movimiento y APIs más simples, especialmente para tipos como std::string.
- Otros señalan que las referencias constantes no evitan los derrames a la pila impuestos por el ABI para structs grandes; solo descomponerlos en parámetros escalares separados puede mantener todo en registros.
- Hay debate sobre usar
T&&frente aTpor valor; algunos venT&&(sin plantillas) como una señal de mal olor de código en comparación con pasar por valor y mover.
Notas específicas de lenguaje
- Zig puede elegir de forma transparente paso por valor o por referencia para structs, pero esto ha provocado errores confusos; hay propuestas (por ejemplo, noalias por defecto) para mitigarlo.
- El ABI interno de Rust es independiente de SysV; donde importan estos problemas de tamaño es en FFI. Los tipos prestados (&str, slices) son naturalmente “punteros gordos” y baratos de pasar.
- Los desarrolladores de .NET también se preocupan por pasar structs grandes; la recomendación implícita es evitar structs mayores que un par de referencias.
Compromisos de rendimiento y perfilado
- Copiar un objeto de 24 bytes frente a pasar un puntero no es siempre obviamente mejor; hay un compromiso entre copias adicionales y localidad de caché frente a encadenamiento de punteros y posibles fallos de caché.
- Algunos participantes insisten en que estas optimizaciones son de nivel micro y, en general, deberían seguir al perfilado y a la optimización de programa completo (LTO), pero otros señalan que los perfiles actuales tienen dificultades para destacar la sobrecarga difusa de la convención de llamadas.
- Un benchmark real citado mostró una aceleración de ~2× (un gran salto en el ranking) al evitar parámetros grandes por valor, lo que sugiere que esto puede importar en bucles ajustados.