当前位置: 首页 > 知识库问答 >
问题:

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

何昆
2023-03-14

我在不同的地方读到过这样做是出于“性能原因”,但我仍然想知道这种16字节对齐方式在哪些特定情况下提高了性能。或者,无论如何,选择它的原因是什么。

编辑:我认为我写这个问题的方式有误导性。我不是在问为什么处理器使用16字节对齐的内存会更快,这在文档中随处都有解释。相反,我想知道的是,强制的16字节对齐如何优于在需要时让程序员自己对齐堆栈。我这样问是因为根据我在汇编方面的经验,堆栈强制有两个问题:它只对执行的代码中不到1%的代码有用(因此在其他99%的代码中实际上是开销);这也是一个非常常见的错误来源。所以我想知道它到底是如何得到回报的。虽然我对此仍有疑问,但我接受彼得的回答,因为它包含了对我最初问题的最详细的回答。

共有1个答案

窦宏旷
2023-03-14

请注意,Linux上使用的i386 System V ABI的当前版本还需要16字节堆栈对齐。看见https://sourceforge.net/p/fbc/bugs/659/对于一些历史,我的评论https://gcc.gnu.org/bugzilla/show_bug.cgi?id=40838#c91为了总结i386 GNU/Linux GCC如何意外地陷入这样一种情况的不幸历史,i386系统V ABI的向后不匹配更改是两害中较小的。

在调用之前,Windows x64还需要16字节堆栈对齐,可能是出于与x86-64 System V类似的动机。

此外,半相关:x86-64 System V要求16字节及以上的全局数组按16对齐。对于的本地阵列相同

SSE2是x86-64的基线,我认为,使ABI对类型(如m128)和编译器自动矢量化有效是设计目标之一。ABI必须定义如何将此类参数作为函数参数传递或通过引用传递。

16字节对齐有时对堆栈上的局部变量(尤其是数组)有用,保证16字节对齐意味着编译器可以在任何时候免费获得它,即使源代码没有明确请求它。

如果不知道相对于16字节边界的堆栈对齐方式,则每个需要对齐局部的函数都需要一个和rsp,-16,以及额外的指令,以在未知的rsp偏移量(0或8)后保存/恢复rsp,例如,对帧指针使用uprbp。

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

还要注意的是,早期的x86 CPU(Nehalem/推土机之前)有一条movups指令,即使指针确实对齐,它也比movaps指令慢。(即,对齐数据上未对齐的加载/存储速度非常慢,并且防止将加载折叠到ALU指令中。)(有关以上所有内容的更多信息,请参阅Agner Fog的优化指南、微阵列指南和指令表。)

这些因素就是为什么保证比“通常”保持堆栈对齐更有用的原因。允许在未对齐的堆栈上生成实际出现故障的代码,可以提供更多优化机会。

对齐数组还可以加速矢量化的memcpy,无论什么函数不能假定对齐,但可以检查它并直接跳到它们的整个向量循环。

来自x86-64 System V ABI(r252)的最新版本:

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

对齐要求允许在阵列上操作时使用SSE指令。编译器通常无法计算可变长度数组(VLA)的大小,但预计大多数VLA至少需要16字节,因此要求VLA至少具有16字节对齐是合乎逻辑的。

这有点咄咄逼人,而且只在自动矢量化的函数可以内联时才有帮助,但通常编译器可以将其他局部变量塞进任何间隙中,这样就不会浪费堆栈空间。只要存在已知的堆栈对齐,就不会浪费指令。(显然,如果ABI设计者决定不需要16字节堆栈对齐,他们可能会忽略这一点。)

当然,它可以自由地执行alignas(16)char buf[1024] 或源请求16字节对齐的其他情况。

还有局部变量。编译器可能无法在寄存器中保留所有向量局部变量(例如,在函数调用中溢出,或寄存器不足),因此出于上述效率原因,它需要能够使用MOVAP溢出/重新加载它们,或作为ALU指令的内存源操作数。

实际上跨越缓存线边界(64字节)拆分的负载/存储会造成严重的延迟损失,并且对现代CPU的吞吐量也会造成轻微的损失。负载需要来自2个单独缓存线的数据,因此需要两次访问缓存。(可能还有2次缓存未命中,但这对于堆栈内存来说很少见。)

我认为movup已经为旧CPU上的向量付出了代价,这很昂贵,但仍然很糟糕。跨越4k页面边界要糟糕得多(在Skylake之前的CPU上),如果加载或存储触及4k边界两侧的字节,则需要大约100个周期。(还需要2次TLB检查。)自然对齐使得跨任何更宽边界的拆分都不可能,因此16字节对齐足以满足您使用SSE2所能做的一切。

max_align_t在x86-64 System V ABI中具有16字节对齐,因为long双(10字节/80位x87)。出于某种奇怪的原因,它被定义为填充到16字节,不像32位代码中的sizeof(long双)==10。x87 10字节加载/存储无论如何都非常慢(例如Core2上的浮点的加载吞吐量的1/3,P4上的1/6,或K8上的1/8),但可能缓存行和页面拆分惩罚在旧CPU上非常糟糕,以至于他们决定这样定义它。我认为在现代CPU(甚至可能是Core2)上,循环遍历一个长双数组并没有用打包的10字节慢,因为fld m80将比每6.4个元素拆分一次缓存行更大的瓶颈。

实际上,ABI是在硅可用于基准测试之前定义的(早在2000年),但这些K8数字与K7相同(32位/64位模式在这里无关紧要)。使长双16字节确实可以使用movaps复制单个数字,即使您在XMM寄存器中无法使用它做任何事情。(除了使用xorps/和ps/orps操作符号位。)

相关:这个定义意味着在x86-64代码中总是返回16字节对齐的内存。这使您可以将其用于SSE对齐的负载,如\u mm\u load\u ps,但当编译为32位时,此类代码可能会中断,其中alignof(max\u align\u t)仅为8。(使用aligned\u alloc或任何东西。)

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

有些类型,如__m128,ABI保证具有16字节对齐。如果您定义一个本地并获取它的地址,并将该指针传递给其他函数,则该本地需要充分对齐。因此,保持16字节堆栈对齐与给某些类型16字节对齐齐头并进,这显然是个好主意。

这些天,很高兴原子

并非所有32位平台都像Linux那样破坏了与现有二进制文件和手写asm的向后兼容性;有些像i386 NetBSD仍然只使用i386 SysV ABI原始版本中的历史4字节堆栈对齐要求。

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

甚至在16字节对齐正式成为ABI的一部分之前,GCC就用于在32位上启用MPrefered stack boundary=4(2^4=16字节)。目前,这假设传入堆栈对齐为16字节(即使在不对齐的情况下也会出现故障),并保留该对齐。我不确定历史gcc版本是否在不依赖SSE code gen或alignas(16)对象的正确性的情况下尝试保留堆栈对齐。

ffmpeg是一个众所周知的例子,它依赖于编译器来实现堆栈对齐:什么是“堆栈对齐”?,e、 g.在32位Windows上。

现代gcc仍然在main的顶部发出代码以将堆栈对齐16(即使在ABI保证内核以对齐的堆栈启动进程的Linux),但不会在任何其他函数的顶部发出代码。您可以使用-mincoming-stack-边界告诉gcc在生成代码时应该假设堆栈的对齐程度。

古老的gcc4.1似乎并不真正尊重自动存储的属性((aligned(16))32,也就是说,在这个例子中,它不需要在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和@ThomasJager报告说,gcc4.5可能是第一个对32位和64位都有-mperite-stack-边界=4的版本。gobolt上的gcc4.1.2和gcc4.4.7似乎是这样的行为,所以可能更改是向后移植的,或者Matt gobolt用更现代的配置配置了旧的gcc。

 类似资料:
  • libdyld.dylib`STACK_NOT_16_BYTE_ALIGNED_ERROR:->0x7FFFC12DA2FA<+0>:movdqa%xMM0,(%RSP)0x7FFFC12DA2FF<+5>:int3 libdyld.dylib`_dyld_func_lookup:0x7fffc12da300<+0>:pushq%rbp 0x7fffc12da301<+1>:movq%rsp,%r

  • 问题内容: 我的用例需要一个数据结构。我应该能够将项目推送到数据结构中,而我只想从堆栈中检索最后一个项目。该堆栈的JavaDoc说: Deque接口及其实现提供了一组更完整和一致的LIFO堆栈操作,应优先使用此类。例如: 我绝对不希望这里出现同步行为,因为我将使用方法本地的数据结构。除了这个,我为什么还要在这里呢? PS:Deque的Javadoc说: 双端队列也可以用作LIFO(后进先出)堆栈。

  • 我需要一个数据结构用于我的用例。我应该能够将项目推入数据结构,并且我只想从堆栈中检索最后一个项目。JavaDoc for Stack表示:

  • 为什么第一个成功而第二个失败?人们可能期望它们产生相同的输出。

  • 问题内容: 我是Hadoop / ZooKeeper的新手。我不明白将ZooKeeper与Hadoop结合使用的目的,ZooKeeper是否在Hadoop中写入数据?如果不是,那么为什么我们将ZooKeeper与Hadoop一起使用? 问题答案: Hadoop 1.x不使用Zookeeper。即使在Hadoop 1.x安装中,HBase也会使用zookeeper。 Hadoop从2.0版开始也采用

  • 我在学Windows上的汇编,想弄清楚栈上的值是什么。< br > Visual C #文档说明高于RSP的值是: 分配空间 保存了RBP 返回地址 注册主页(RCX、RDX、R8、R9) 函数参数 问题是堆栈中有32个额外的字节,文档中没有提到。 在内存快照中,RSP从0x0000000000DAF5E0开始。彩色框为: 黄色:两个值为9的64位变量 白色:保存旧RBP返回地址 蓝色:函数参数