Don't pass structs bigger than 16 bytes on AMD64

Passing structs larger than 16 bytes by value on AMD64 can silently degrade performance because the System V and MSVC x64 ABIs spill such arguments to the stack instead of using registers. Commenters debate when to prefer passing by value versus by reference in C, C++, Rust, Zig, and other languages, weighing clearer APIs and move semantics against hidden costs in hot paths and calling conventions. Several note that these overheads are hard to see in profilers, argue for whole-program optimization or custom internal ABIs, and stress profiling and context over blanket rules.

ABI and Calling-Convention Constraints

  • The main issue is the SysV AMD64 ABI: structs >16 bytes are passed via memory, not registers, which can add hidden overhead in hot code.
  • On MSVC/x64 the cutoff is even smaller (8 bytes). Different platforms have different thresholds, so this is an ABI detail, not a universal rule.
  • Some note that large returns are handled efficiently via hidden “return pointer” arguments, so return-value overhead is often less of a problem than parameter passing.

Pass-by-Value vs Pass-by-Reference in C/C++

  • Many C++ codebases default to passing nontrivial types by pointer or reference, with view types (string_view, span, FunctionRef) as common exceptions.
  • Some argue that passing by value is often OK or even preferable when it enables move semantics and simpler APIs, especially for types like std::string.
  • Others point out that const references do not avoid ABI-mandated stack spills for large structs; only decomposing into separate scalar parameters can keep everything in registers.
  • There’s debate on using T&& vs T by value; some view T&& (without templates) as a code smell compared to pass-by-value plus move.

Language-Specific Notes

  • Zig can choose pass-by-value or pass-by-reference transparently for structs, but this has led to confusing bugs; there are proposals (e.g., noalias-by-default) to mitigate.
  • Rust’s internal ABI is independent of SysV; FFI is where these size issues matter. Borrowed types (&str, slices) are naturally “fat pointers” and cheap to pass.
  • .NET developers also worry about passing large structs; guidance implied is to avoid structs larger than a couple of references.

Performance Tradeoffs and Profiling

  • Copying a 24-byte object vs passing a pointer is not always obviously better; there’s a tradeoff between extra copies and cache locality versus pointer chasing and potential cache misses.
  • Some participants stress that such optimizations are micro-level and should generally follow profiling and whole-program optimization (LTO), but others note current profilers struggle to highlight diffuse calling-convention overhead.
  • One real-world benchmark cited saw a ~2× speedup (large ranking jump) by avoiding large-by-value parameters, suggesting this can matter in tight loops.