不要在 AMD64 上传递大于 16 字节的结构体
在 AMD64 上按值传递大于 16 字节的结构体可能会悄悄降低性能,因为 System V 和 MSVC x64 ABI 会把这类参数溢出到栈上,而不是使用寄存器。评论者讨论了在 C、C++、Rust、Zig 和其他语言中何时应按值传递、何时应按引用传递,在更清晰的 API 和移动语义,与热点路径和调用约定中的隐藏成本之间做权衡。多人指出,这些开销在性能分析工具中很难看出来,主张使用全程序优化或自定义内部 ABI,并强调应根据性能分析和具体上下文,而不是一刀切地定规则。
ABI 和调用约定限制
- 主要问题是 SysV AMD64 ABI:大于 16 字节的结构体会通过内存而不是寄存器传递,这会在热点代码中引入隐性开销。
- 在 MSVC/x64 上,这个阈值甚至更小(8 字节)。不同平台有不同的阈值,因此这是一条 ABI 细节,而不是普遍规则。
- 有人指出,大对象返回会通过隐藏的“返回指针”参数高效处理,所以返回值的开销通常比参数传递的问题要小。
在 C/C++ 中按值传递与按引用传递
- 许多 C++ 代码库默认对非平凡类型使用指针或引用传递,而视图类型(string_view、span、FunctionRef)是常见例外。
- 有人认为,按值传递往往没问题,甚至更可取,因为它能启用移动语义并简化 API,尤其是像
std::string这样的类型。 - 也有人指出,
const引用并不能避免 ABI 强制要求的大结构体栈溢出;只有拆分成独立的标量参数,才能让所有内容都保留在寄存器中。 - 关于使用
T&&还是按值传递T存在争论;有人认为,在非模板场景下,T&&相比按值传递再移动是一种代码异味。
语言特定说明
- Zig 可以透明地为结构体选择按值传递或按引用传递,但这已经导致了一些令人困惑的 bug;有人提出了相关方案(例如默认 noalias)来缓解。
- Rust 的内部 ABI 独立于 SysV;这些大小问题主要出现在 FFI 中。借用类型(&str、slice)本质上是“胖指针”,传递起来很便宜。
- .NET 开发者也会担心传递大结构体;暗示性的建议是避免使用大于几个引用大小的结构体。
性能权衡与性能分析
- 复制一个 24 字节对象与传递指针相比,并不总是显然谁更优;这里存在额外复制和缓存局部性,与指针追踪和潜在缓存未命中之间的权衡。
- 一些参与者强调,这类优化属于微观层面,通常应当基于性能分析和全程序优化(LTO)来决定,但也有人指出,当前的性能分析工具很难突出分散的调用约定开销。
- 引用的一个真实基准显示,通过避免按值传递大参数,性能提升了约 2 倍(排名大幅提升),这说明在紧密循环中它可能很重要。