在选择按值传递与按常量传递时,我试图理解并利用 ARMv8 ABI。特别是,我有一个结构,用 ARM 术语来说是“同质浮点聚合 (HFA)”。我将使用带有 -O 编译器开关的 armv8-a clang 18.1.0 粘贴来自 godbolt 的一些结果。 (我在使用 Xcode 15 的 Mac 上看到类似的反汇编结果。)
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(Thing)
的接口是让调用者将两个双精度值粘贴到寄存器对 (d0,d1) 中,并在寄存器 d0 中返回答案。任何地方都没有访问内存——没有 SP
或 LP
的破坏;没有在任何地方创建堆栈框架;没有必要让争论永远存在于记忆中。而且baz(double, double)
看起来也不错。一切都内嵌了——没有 Thing
甚至变得栩栩如生。
但是当我切换到另一个版本的复制构造函数时,我得到了这个
diff(Thing)
:
diff(Thing): // @diff(Thing)
ldp d0, d1, [x0]
fsub d0, d0, d1
ret
突然,调用者需要将
Thing
放置在内存中的某个位置,并在 x0 中传递指向该内存的指针。然后 diff(Thing)
必须从该内存位置获取这对双精度数。哎呀。
diff(Thing)
签名没有改变,但突然ABI完全不同了!这种巨大的差异是否记录在某处?为什么我的手写复制构造函数不如默认复制构造函数那么好?我很高兴有这么多内联 baz
,但我想知道我可以挂在墙上的最佳实践是什么?例如,对于这样的结构我想要:
bool operator==( const Thing& lhs, const Thing& rhs)
或
bool operator==( Thing lhs, Thing rhs)
或
(let the compiler make one)
我想要解释上面的巨大差异以及关于值与常量和参数的建议。
我不是 C++ ABI 专家,但我会尝试一下。
ARM64 与大多数现代平台一样,通常遵循 Itanium C++ ABI,并进行一些与此处不相关的修改。 Itanium ABI 定义了“对于调用来说非常重要”的类型概念,这迫使它“始终在内存中传递,而不是在寄存器中传递”。 如果满足以下条件,则类型被认为对于调用而言是重要的:
它有一个不平凡的复制构造函数、移动构造函数或析构函数,或者其所有复制和移动构造函数都被删除。
:
- “简单复制构造函数”的概念在 C++ 标准中定义,
class.copy.ctor p11
如果不是的话,类 X 的复制/移动构造函数是微不足道的 用户提供并且如果:
类 X 没有虚函数 ([class.virtual]) 和虚基类 ([class.mi]),并且选择复制/移动每个直接基类子对象的构造函数很简单,并且
您的复制构造函数
- 对于 X 的每个类类型(或其数组)的非静态数据成员,选择复制/移动该成员的构造函数是 微不足道;
- 否则复制/移动构造函数是不平凡的。
Thing( const Thing& other) : x(other.x), y(other.y) {}
由用户提供。因此,它不是一个简单的复制构造函数,因此它使得
Thing
对于调用而言非常重要,因此它不能在寄存器中传递。
您的复制构造函数实际上所做的事情与此分析无关,即使它执行与默认复制构造函数完全相同的操作。