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

时间:2018-03-20 17:48:44

标签: assembly x86-64 abi

我在不同的地方读到它是出于“性能原因”而完成的,但我仍然想知道这个16字节对齐可以改善性能的具体情况是什么。或者,无论如何,选择这个的原因是什么。

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

1 个答案:

答案 0 :(得分:10)

请注意, Linux上使用的当前版本的i386 System V ABI还需要16字节堆栈对齐 1 。有关历史记录,请参阅https://sourceforge.net/p/fbc/bugs/659/

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

16字节对齐有时对堆栈上的局部变量(尤其是数组)有用,并且保证16字节对齐意味着编译器可以在任何有用的情况下免费获得它,即使源没有这样做也是如此。明确要求它。

如果相对于16字节边界的堆栈对齐未知,则每个需要对齐本地的函数都需要and rsp, -16,以及额外的保存/恢复指令{{1在rsprsp0)之后的未知偏移量之后。,例如使用-8作为帧指针。

如果没有AVX,内存源操作数必须是16字节对齐的。例如如果内存操作数未对齐,则rbp出现错误。因此,如果对齐尚未知,则您必须使用paddd xmm0, [rsp+rdi] / movups xmm1, [rsp+rdi],或者编写循环序言/结尾以处理未对齐的元素。对于编译器想要自动向量化的局部数组,它可以简单地选择将它们对齐16。

另请注意,早期的x86 CPU(在Nehalem / Bulldozer之前)的paddd xmm0, xmm1指令慢于movups,即使指针确实已对齐。 (即,对齐数据上的未对齐加载/存储特别慢,以及防止将折叠加载到ALU指令中)。 (有关上述所有内容的详情,请参阅Agner Fog's optimization guides, microarch guide, and instruction tables。)

这些因素是保证比仅仅"通常"更有用的原因。保持堆栈对齐。 允许编写实际错误的堆栈代码可以提供更多优化机会。

对齐数组还可以加速向量化movaps / memcpy /无论函数<\ n> 对齐的函数,而是检查它可以直接跳到它们的整个矢量循环。

来自a recent version of the x86-64 System V ABI (r252)

  

数组使用与其元素相同的对齐方式,但本地或全局除外   长度至少为16个字节的数组变量或C99可变长度数组变量   始终具有至少16个字节的对齐。 4

     

4 对齐要求允许在阵列上操作时使用SSE指令。   编译器通常不能计算可变长度数组(VLA)的大小,但它是预期的   大多数VLA需要至少16个字节,因此强制要求VLA具有   至少16字节对齐。

这有点过于激进,并且只有在自动向量化的函数可以内联时才有用,但通常还有其他本地编译器可以填充到任何间隙中,因此它不会浪费堆栈空间。只要有已知的堆栈对齐,就不会浪费指令。 (显然,如果他们决定不要求16字节堆栈对齐,那么ABI设计人员可能会将其排除在外。)

strcmp

的溢出/重新加载

当然,它可以自由地执行__m128或源请求 16字节对齐的其他情况。

还有alignas(16) char buf[1024]; / __m128 / __m128d本地人。编译器可能无法将所有向量局部保留在寄存器中(例如,溢出函数调用,或者没有足够的寄存器),因此它需要能够使用__m128i溢出/重新加载它们,或者作为内存源ALU指令的操作数,出于上面讨论的效率原因。

实际上在高速缓存行边界(64字节)上拆分的负载/存储具有显着的延迟惩罚,并且对现代CPU也有轻微的吞吐量损失。加载需要来自2个独立缓存行的数据,因此需要对缓存进行两次访问。 (可能还有2次缓存未命中,但堆栈内存很少见。)

我认为movaps已经为那些昂贵的旧CPU上的矢量带来了成本,但它仍然很糟糕。跨越4k页面边界很多更糟(在Skylake之前的CPU上),如果它接触4k边界两侧的字节,则加载或存储需要约100个周期。 (还需要2次TLB检查)。 自然对齐使得在任何更宽的边界上分割都不可能,因此16字节对齐就足以满足您对SSE2所做的一切。

由于movups(10字节/ 80位x87),

max_align_t在x86-64 System V ABI中具有16字节对齐。由于某些奇怪的原因,它被定义为填充到16个字节,这与long double的32位代码不同。 x87 10字节加载/存储无论如何都很慢(比如Core2上sizeof(long double) == 10double的负载吞吐量的1/3,P4上的1/6或K8上的1/8,但也许缓存行和页面拆分惩罚在较旧的CPU上是如此糟糕,以至于他们决定以这种方式定义它。我认为在现代CPU(甚至可能是Core2)上循环遍历float数组并不会因打包10字节而变慢,因为long double会比每个缓存行拆分更大的瓶颈〜 6.4元素。

实际上,ABI是在硅可用于基准测试(back in ~2000)之前定义的,但是那些K8数字与K7相同(32位/ 64位模式在这里无关紧要)。使fld m80 16字节确实可以复制带有long double的单个字节,即使您无法在XMM寄存器中对其执行任何操作。 (除非使用movaps / xorps / andps操纵符号位)

相关:此orps定义意味着max_align_t始终在x86-64代码中返回16字节对齐的内存。这使得您可以将它用于SSE对齐的负载,例如malloc,但是当编译为32位而_mm_load_ps仅为8时,此类代码可能会中断。(使用alignof(max_align_t)或其他)

其他ABI因子包括在堆栈上传递aligned_alloc值(在xmm0-7之后有前8个浮点数/向量args)。对内存中的向量要求16字节对齐是有意义的,因此被调用者可以有效地使用它们,并且由调用者有效地存储它们。始终保持16字节堆栈对齐使得需要将一些arg传递空间对齐16的函数变得容易。

__m128这样的类型,ABI保证有16字节对齐。如果你定义一个local并获取它的地址,并将该指针传递给其他函数,那么本地需要充分对齐。因此,保持16字节的堆栈对齐与提供一些类型的16字节对齐密切相关,这显然是一个好主意。

现在,__m128可以便宜地获得16字节对齐,因此atomic<struct_of_16_bytes>无法跨越缓存行边界,这很不错。对于极少数情况,你有一个带有自动存储的原子本地,并且你将指针传递给多个线程......

脚注1:32位Linux

并非所有32位平台都破坏了与现有二进制文件的兼容性,并以Linux的方式手写。某些like i386 NetBSD仍然只使用原始版本的i386 SysV ABI中的历史4字节堆栈对齐要求。

历史的4字节堆栈对齐也不足以在现代CPU上实现高效的8字节lock cmpxchg16b。未对齐double / fld通常是有效的,除非它们越过缓存行边界(像其他加载/存储一样),所以它并不可怕,但自然对齐很好。

即使在16字节对齐正式成为ABI的一部分之前,GCC也曾在32位上启用fstp(2 ^ 4 = 16字节)。这当前假设传入的堆栈对齐是16个字节(即使对于没有错误的情况),以及保留该对齐。我不确定历史gcc版本是否曾经尝试保留堆栈对齐而不依赖于SSE代码生成或-mpreferred-stack-boundary=4对象的正确性。

ffmpeg是一个众所周知的例子,依赖于编译器给它进行堆栈对齐:what is "stack alignment"?,例如在32位Windows上。

现代gcc仍会在alignas(16)的顶部发出代码,使堆栈与16对齐(即使在ABI保证内核以对齐的堆栈启动进程的Linux上),但不在任何顶部其他功能。您可以使用main告诉gcc在生成代码时它应该假设堆栈是如何对齐的。

古代gcc4.1似乎并没有真正尊重-mincoming-stack-boundary__attribute__((aligned(16)))进行自动存储,也就是说它不会额外对齐任何额外的in this example on Godbolt,所以当谈到堆栈对齐时,老gcc有一种格格不入的过去。我认为官方Linux ABI到16字节对齐的变化首先发生在事实上的变化,而不是一个精心策划的变更。在变化发生的时候我还没有发现任何正式的事情,但在2005年到2010年的某个地方我认为,在x86-64变得流行之后,x86-64 System V ABI的16字节堆栈对齐被证明是有用的

起初,对GCC代码生成的改变是使用比ABI要求更多的对齐(即对gcc编译的代码使用更严格的ABI),但后来它写入了版本的i386 System V ABI维持在https://github.com/hjl-tools/x86-psABI/wiki/X86-psABI(至少是Linux官方版)。

@MichaelPetch and @ThomasJager report gcc4.5可能是32位和64位32的第一个版本。 Godbolt上的gcc4.1.2和gcc4.4.7似乎表现得那样,所以也许这个改变是向后移植的,或者Matt Godbolt用更现代的配置配置旧的gcc。