我在不同的地方读到过这样做是出于"性能原因",但我仍然想知道这种16字节对齐方式在哪些特定情况下可以提高性能,或者,无论如何,选择这种方式的原因是什么。
- 编辑**:我想我写这个问题的方式是误导的。我不是问为什么处理器用16字节对齐的内存做事情更快,这在文档中到处都有解释。相反,我想知道的是,强制的16字节对齐比让程序员在需要时自己对齐堆栈更好。我问这个问题是因为从我的汇编经验来看,堆栈实施具有两个问题:它只对所执行代码的不到1%有用(因此,其余99%实际上是开销);这也是一个非常常见的bug来源。所以我想知道它最终是如何真正得到回报的。虽然我对此仍有疑问,但我接受peter的回答,因为它包含了对我最初问题的最详细的回答。
1条答案
按热度按时间bybem2ql1#
有关如何编写遵循ABI的asm的详细信息,请参见 * glibc scanf Segmentation faults when called from a function that doesn't align RSP *。(
scanf
只是函数的一个示例,其中库中编译器生成的asm依赖于ABI保证,使用movaps
一次将16个字节复制到堆栈上的局部变量,或从堆栈上的局部变量复制16个字节。)注意Linux上使用的i386 System V ABI的当前版本也需要16字节堆栈对齐1。有关历史信息,请参见https://sourceforge.net/p/fbc/bugs/659/,我在www.example.com上的评论https://gcc.gnu.org/bugzilla/show_bug.cgi?id=40838#c91试图总结i386 GNU/Linux + GCC如何意外地陷入向后-i386 System V ABI的不兼容变更是两害相权取其轻。
Windows x64也要求在
call
之前进行16字节的堆栈对齐,大概是出于与x86-64 System V类似的动机。此外,半相关:x86-64 System V要求16字节或更大的全局数组按16对齐。对于〉= 16字节或可变大小的局部数组也是如此,尽管该细节仅在您知道传递给您的是数组起始地址的情况下才与函数相关。而不是指向中间的指针。(Different memory alignment for different buffer sizes)。它不允许你对任意的
int *
做任何额外的假设。SSE 2是x86-64的基线,我认为设计目标之一是使ABI对
__m128
这样的类型和编译器自动向量化有效。ABI必须定义如何将这些参数作为函数参数传递,或者通过引用传递。16-字节对齐有时对堆栈上的局部变量(尤其是数组)很有用,保证16字节对齐意味着编译器可以在任何有用的时候免费获得它,即使源代码没有显式地请求它。
**如果相对于16字节边界的堆栈对齐未知,则每个需要对齐局部变量的函数都需要一个
and rsp, -16
,以及额外的指令,以便在rsp
(0
或-8
)的未知偏移量之后保存/恢复rsp
**例如,将rbp
用于帧指针。如果没有AVX,内存源操作数必须是16字节对齐的。例如,如果内存操作数未对齐,则
paddd xmm0, [rsp+rdi]
错误。因此,如果对齐未知,则必须使用movups xmm1, [rsp+rdi]
/paddd xmm0, xmm1
,或编写循环序言/尾声来处理未对齐的元素。对于编译器想要自动矢量化的局部数组,它可以简单地选择以16排列它们。还要注意早期的x86 CPU(Nehalem / Bulldozer之前)有一条
movups
指令,即使指针被对齐,它也比movaps
慢(也就是说,对齐数据上的未对齐加载/存储非常慢,并且阻止了将加载折叠到ALU指令中)(参见Agner Fog's optimization guides, microarch guide, and instruction tables以了解以上所有内容)。这些因素就是为什么保证比仅仅“通常”保持堆栈对齐更有用的原因。允许编写在未对齐的堆栈上实际出错的代码可以提供更多的优化机会。
对齐数组还可以加快矢量化的
memcpy
/strcmp
/无论什么函数的速度,这些函数不能 * 假定 * 对齐,而是检查它,并可以直接跳转到它们的全向量循环。从a recent version of the x86-64 System V ABI (r252)开始:
数组使用与其元素相同的对齐方式,除了长度至少为16个字节的局部或全局数组变量或C99可变长度数组变量始终具有至少16个字节的对齐方式。4
4对齐要求允许在操作数组时使用SSE指令。编译器通常无法计算可变长度数组(VLA)的大小,但预计大多数VLA至少需要16字节,因此要求VLA至少有16字节对齐是合乎逻辑的。
这有点激进,而且大多数情况下只有当自动向量化的函数可以内联时才有帮助,但通常编译器可以在任何间隙中填充其他局部变量,这样就不会浪费堆栈空间,而且只要堆栈对齐已知,就不会浪费指令(显然,如果ABI设计者决定不需要16字节堆栈对齐,他们可能会忽略这一点)。
溢出/重新装载
__m128
当然,它可以自由地执行
alignas(16) char buf[1024];
或其他源 * 请求 * 16字节对齐的情况。还有
__m128
/__m128d
/__m128i
局部变量,编译器可能无法将所有向量局部变量保存在寄存器中(例如,在函数调用中溢出,或寄存器不足),因此,出于上面讨论的效率原因,编译器需要能够使用movaps
溢出/重新加载它们,或作为ALU指令的存储器源操作数。在现代CPU上,实际上跨缓存行边界(64字节)拆分的加载/存储会有很大的延迟损失,也会有很小的吞吐量损失。加载需要来自2个独立缓存行的数据,因此需要该高速缓存进行两次访问。(可能还有2次缓存未命中,但这对于堆栈内存来说很少见。)
我认为
movups
已经为旧CPU上的向量计算出了这个成本,但它仍然很糟糕。跨越4k页面边界要糟糕得多(在Skylake之前的CPU上),如果加载或存储触及4k边界两侧的字节,则需要大约100个周期。(还需要2次TLB检查。)自然对齐使拆分无法跨越任何更宽的边界,因此16字节对齐足以满足您使用SSE 2所能做的一切。由于
long double
,max_align_t
在x86-64系统V ABI中具有16字节对齐(10字节/80位x87)。由于某些奇怪的原因,它被定义为填充到16字节,不像在32位代码中sizeof(long double) == 10
.x8710字节加载/存储无论如何都相当慢(例如,Core 2上double
或float
负载吞吐量的1/3,P4上的1/6或K8上的1/8),但也许缓存行和页分割的代价在旧CPU上太严重了,以至于他们决定这样定义它。(甚至可能是Core 2)在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字节对齐的内存。这使您可以在SSE对齐的加载(如_mm_load_ps
)中使用它,但这样的代码在编译为32位时可能会中断,其中alignof(max_align_t)
仅为8。(使用aligned_alloc
或其他代码。)其他ABI因素包括在堆栈上传递
__m128
值(在xmm 0 -7具有前8个float / vector参数之后)。在内存中需要16字节的向量对齐是有意义的,这样被调用者就可以有效地使用它们,并由调用方有效地存储。始终保持16字节堆栈对齐使需要对齐某些arg-以16的速度穿越太空。有一些类型,如
__m128
,ABI保证有16字节对齐。如果你定义了一个局部变量,取它的地址,并将指针传递给其他函数,那么这个局部变量需要充分对齐。所以保持16字节的堆栈对齐与给一些类型16字节对齐是齐头并进的,这显然是一个好主意。现在,
atomic<struct_of_16_bytes>
可以很便宜地获得16字节对齐,所以lock cmpxchg16b
永远不会跨越缓存线边界,对于非常罕见的情况,您有一个自动存储的原子本地,并且您将指向它的指针传递给多个线程...脚注1:32位Linux
并不是所有的32位平台都像Linux那样破坏了与现有二进制文件和手写asm的向后兼容性;某些like i386 NetBSD仍然只使用i386 SysV ABI原始版本的历史4字节堆栈对齐要求。
历史上的4字节堆栈对齐也不足以在现代CPU上实现高效的8字节
double
。未对齐的fld
/fstp
通常是高效的,除非它们跨越缓存行边界(如其他加载/存储),所以这并不可怕,但自然对齐很好。甚至在16字节对齐正式成为ABI的一部分之前,GCC就已经启用了
-mpreferred-stack-boundary=4
(2^4 = 16字节)。当前假定传入堆栈对齐为16字节(即使对于如果不是的话将出错的情况),同时保持这种一致性我不确定历史上的gcc版本是否曾经试图保持堆栈对齐,而不依赖于它来确保SSE代码生成或alignas(16)
对象的正确性。FFMPEG是一个依赖于编译器给予其堆栈对齐的公知示例:what is "stack alignment"?,例如在32位Windows上。
现代的gcc仍然在
main
的顶部发出代码,以16对齐堆栈(即使在Linux上,ABI保证内核以对齐的堆栈启动进程),但不在任何其他函数的顶部发出代码。您可以使用-mincoming-stack-boundary
告诉gcc在生成代码时应该假设堆栈如何对齐。古老的gcc4.1似乎并没有真正尊重
__attribute__((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可能是第一个拥有
-mpreferred-stack-boundary=4
的32位和64位版本。Godbolt上的gcc4.1.2和gcc4.4.7似乎也是这样,所以可能是后移植的变化,或者Matt Godbolt用更现代的配置配置了旧的gcc。