为什么System V / AMD64 ABI要求16字节的堆栈对齐?
我已经在不同的地方阅读过它是为了“性能原因”而完成的,但我仍然想知道通过这种16字节对齐性能得到改进的特定情况是什么。 或者,无论如何,选择这个的原因是什么。
编辑 :我在想我以误导的方式写下了这个问题。 我并没有问及为什么处理器能够以16字节对齐的内存更快地完成任务,这在文档中无处不在。 我想知道的是,强制执行的16字节对齐方式比只让程序员在需要时自行调整堆栈的方式更好。 我在问这个问题是因为根据我的汇编经验,堆栈执行有两个问题:只有少于执行代码的1%(因此其他99%实际开销); 它也是一个非常常见的错误来源。 所以我想知道它最终如何真正得到回报。 虽然我仍然对此有疑问,但我接受彼得的答案,因为它包含了我最初的问题最详细的答案。
我认为,SSE2是x86-64的基准,并且使ABI对像__m128
这样的类型以及编译器自动向量化有效,这是我设计的目标之一。 ABI必须定义如何将这些参数作为函数参数传递,或者通过引用来传递。
对于堆栈中的局部变量(特别是数组),16字节的对齐有时是有用的,并且保证16字节的对齐意味着即使源没有明确地请求它,编译器也可以在有用时免费获得它。
如果不知道相对于16字节边界的堆栈对齐,则每个需要对齐本地的函数都需要一个and rsp, -16
和额外的指令,以便在未知的rsp
偏移( 0
或0
之后保存/恢复rsp
-8
)。 例如使用rbp
作为帧指针。
没有AVX,存储器源操作数必须是16字节对齐的。 如果内存操作数未对齐paddd xmm0, [rsp+rdi]
出错。 所以如果不知道对齐,你必须使用movups xmm1, [rsp+rdi]
/ paddd xmm0, xmm1
,或者编写一个循环序言/结尾来处理未对齐的元素。 对于编译器想要自动矢量化的本地数组,它可以简单地选择将它们对齐16。
另外请注意,早期的x86 CPU(在Nehalem / Bulldozer之前)有一个movups
指令,即使指针确实对齐,它也比movaps
慢。 (即对齐数据上未对齐的加载/存储特别慢,并且防止将折叠加载到ALU指令中)。 (有关以上所有内容的更多信息,请参阅Agner Fog的优化指南,微型指南和指令表。)
这些因素是为什么保证比“平常”保持堆栈对齐更有用。 被允许编写错误堆栈中实际发生错误的代码可以提供更多优化机会。
对齐数组还可以加速矢量化的memcpy
/ strcmp
/无论如何不能对齐的函数,而是检查它并可以直接跳转到它们的全矢量循环。
从最新版本的x86-64 System V ABI(r252)开始:
除了长度至少为16字节的本地或全局数组变量或C99可变长度数组变量始终具有至少16个字节的对齐外,数组使用与其元素相同的对齐方式.4
4对齐要求允许在阵列上操作时使用SSE指令。 编译器通常不能计算可变长度数组(VLA)的大小,但预计大多数VLA至少需要16个字节,因此要求VLA至少具有16个字节的对齐是合理的。
这有点激进,大多只在内联自动矢量化函数时才有用,但通常还有其他本地编译器可以填充到任何空隙中,因此不会浪费堆栈空间。 只要有已知的堆栈对齐方式,不会浪费指令。 (很显然,如果ABI设计人员决定不需要16字节的堆栈对齐,ABI设计人员可能会将其抛弃。)
溢出/重新加载__m128
当然,它可以免费使用alignas(16) char buf[1024];
或源请求16字节对齐的其他情况。
还有__m128
/ __m128d
/ __m128i
当地人。 编译器可能无法将所有向量movaps
保存在寄存器中(例如溢出函数调用或没有足够的寄存器),所以它需要能够使用movaps
或作为ALU指令的存储器源操作数进行溢出/重新加载出于上述讨论的效率原因。
实际上在高速缓存行边界(64字节)上分割的加载/存储具有显着的延迟惩罚,并且在现代CPU上也有较小的吞吐量损失。 加载需要来自2个独立缓存行的数据,因此需要对缓存进行两次访问。 (可能有2个缓存未命中,但堆栈内存很少见)。
我认为movups
已经在价格昂贵的旧CPU上花费了这些成本,但它仍然很糟糕。 跨越4k页面边界更糟糕(在Skylake之前的CPU上),如果在4k边界的两侧触及字节,则加载或存储会花费大约100个周期。 (还需要2个TLB检查)。 自然对齐使得不可能跨越任何更宽的边界进行分割 ,因此对于使用SSE2可以执行的所有操作,16字节对齐就足够了。
由于long double
(10字节/ 80位x87), max_align_t
在x86-64 System V ABI中有16字节对齐 。 它被定义为填充为16个字节的奇怪原因,与sizeof(long double) == 10
32位代码不同。 无论如何,x87 10字节加载/存储的速度相当缓慢(比如Core2上的double
或float
的负载吞吐量的1/3,P4上的1/6或K8上的1/8),但是缓存行和页面拆分处罚可能是在较老的CPU上如此糟糕以至于他们决定以这种方式定义它。 我认为现代CPU(甚至可能是Core2)循环遍历一个long double
精度数组的速度并不会因打包的10字节而变慢,因为fld m80
将比每个约6.4个元素的高速缓存行分裂更大的瓶颈。
实际上,ABI是在芯片可用于基准测试之前定义的(大约在2000年),但是这些K8数字与K7相同(32位/ 64位模式在这里是不相关的)。 做long double
16字节确实可以用movaps
复制一个,即使你在XMM寄存器中不能做任何事情。 (除了用xorps
/ andps
/ orps
处理符号位)
相关:这个max_align_t
定义意味着malloc
总是返回x86-64代码中的16字节对齐内存。 这_mm_load_ps
将它用于像_mm_load_ps
这样的SSE对齐的负载,但是这样的代码在编译为32位时可能会中断,其中alignof(max_align_t)
仅为8(使用aligned_alloc
或其他)。
其他ABI因素包括在栈上传递__m128
值(在xmm0-7具有前8个浮点/向量参数之后)。 要求内存中的向量需要16字节对齐是有意义的,因此被调用者可以高效地使用它们,并且可以由调用者高效地存储。 始终保持16字节的堆栈对齐方式使需要将某些参数传递空间对齐16的函数变得容易。
有类似__m128
的ABI保证有16字节的对齐 。 如果您定义了一个本地并获取其地址,并将该指针传递给其他某个函数,那么该本地需要充分对齐。 因此保持16字节的堆栈对齐与提供某些类型的16字节对齐并行,这显然是一个好主意。
现在, atomic<struct_of_16_bytes>
可以便宜地获得16字节对齐,这lock cmpxchg16b
,所以lock cmpxchg16b
不会跨越缓存行边界。 对于非常罕见的情况,你有一个自动存储的原子本地,并将指针传递给多个线程...
上一篇: Why does System V / AMD64 ABI mandate a 16 byte stack alignment?