在C/C++中对齐特定地址边界的内存是否仍能提高x86的性能?

mez*_*hic 6 c c++ performance x86 latency

许多低延迟开发指南讨论了在特定地址边界上对齐内存分配:

https://github.com/real-logic/simple-binary-encoding/wiki/Design-Principles#word-aligned-access

http://www.alexonlinux.com/aligned-vs-unaligned-memory-access

但是,第二个链接是从2008年开始的.在2019年,在地址边界上调整内存是否仍能提高英特尔CPU的性能?我认为英特尔CPU不再会因访问未对齐地址而导致延迟损失?如果没有,在什么情况下应该这样做?我应该对齐每个堆栈变量吗?类成员变量?

有没有人有任何例子表明他们在调整内存方面取得了显着的性能提升?

Pet*_*des 9

惩罚通常很小,但在Skylake遭受大额惩罚(约150个周期)之前,在Intel CPU上跨越4k页边界. 如何在x86_64上准确地对准未对齐的访问速度,以及有关跨越缓存行边界或4k边界的实际效果的一些细节.(这适用即使负载/存储是一个2M或1G hugepage内,因为硬件无法知道,直到在启动后检查TLB两倍的过程.)例如,在数组double也仅仅是4字节对齐在页面边界处,有一个双重均匀分布在两个4k页面上.每个缓存行边界都相同.

不超过4k页面的常规高速缓存行分割在英特尔上花费大约6个额外的延迟周期(Skylake总共11c,而普通L1d命中则为4或5c),并且额外的吞吐量成本(这可能是重要的)通常每个时钟承受近2个负载的代码.)

不跨越64字节高速缓存行边界的未对齐对英特尔没有任何惩罚.在AMD上,缓存行仍然是64字节,但缓存行中有32个字节的相关边界,某些CPU上可能有16个.

我应该对齐每个堆栈变量吗?

不,编译器已经为您做到了.x86-64调用约定保持16字节的堆栈对齐,因此它们可以获得任意对齐,包括8字节int64_tdouble数组.

还要记住,大多数局部变量在大量使用时都会保存在寄存器中.除非是变量volatile,否则您在没有优化的情况下进行编译,则不必在访问之间存储/重新加载该值.

普通的ABI也需要对所有基本类型进行自然对齐(与其大小对齐),因此即使在结构体内等等,您也会得到对齐,并且单个基本类型永远不会跨越缓存行边界.(例外:i386 System V只需要4字节对齐int64_tdouble.结构之外,编译器会选择给它们更多的对齐,但是在结构内部它不能改变布局规则.所以声明你的结构按顺序放置首先是8字节成员,或者至少是为了使它们得到8字节对齐.alignas(8)如果你关心32位代码,如果还没有任何需要那么多对齐的成员,也许可以使用这样的struct成员.)

x86-64 System V ABI(所有非Windows平台)如果在结构外部具有自动或静态存储,则需要将数组对齐16. maxalign_t在x86-64 SysV上为16,因此malloc/ new返回16字节对齐的内存以进行动态分配.gcc定位Windows如果它在该函数中对它们进行自动矢量化,也会对齐堆栈数组.


(如果通过违反ABI的对齐要求导致未定义的行为,它通常不会使任何性能不同.它通常不会导致x86的正确性问题,但它可能导致SIMD类型的错误,并且标量的自动矢量化类型.例如,为什么要映射的内存对齐访问有时段错误的AMD64? .因此,如果你有意错位数据,请确保您不宽于任何指针访问char*.例如,使用memcpy(&tmp, buf, 8)uint64_t tmp做不对齐的负荷.GCC不能通过IIRC进行自动审核.)


alignas(32)如果在启用AVX或AVX512的情况下编译,有时可能需要64或大型数组.对于大阵列上的SIMD循环(不适合L2或L1d缓存),使用AVX/AVX2(32字节向量),确保在Intel Haswell/Skylake上确定它与32对齐时通常接近于零.来自L3或DRAM的数据中的内存瓶颈将使核心的加载/存储单元和L1d缓存时间在引擎盖下进行多次访问,即使每个其他加载/存储都跨越缓存行边界.

但是对于Skylake服务器上的AVX512,在阵列的64字节对齐方面有很大的影响,即使是来自L3缓存或DRAM的阵列也是如此.我忘记了细节,自从我看了一个例子以来已经有一段时间了,但即使对于一个内存限制的循环也可能有10到15%? 如果未对齐,则每个 64字节向量加载和存储将跨越64字节高速缓存行边界.

根据循环,您可以通过执行第一个可能未对齐的向量来处理欠对齐输入,然后循环对齐的向量,直到最后一个对齐的向量.另一个可能重叠的向量到达数组的末尾可以处理最后几个字节.这对于一个复制和过程的循环,它的确定重新复制和重新处理在重叠的相同元素的伟大工程,但也有其他的技术可以用于其他情况,例如标量循环,达到对齐边界,较窄的向量或掩蔽.如果您的编译器是自动向量化的,则由编译器来选择.如果您使用内在函数手动进行矢量化,则需要/必须选择.如果数组通常是对齐的,那么最好只使用未对齐的加载(如果指针在运行时对齐则不会受到惩罚),并让硬件处理罕见的未对齐输入情况,这样就不会有任何软件开销对齐输入.

  • @IsaakEriksson:应用程序二进制接口.调用约定是ABI的一部分,但ABI还包括每个类型具有的大小和最小对齐的规则(例如,在x86-64 System V中,`long`是64位,但在Windows中`long`是32位x64)和struct-packing规则.还有任何其他要求,例如用于异常的堆栈展开的元数据,或用于相同目的的帧指针/堆栈框架布局规则,以及GOT/PLT如何用于动态链接等.请参阅[x86-64在哪里系统V ABI记录了吗?](/sf/ask/1269366871/) (2认同)