为什么 x86-64 / AMD64 System V ABI 要求 16 字节堆栈对齐?
Why does the x86-64 / AMD64 System V ABI mandate a 16 byte stack alignment?
我在不同的地方读到它是为 "performance reasons" 完成的,但我仍然想知道在哪些特殊情况下通过这种 16 字节对齐可以提高性能。或者,无论如何,选择它的原因是什么。
编辑:我想我以误导的方式写了这个问题。我不是在问为什么处理器使用 16 字节对齐的内存执行速度更快,这在文档中到处都有解释。相反,我想知道的是强制 16 字节对齐比仅让程序员在需要时自行对齐堆栈更好。我问这个是因为根据我在汇编方面的经验,堆栈强制执行有两个问题:它只对执行代码的不到 1% 有用(因此在其他 99% 中实际上是开销);它也是一个非常常见的错误来源。所以我想知道它最终是如何真正得到回报的。虽然我对此仍有疑问,但我接受了 peter 的回答,因为它包含了我最初问题的最详细答案。
请注意 Linux 上使用的 i386 System V ABI 的当前版本也需要 16 字节堆栈对齐1.请参阅 https://sourceforge.net/p/fbc/bugs/659/ for some history, and my comment on https://gcc.gnu.org/bugzilla/show_bug.cgi?id=40838#c91 以尝试总结 i386 GNU/Linux + GCC 如何意外陷入 backwards-incompat 对 i386 System V ABI 的更改是两害相权取其轻的情况的不幸历史。
Windows x64 也需要在 call
之前进行 16 字节堆栈对齐,大概是出于与 x86-64 System V 类似的动机。
此外,semi-related:x86-64 System V 要求 16 字节和更大的全局数组按 16 对齐。对于 >= 16 字节或可变大小的本地数组也是如此,尽管该细节仅如果您知道传递给您的是数组开头的地址,而不是指向中间的指针,则跨函数相关。 ()。它不允许您对任意 int *
.
做出任何额外的假设
SSE2 是 x86-64 的基线,使 ABI 对 __m128
和编译器 auto-vectorization 等类型有效是其中之一设计目标,我想。 ABI 必须定义此类参数如何作为函数参数或通过引用传递。
16 字节对齐有时对堆栈上的局部变量(尤其是数组)很有用,保证 16 字节对齐意味着编译器可以在有用时免费获取它,即使源代码没有明确要求它.
如果不知道相对于 16 字节边界的堆栈对齐,则每个需要对齐局部的函数都需要一个 and rsp, -16
,以及 [=174= 的额外指令] rsp
在 rsp
的未知偏移量之后(0
或 -8
) 例如用完 rbp
作为帧指针。
没有 AVX,内存源操作数必须 16 字节对齐。例如paddd xmm0, [rsp+rdi]
如果内存操作数未对齐,则会出错。因此,如果不知道对齐方式,则必须使用 movups xmm1, [rsp+rdi]
/ paddd xmm0, xmm1
,或者编写循环序言/结尾来处理未对齐的元素。对于编译器想要 auto-vectorize 结束的本地数组,它可以简单地选择将它们对齐 16.
另请注意,早期的 x86 CPU(在 Nehalem / Bulldozer 之前)有一条 movups
指令比 movaps
慢,即使指针确实对齐也是如此。 (即对齐数据上的未对齐 loads/stores 非常慢,并且会阻止将负载折叠到 ALU 指令中。)(有关上述所有内容的更多信息,请参阅 Agner Fog's optimization guides, microarch guide, and instruction tables。)
这些因素是保证比仅仅“通常”保持堆栈对齐更有用的原因。 允许编写在未对齐堆栈上实际出错的代码允许更多优化机会。
对齐数组还可以加速矢量化 memcpy
/ strcmp
/ 任何 不能 假设 的函数对齐,而是检查它并可以直接跳转到它们的 whole-vector 循环。
来自a recent version of the x86-64 System V ABI (r252):
An array uses the same alignment as its elements, except that a local or global
array variable of length at least 16 bytes or a C99 variable-length array variable
always has alignment of at least 16 bytes.4
4 The alignment requirement allows the use of SSE instructions when operating on the array.
The compiler cannot in general calculate the size of a variable-length array (VLA), but it is expected
that most VLAs will require at least 16 bytes, so it is logical to mandate that VLAs have at
least a 16-byte alignment.
这有点激进,主要只在 auto-vectorize 可以内联的函数时有用,但通常还有其他局部变量,编译器可以填充任何间隙,这样就不会浪费堆栈 space.只要已知堆栈对齐,就不会浪费指令。 (显然,如果 ABI 设计者决定不需要 16 字节堆栈对齐,他们可能会忽略它。)
Spill/reload 共 __m128
当然,它可以自由地执行 alignas(16) char buf[1024];
或源 请求 16 字节对齐的其他情况。
还有 __m128
/ __m128d
/ __m128i
当地人。编译器可能无法将所有向量局部变量保存在寄存器中(例如,在函数调用中溢出,或者寄存器不足),因此它需要能够 spill/reload 它们 movaps
,或者作为ALU 指令的内存源操作数,出于上面讨论的效率原因。
Loads/stores 实际上是跨 cache-line 边界(64 字节)分割的,具有显着的延迟损失,并且对现代 CPU 的吞吐量损失也很小。加载需要来自 2 个独立缓存行的数据,因此需要两次访问缓存。 (并且可能有 2 次缓存未命中,但这对于堆栈内存来说很少见。)
我认为 movups
已经在较旧的 CPU 上为向量考虑了成本,但它仍然很糟糕。跨越 4k 页面边界 更 更糟(在 Skylake 之前的 CPU 上),如果加载或存储涉及 4k 边界两侧的字节,则加载或存储需要大约 100 个周期。 (还需要 2 次 TLB 检查。)自然对齐使得不可能跨越任何更宽的边界进行拆分,因此 16 字节对齐足以满足您使用 SSE2 所做的一切。
max_align_t
在 x86-64 系统 V ABI 中有 16 字节对齐,因为 long double
(10-byte/80-bit x87 ).由于某些奇怪的原因,它被定义为填充到 16 个字节,这与 sizeof(long double) == 10
的 32 位代码不同。 x87 10 字节 load/store 无论如何都相当慢(比如 Core2 上 double
或 float
的负载吞吐量的 1/3,1/6P4,或 K8 上的 1/8),但也许 cache-line 和页面拆分惩罚在较旧的 CPU 上非常糟糕,因此他们决定以这种方式定义它。我认为在现代 CPU(甚至可能是 Core2)上循环遍历 long double
的数组对于打包的 10 字节不会慢,因为 fld m80
比 cache-line 拆分是一个更大的瓶颈每 ~6.4 个元素。
实际上,ABI 是在硅可用于基准测试之前定义的 (back in ~2000),但那些 K8 数字与 K7 相同(32 位/64 位模式与此处无关)。使 long double
成为 16 字节确实可以使用 movaps
复制单个字节,即使您不能在 XMM 寄存器中对其进行任何操作。 (除了使用 xorps
/ andps
/ orps
操作符号位。)
相关:这个 max_align_t
定义意味着 malloc
在 x86-64 代码中总是 returns 16 字节对齐内存。这使您可以将它用于 _mm_load_ps
之类的 SSE 对齐加载,但此类代码在为 32 位编译时可能会中断,其中 alignof(max_align_t)
仅为 8。(使用 aligned_alloc
或其他。 )
其他 ABI 因素 包括在堆栈上传递 __m128
值(在 xmm0-7 具有前 8 个浮点数/向量 args 之后)。要求向量在内存中进行 16 字节对齐是有意义的,因此它们可以被被调用者有效地使用,并被调用者有效地存储。始终保持 16 字节堆栈对齐使得需要将某些 arg-passing space 对齐 16.
的函数变得容易
像 __m128
这样的类型 ABI 保证有 16 字节对齐。如果您定义一个局部变量并获取其地址,并将该指针传递给其他某个函数,则该局部变量需要充分对齐。因此,保持 16 字节堆栈对齐与为某些类型提供 16 字节对齐密切相关,这显然是个好主意。
如今,atomic<struct_of_16_bytes>
可以廉价地获得 16 字节对齐是件好事,因此 lock cmpxchg16b
永远不会跨越缓存行边界。对于极少数情况,您有一个带有自动存储的原子本地,并将指向它的指针传递给多个线程...
脚注 1:32 位 Linux
并非所有 32 位平台都打破了与现有二进制文件的向后兼容性,hand-written asm 的方式与 Linux 一样;一些 like i386 NetBSD 仍然只使用 i386 SysV ABI 原始版本的历史 4 字节堆栈对齐要求。
历史上的 4 字节堆栈对齐对于现代 CPU 上的高效 8 字节 double
来说也是不够的。未对齐的 fld
/ fstp
通常是有效的,除非它们跨越 cache-line 边界(像其他 loads/stores 一样),所以它并不可怕,但 naturally-aligned 很好。
甚至在 16 字节对齐正式成为 ABI 的一部分之前,GCC 就曾经在 32 位上启用 -mpreferred-stack-boundary=4
(2^4 = 16 字节)。这当前假定传入堆栈对齐为 16 字节(即使对于如果不是则将出错的情况),以及保留该对齐。我不确定历史 gcc 版本是否曾经尝试保留堆栈对齐而不依赖于 SSE code-gen 或 alignas(16)
对象的正确性。
ffmpeg 是一个 well-known 示例,它依赖于编译器为其提供堆栈对齐:what is "stack alignment"?,例如在 32 位 Windows.
现代 gcc 仍然在 main
的顶部发出代码以将堆栈对齐 16(即使在 Linux 上 ABI 保证内核以对齐的堆栈启动进程),但是不在任何其他功能的顶部。您可以使用 -mincoming-stack-boundary
告诉 gcc 在生成代码时假定堆栈的对齐方式。
古老的 gcc4.1 似乎并没有真正尊重 __attribute__((aligned(16)))
或 32
自动存储,即它不会费心对齐堆栈任何额外的 in this example on Godbolt,太旧了当谈到堆栈对齐时,gcc 有一种曲折的过去。我认为官方 Linux ABI 到 16 字节对齐的更改首先是 de-facto 更改,而不是 well-planned 更改。关于更改发生的时间,我还没有找到任何官方资料,但我认为是在 2005 年到 2010 年之间的某个时间,在 x86-64 变得流行并且 x86-64 System V ABI 的 16 字节堆栈对齐被证明有用之后。
起初是对 GCC code-gen 的更改,使用比 ABI 要求更多的对齐方式(即对 gcc-compiled 代码使用更严格的 ABI),但后来它被写入版本的 i386 System V ABI 保持在 https://github.com/hjl-tools/x86-psABI/wiki/X86-psABI(至少 Linux 是官方的)。
gcc4.5 可能是第一个具有 -mpreferred-stack-boundary=4
32 位和 64 位版本的版本。 Godbolt 上的 gcc4.1.2 和 gcc4.4.7 似乎以这种方式运行,因此可能更改已向后移植,或者 Matt Godbolt 使用更现代的配置配置了旧的 gcc。
我在不同的地方读到它是为 "performance reasons" 完成的,但我仍然想知道在哪些特殊情况下通过这种 16 字节对齐可以提高性能。或者,无论如何,选择它的原因是什么。
编辑:我想我以误导的方式写了这个问题。我不是在问为什么处理器使用 16 字节对齐的内存执行速度更快,这在文档中到处都有解释。相反,我想知道的是强制 16 字节对齐比仅让程序员在需要时自行对齐堆栈更好。我问这个是因为根据我在汇编方面的经验,堆栈强制执行有两个问题:它只对执行代码的不到 1% 有用(因此在其他 99% 中实际上是开销);它也是一个非常常见的错误来源。所以我想知道它最终是如何真正得到回报的。虽然我对此仍有疑问,但我接受了 peter 的回答,因为它包含了我最初问题的最详细答案。
请注意 Linux 上使用的 i386 System V ABI 的当前版本也需要 16 字节堆栈对齐1.请参阅 https://sourceforge.net/p/fbc/bugs/659/ for some history, and my comment on https://gcc.gnu.org/bugzilla/show_bug.cgi?id=40838#c91 以尝试总结 i386 GNU/Linux + GCC 如何意外陷入 backwards-incompat 对 i386 System V ABI 的更改是两害相权取其轻的情况的不幸历史。
Windows x64 也需要在 call
之前进行 16 字节堆栈对齐,大概是出于与 x86-64 System V 类似的动机。
此外,semi-related:x86-64 System V 要求 16 字节和更大的全局数组按 16 对齐。对于 >= 16 字节或可变大小的本地数组也是如此,尽管该细节仅如果您知道传递给您的是数组开头的地址,而不是指向中间的指针,则跨函数相关。 (int *
.
SSE2 是 x86-64 的基线,使 ABI 对 __m128
和编译器 auto-vectorization 等类型有效是其中之一设计目标,我想。 ABI 必须定义此类参数如何作为函数参数或通过引用传递。
16 字节对齐有时对堆栈上的局部变量(尤其是数组)很有用,保证 16 字节对齐意味着编译器可以在有用时免费获取它,即使源代码没有明确要求它.
如果不知道相对于 16 字节边界的堆栈对齐,则每个需要对齐局部的函数都需要一个 and rsp, -16
,以及 [=174= 的额外指令] rsp
在 rsp
的未知偏移量之后(0
或 -8
) 例如用完 rbp
作为帧指针。
没有 AVX,内存源操作数必须 16 字节对齐。例如paddd xmm0, [rsp+rdi]
如果内存操作数未对齐,则会出错。因此,如果不知道对齐方式,则必须使用 movups xmm1, [rsp+rdi]
/ paddd xmm0, xmm1
,或者编写循环序言/结尾来处理未对齐的元素。对于编译器想要 auto-vectorize 结束的本地数组,它可以简单地选择将它们对齐 16.
另请注意,早期的 x86 CPU(在 Nehalem / Bulldozer 之前)有一条 movups
指令比 movaps
慢,即使指针确实对齐也是如此。 (即对齐数据上的未对齐 loads/stores 非常慢,并且会阻止将负载折叠到 ALU 指令中。)(有关上述所有内容的更多信息,请参阅 Agner Fog's optimization guides, microarch guide, and instruction tables。)
这些因素是保证比仅仅“通常”保持堆栈对齐更有用的原因。 允许编写在未对齐堆栈上实际出错的代码允许更多优化机会。
对齐数组还可以加速矢量化 memcpy
/ strcmp
/ 任何 不能 假设 的函数对齐,而是检查它并可以直接跳转到它们的 whole-vector 循环。
来自a recent version of the x86-64 System V ABI (r252):
An array uses the same alignment as its elements, except that a local or global array variable of length at least 16 bytes or a C99 variable-length array variable always has alignment of at least 16 bytes.4
4 The alignment requirement allows the use of SSE instructions when operating on the array. The compiler cannot in general calculate the size of a variable-length array (VLA), but it is expected that most VLAs will require at least 16 bytes, so it is logical to mandate that VLAs have at least a 16-byte alignment.
这有点激进,主要只在 auto-vectorize 可以内联的函数时有用,但通常还有其他局部变量,编译器可以填充任何间隙,这样就不会浪费堆栈 space.只要已知堆栈对齐,就不会浪费指令。 (显然,如果 ABI 设计者决定不需要 16 字节堆栈对齐,他们可能会忽略它。)
Spill/reload 共 __m128
当然,它可以自由地执行 alignas(16) char buf[1024];
或源 请求 16 字节对齐的其他情况。
还有 __m128
/ __m128d
/ __m128i
当地人。编译器可能无法将所有向量局部变量保存在寄存器中(例如,在函数调用中溢出,或者寄存器不足),因此它需要能够 spill/reload 它们 movaps
,或者作为ALU 指令的内存源操作数,出于上面讨论的效率原因。
Loads/stores 实际上是跨 cache-line 边界(64 字节)分割的,具有显着的延迟损失,并且对现代 CPU 的吞吐量损失也很小。加载需要来自 2 个独立缓存行的数据,因此需要两次访问缓存。 (并且可能有 2 次缓存未命中,但这对于堆栈内存来说很少见。)
我认为 movups
已经在较旧的 CPU 上为向量考虑了成本,但它仍然很糟糕。跨越 4k 页面边界 更 更糟(在 Skylake 之前的 CPU 上),如果加载或存储涉及 4k 边界两侧的字节,则加载或存储需要大约 100 个周期。 (还需要 2 次 TLB 检查。)自然对齐使得不可能跨越任何更宽的边界进行拆分,因此 16 字节对齐足以满足您使用 SSE2 所做的一切。
max_align_t
在 x86-64 系统 V ABI 中有 16 字节对齐,因为 long double
(10-byte/80-bit x87 ).由于某些奇怪的原因,它被定义为填充到 16 个字节,这与 sizeof(long double) == 10
的 32 位代码不同。 x87 10 字节 load/store 无论如何都相当慢(比如 Core2 上 double
或 float
的负载吞吐量的 1/3,1/6P4,或 K8 上的 1/8),但也许 cache-line 和页面拆分惩罚在较旧的 CPU 上非常糟糕,因此他们决定以这种方式定义它。我认为在现代 CPU(甚至可能是 Core2)上循环遍历 long double
的数组对于打包的 10 字节不会慢,因为 fld m80
比 cache-line 拆分是一个更大的瓶颈每 ~6.4 个元素。
实际上,ABI 是在硅可用于基准测试之前定义的 (back in ~2000),但那些 K8 数字与 K7 相同(32 位/64 位模式与此处无关)。使 long double
成为 16 字节确实可以使用 movaps
复制单个字节,即使您不能在 XMM 寄存器中对其进行任何操作。 (除了使用 xorps
/ andps
/ orps
操作符号位。)
相关:这个 max_align_t
定义意味着 malloc
在 x86-64 代码中总是 returns 16 字节对齐内存。这使您可以将它用于 _mm_load_ps
之类的 SSE 对齐加载,但此类代码在为 32 位编译时可能会中断,其中 alignof(max_align_t)
仅为 8。(使用 aligned_alloc
或其他。 )
其他 ABI 因素 包括在堆栈上传递 __m128
值(在 xmm0-7 具有前 8 个浮点数/向量 args 之后)。要求向量在内存中进行 16 字节对齐是有意义的,因此它们可以被被调用者有效地使用,并被调用者有效地存储。始终保持 16 字节堆栈对齐使得需要将某些 arg-passing space 对齐 16.
像 __m128
这样的类型 ABI 保证有 16 字节对齐。如果您定义一个局部变量并获取其地址,并将该指针传递给其他某个函数,则该局部变量需要充分对齐。因此,保持 16 字节堆栈对齐与为某些类型提供 16 字节对齐密切相关,这显然是个好主意。
如今,atomic<struct_of_16_bytes>
可以廉价地获得 16 字节对齐是件好事,因此 lock cmpxchg16b
永远不会跨越缓存行边界。对于极少数情况,您有一个带有自动存储的原子本地,并将指向它的指针传递给多个线程...
脚注 1:32 位 Linux
并非所有 32 位平台都打破了与现有二进制文件的向后兼容性,hand-written asm 的方式与 Linux 一样;一些 like i386 NetBSD 仍然只使用 i386 SysV ABI 原始版本的历史 4 字节堆栈对齐要求。
历史上的 4 字节堆栈对齐对于现代 CPU 上的高效 8 字节 double
来说也是不够的。未对齐的 fld
/ fstp
通常是有效的,除非它们跨越 cache-line 边界(像其他 loads/stores 一样),所以它并不可怕,但 naturally-aligned 很好。
甚至在 16 字节对齐正式成为 ABI 的一部分之前,GCC 就曾经在 32 位上启用 -mpreferred-stack-boundary=4
(2^4 = 16 字节)。这当前假定传入堆栈对齐为 16 字节(即使对于如果不是则将出错的情况),以及保留该对齐。我不确定历史 gcc 版本是否曾经尝试保留堆栈对齐而不依赖于 SSE code-gen 或 alignas(16)
对象的正确性。
ffmpeg 是一个 well-known 示例,它依赖于编译器为其提供堆栈对齐:what is "stack alignment"?,例如在 32 位 Windows.
现代 gcc 仍然在 main
的顶部发出代码以将堆栈对齐 16(即使在 Linux 上 ABI 保证内核以对齐的堆栈启动进程),但是不在任何其他功能的顶部。您可以使用 -mincoming-stack-boundary
告诉 gcc 在生成代码时假定堆栈的对齐方式。
古老的 gcc4.1 似乎并没有真正尊重 __attribute__((aligned(16)))
或 32
自动存储,即它不会费心对齐堆栈任何额外的 in this example on Godbolt,太旧了当谈到堆栈对齐时,gcc 有一种曲折的过去。我认为官方 Linux ABI 到 16 字节对齐的更改首先是 de-facto 更改,而不是 well-planned 更改。关于更改发生的时间,我还没有找到任何官方资料,但我认为是在 2005 年到 2010 年之间的某个时间,在 x86-64 变得流行并且 x86-64 System V ABI 的 16 字节堆栈对齐被证明有用之后。
起初是对 GCC code-gen 的更改,使用比 ABI 要求更多的对齐方式(即对 gcc-compiled 代码使用更严格的 ABI),但后来它被写入版本的 i386 System V ABI 保持在 https://github.com/hjl-tools/x86-psABI/wiki/X86-psABI(至少 Linux 是官方的)。
-mpreferred-stack-boundary=4
32 位和 64 位版本的版本。 Godbolt 上的 gcc4.1.2 和 gcc4.4.7 似乎以这种方式运行,因此可能更改已向后移植,或者 Matt Godbolt 使用更现代的配置配置了旧的 gcc。