ARMv8 ABI下拷贝构造函数与传值/const&的性能差异及最佳实践
ARMv8 ABI下HFA结构体的传值/传引用选择与ABI行为解析
问题背景
定义ARMv8术语中的**同质浮点聚合体(HFA)**结构体Thing,使用armv8-a clang 18.1.0(-O优化)编译后,出现了不同拷贝构造函数下的ABI调用差异:
测试代码
struct Thing { const double x,y; Thing( double a, double b) : x(a), y(b){} Thing( const Thing& other) = default; // Thing( const Thing& other) : x(other.x), y(other.y) {} }; double diff( Thing t) { return t.x - t.y; } double baz(double x, double y) { Thing i = {x, 5.}; return diff(i); }
默认拷贝构造的汇编结果
diff(Thing): // @diff(Thing) fsub d0, d0, d1 ret baz(double, double): // @baz(double, double) fmov d1, #-5.00000000 fadd d0, d0, d1 ret
此时diff的调用方将两个double存入寄存器对(d0,d1),无内存访问,baz完全内联,未构造实际的Thing对象。
手写拷贝构造的汇编结果
diff(Thing): // @diff(Thing) ldp d0, d1, [x0] fsub d0, d0, d1 ret
此时调用方需将Thing存入内存,通过x0传递指针,diff需从内存读取数据。
疑问解答
1. 为何diff签名未变,但ABI调用方式差异巨大?
核心原因是ARMv8 ABI对HFA类型的传递规则限制:
- ARMv8 ABI中,HFA(同质浮点聚合体)指由同类型浮点成员组成的聚合体,且满足特殊成员函数(拷贝构造、析构等)为
trivial(默认生成)的条件。这类类型的参数可以直接通过浮点寄存器组传递(比如两个double用d0+d1),无需内存中转。 - 当使用手写拷贝构造函数时,即使逻辑和默认构造完全一致,编译器会将该结构体判定为非HFA类型。此时ABI规则要求这类结构体参数通过通用寄存器传递指针(指向内存中的结构体实例),因此出现了内存访问的开销。
2. 手写拷贝构造为何不如默认的?
默认拷贝构造函数属于trivial特殊成员函数,满足ARMv8 ABI对HFA类型的识别条件,编译器可以将结构体按HFA处理,享受寄存器传递的优化。
而手写拷贝构造函数会让结构体的拷贝构造函数变为non-trivial,破坏了HFA的判定标准,导致结构体无法被归类为HFA,只能走内存指针传递的路径,额外增加了内存读写的开销。
3. HFA结构体的operator==该如何选择?
优先选择让编译器自动生成,原因如下:
- 编译器生成的
operator==会保持结构体的HFA特性,调用时可以利用寄存器传递参数,避免内存访问。 - 如果必须手写
operator==,优先选择传值参数(bool operator==(Thing lhs, Thing rhs)):HFA类型传值时通过寄存器传递,无需内存拷贝;相比传const&,避免了指针解引用的开销。 - 不推荐手写
const&版本的operator==,除非结构体后续不再满足HFA条件(比如添加了non-trivial成员),否则会浪费寄存器传递的优化机会。
传值vsconst&的最佳实践
- HFA类型:优先传值。ARMv8 ABI允许HFA通过浮点寄存器直接传递,无内存拷贝或访问开销,效率远高于传
const&。同时必须保持特殊成员函数(拷贝构造、析构等)为默认生成状态(=default),不要手写,避免破坏HFA识别。 - 小型非HFA结构体(大小≤通用寄存器宽度,比如≤8字节):优先传值。寄存器传递的开销远小于指针解引用,且避免了内存访问。
- 大型结构体(大小超过寄存器组可承载的范围):优先传
const&。避免大量寄存器占用或内存拷贝的开销,通过指针访问内存更高效。 - 特殊成员函数:尽量使用
=default或=delete,不要手写逻辑等价的代码,避免破坏编译器对trivial属性的判定,进而影响ABI优化。
内容的提问来源于stack exchange,提问作者Mustang
相关产品推荐
相关产品推荐

