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



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

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 在访问未对齐的地址时不再会产生延迟损失?如果没有,应在什么情况下这样做?我应该对齐每个堆栈变量吗?类成员变量?

有没有人有任何例子,他们发现对齐内存可以显着提高性能?

惩罚通常很小,但在 Skylake 之前,在英特尔 CPU 上跨越 4k 页面边界会产生很大的惩罚(~150 个周期)。 如何准确地对未对齐的访问速度进行基准测试x86_64有一些关于跨越缓存线边界或 4k 边界的实际影响的详细信息。 (即使负载/存储位于一个 2M 或 1G 的巨大页面内,这也适用,因为硬件在启动两次检查 TLB 的过程之前无法知道这一点。 例如,在仅 4 字节对齐的double数组中,在页面边界处会有一个双精度均匀地分布在两个 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 字节对齐。 如果您关心 32 位代码,如果还没有需要那么多对齐的成员,则可以在此类结构成员上使用alignas(8)

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


(如果您通过违反 ABI 的对齐要求而导致未定义的行为,则通常不会使任何性能有所不同。 它通常不会导致 x86 的正确性问题,但它可能会导致 SIMD 类型以及标量类型的自动矢量化出现故障。 例如,为什么对 mmap 内存的未对齐访问有时会在 AMD64 上出现段错误? 因此,如果您故意未对齐数据,请确保不要使用任何宽度超过char*的指针访问它。 例如,使用带有uint64_t tmpmemcpy(&tmp, buf, 8)来执行未对齐的负载。 GCC可以通过它自动矢量化,IIRC。


如果在启用 AVX 或 AVX512 的情况下进行编译,则有时可能需要对大型数组使用alignas(32)或 64。 对于大型阵列(不适合 L2 或 L1d 缓存)上的 SIMD 环路,使用 AVX/AVX2(32 字节矢量),在英特尔 Haswell/Skylake 上确保其对齐 32 通常效果接近于零。 来自 L3 或 DRAM 的数据中的内存瓶颈将使内核的加载/存储单元和 L1d 缓存有时间在后台进行多次访问,即使每隔一个加载/存储都跨越缓存行边界。

但是,对于Skylake服务器上的AVX512,即使对于来自L3缓存或DRAM的阵列,64字节的阵列对齐在实践中也存在显着影响。 我忘记了细节,我已经有一段时间没有看过一个例子了,但即使是内存绑定循环,也可能有 10% 到 15%?如果每个 64 字节矢量加载和存储未对齐,它们将跨越 64 字节缓存行边界。

根据循环,您可以通过执行第一个可能未对齐的向量来处理对齐不足的输入,然后在对齐的向量上循环,直到最后一个对齐的向量。 另一个可能重叠的向量可以处理最后几个字节。 这适用于复制和处理循环,在复制和处理循环中,可以重新复制和重新处理重叠中的相同元素,但还有其他技术可用于其他情况,例如标量循环直至对齐边界、更窄的矢量或掩码。 如果编译器是自动矢量化,则由编译器进行选择。 如果您使用内部函数手动矢量化,则可以/必须选择。 如果数组正常对齐,最好只使用未对齐的负载(如果指针在运行时对齐,则不会受到任何惩罚),并让硬件处理未对齐输入的罕见情况,这样您就不会在对齐的输入上产生任何软件开销。

当然有惩罚, 您必须在单词边框上对齐结构成员以最大化

最新更新