我读过的,因为它是为"业绩原因"做不同的地方,但我仍然不知道什么是在性能得到这个16字节对齐提高了特殊情况.或者,无论如何,选择这个的原因是什么.
编辑:我想我以误导的方式写了这个问题.我没有询问为什么处理器使用16字节对齐的内存更快地处理事情,这在文档中随处可见.我想要知道的是,强制执行16字节对齐比仅让程序员在需要时自己对齐堆栈更好.我问这个是因为根据我的汇编经验,堆栈实施有两个问题:只有少于1%的执行代码才有用(所以其他99%实际上是开销); 它也是一个非常常见的错误来源.所以我想知道它最终是如何得到回报的.虽然我对此仍有疑问,但我接受了彼得的回答,因为它包含了我原来问题的最详细答案.
1> Peter Cordes..:
请注意,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
,并且rsp
在未知偏移rsp
(0
或者-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字节对齐是合乎逻辑的.
这有点过于激进,并且大多数只有在自动向量化的函数可以内联时才有用,但通常还有其他本地编译器可以填充到任何间隙中,因此它不会浪费堆栈空间.只要有已知的堆栈对齐,就不会浪费指令.(显然,如果他们决定不要求16字节堆栈对齐,那么ABI设计人员就可以将其排除在外.)
溢出/重装 __m128
当然,它可以自由地执行alignas(16) char buf[1024];
源或请求 16字节对齐的其他情况.
还有__m128
/ __m128d
/ __m128i
本地人.编译器可能无法将所有向量局部保留在寄存器中(例如,溢出函数调用,或者没有足够的寄存器),因此它需要能够使用movaps
或作为ALU指令的内存源操作数来溢出/重新加载它们,出于效率原因,上面讨论了
实际上分布在缓存行边界(64字节)上的加载/存储具有显着的延迟惩罚,并且对现代CPU也有轻微的吞吐量惩罚.加载需要来自2个独立缓存行的数据,因此需要对缓存进行两次访问.(可能还有2次缓存未命中,但堆栈内存很少见).
我认为movups
已经为那些价格昂贵的旧CPU上的矢量带来了成本,但它仍然很糟糕.跨越4k页面边界要差得多(在Skylake之前的CPU上),如果它接触4k边界两侧的字节,则加载或存储需要约100个周期.(还需要2次TLB检查). 自然对齐使得跨越任何更宽边界的分裂变得不可能,因此16字节对齐对于您可以使用SSE2做的一切就足够了.
max_align_t
在x86-64 System V ABI中有16字节对齐,因为long double
(10字节/ 80位x87).由于某些奇怪的原因,它被定义为填充到16个字节,这与32位代码不同sizeof(long double) == 10
.的x87 10字节加载/存储是相当缓慢反正(如1/3的负载吞吐量double
或float
对酷睿2,在P4 1/6,或K8 1/8),但也许缓存线和页面拆分的处罚是如此糟糕在较旧的CPU上,他们决定以这种方式定义它.我认为在现代CPU(甚至可能是Core2)上循环遍历数组long double
并不会因打包10字节而变慢,因为这fld m80
将比每个~64个元素的缓存行分割更大的瓶颈.
实际上,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
在堆栈上传递值(在xmm0-7之后具有前8个浮点数/向量args).对内存中的向量要求16字节对齐是有意义的,因此被调用者可以有效地使用它们,并且由调用者有效地存储它们.始终保持16字节堆栈对齐使得需要将一些arg传递空间对齐16的函数变得容易.
有类似的类型__m128
,ABI保证有16字节对齐.如果你定义一个local并获取它的地址,并将该指针传递给其他函数,那么本地需要充分对齐.因此,保持16字节堆栈对齐与提供某些类型的16字节对齐密切相关,这显然是一个好主意.
这些天,它atomic
可以廉价地获得16字节对齐,因此lock cmpxchg16b
不会跨越缓存行边界.对于极少数情况,你有一个带有自动存储的原子本地,你将指针传递给多个线程......
脚注1:32位Linux
并非所有32位平台都破坏了与现有二进制文件的兼容性,并且像Linux一样手写.有些像i386 NetBSD仍然只使用原始版本的i386 SysV ABI的历史4字节堆栈对齐要求.
历史的4字节堆栈对齐也不足以double
在现代CPU上实现高效的8字节.未对齐fld
/ fstp
通常是高效的,除非它们越过缓存线边界(像其他加载/存储一样),所以它并不可怕,但自然对齐很好.
即使在16字节对齐正式成为ABI的一部分之前,GCC也曾-mpreferred-stack-boundary=4
在32位上启用(2 ^ 4 = 16字节).这当前假设传入的堆栈对齐是16个字节(即使对于不会出现故障的情况),以及保留该对齐.我不确定历史gcc版本是否曾经尝试保留堆栈对齐而不依赖于SSE代码或alignas(16)
对象的正确性.
ffmpeg是一个众所周知的例子,依赖于编译器给它堆栈对齐:什么是"堆栈对齐"?,例如在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),但后来它写入了维护在https的i386 System V ABI版本://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.