为什么System V / AMD64 ABI要求16字节的堆栈对齐?

我已经在不同的地方阅读过它是为了“性能原因”而完成的,但我仍然想知道通过这种16字节对齐性能得到改进的特定情况是什么。 或者,无论如何,选择这个的原因是什么。

编辑 :我在想我以误导的方式写下了这个问题。 我并没有问及为什么处理器能够以16字节对齐的内存更快地完成任务,这在文档中无处不在。 我想知道的是,强制执行的16字节对齐方式比只让程序员在需要时自行调整堆栈的方式更好。 我在问这个问题是因为根据我的汇编经验,堆栈执行有两个问题:只有少于执行代码的1%(因此其他99%实际开销); 它也是一个非常常见的错误来源。 所以我想知道它最终如何真正得到回报。 虽然我仍然对此有疑问,但我接受彼得的答案,因为它包含了我最初的问题最详细的答案。


我认为,SSE2是x86-64的基准,并且使ABI对像__m128这样的类型以及编译器自动向量化有效,这是我设计的目标之一。 ABI必须定义如何将这些参数作为函数参数传递,或者通过引用来传递。


对于堆栈中的局部变量(特别是数组),16字节的对齐有时是有用的,并且保证16字节的对齐意味着即使源没有明确地请求它,编译器也可以在有用时免费获得它。

如果不知道相对于16字节边界的堆栈对齐,则每个需要对齐本地的函数都需要一个and rsp, -16和额外的指令,以便在未知的rsp偏移( 00之后保存/恢复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上的doublefloat的负载吞吐量的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不会跨越缓存行边界。 对于非常罕见的情况,你有一个自动存储的原子本地,并将指针传递给多个线程...

链接地址: http://www.djcxy.com/p/80331.html

上一篇: Why does System V / AMD64 ABI mandate a 16 byte stack alignment?

下一篇: Safely override C++ virtual functions