【问题标题】:Does aligning memory on particular address boundaries in C/C++ still improve x86 performance?在 C/C++ 中的特定地址边界上对齐内存是否仍能提高 x86 性能?
【发布时间】:2019-05-31 15:32:05
【问题描述】:

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

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

有没有人发现内存对齐可以显着提高性能的例子?

【问题讨论】:

  • 你问缓存行还存在吗?关于 SIMD?或者它是“曾经有任何性能影响吗?(a:是)以及所有性能影响是什么?(a:太宽泛)
  • 一些较早的结果here,不管怎样,问题不是错位,而是跨越了某些界限(例如,64 字节、4K、AMD 上的 16 字节)
  • 类似的question
  • Should I align every stack variable? 不。大多数变量对性能不敏感。
  • C++ 实现已经对齐了它们的变量。甚至动态分配也是特定于类型的,并且结构得到填充以使成员对齐。实现决定在支持未对齐内存访问的平台上,但我认为,除非你告诉编译器优化空间而不是速度,否则你应该很好。

标签: c++ c performance x86 latency


【解决方案1】:

惩罚通常很小,但在英特尔 CPU 上超过 4k 页面边界后 Skylake 会受到很大惩罚(约 150 个周期)。 How can I accurately benchmark unaligned access speed on x86_64 有一些关于跨越缓存线边界或 4k 边界的实际影响的详细信息。 (这适用于即使加载/存储在一个 2M 或 1G 的大页面内,因为硬件在它开始检查 TLB 两次的过程之后才能知道这一点。)例如在 double 的数组中只有 4 -byte 对齐,在页面边界处会有一个 double 均匀分布在两个 4k 页面上。每个缓存行边界都相同。

不跨越 4k 页面的常规高速缓存行拆分在 Intel 上会花费大约 6 个额外的延迟周期(Skylake 上总共 11c,而普通 L1d 命中需要 4 或 5c),并花费额外的吞吐量(这在通常每个时钟维持接近 2 个负载的代码中可能很重要。)

未跨越 64 字节高速缓存行边界的未对齐对 Intel 的惩罚为零。在 AMD 上,缓存线仍然是 64 字节,但在 32 字节的缓存线内有相关的边界,在某些 CPU 上可能是 16 字节。

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

不,编译器已经为您完成了这项工作。 x86-64 调用约定维护 16 字节堆栈对齐,因此它们可以免费获得任何对齐,包括 8 字节 int64_tdouble 数组。

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

普通的ABIs 还要求所有原始类型自然对齐(与其大小对齐),因此即使在结构等内部,您也会得到对齐,并且单个原始类型永远不会跨越缓存线边界。 (例外:i386 System V 只需要 int64_tdouble 的 4 字节对齐。在结构之外,编译器会选择给它们更多对齐,但在结构内部它不能更改布局规则。所以声明你的结构以将 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 类型的错误,和使用标量类型的自动矢量化。例如Why does unaligned access to mmap'ed memory sometimes segfault on AMD64?。因此,如果您故意不对齐数据,请确保不要使用任何比char* 更宽的指针访问它。 例如使用 memcpy(&tmp, buf, 8)uint64_t tmp 进行未对齐的加载。 gcc 可以通过它自动矢量化,IIRC。)


如果您在启用 AVX 或 AVX512 的情况下编译,您有时可能需要 alignas(32) 或 64 来处理大型数组。对于大型阵列上的 SIMD 循环(不适合 L2 或 L1d 缓存),使用 AVX/AVX2(32 字节向量),确保它在 Intel Haswell/Skylake 上对齐 32 通常几乎为零。来自 L3 或 DRAM 的数据中的内存瓶颈将使核心的加载/存储单元和 L1d 缓存有时间在后台进行多次访问,即使其他加载/存储跨越缓存线边界也是如此。

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

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

【讨论】:

  • ABI 代表什么?
  • @IsaakEriksson:应用程序二进制接口。调用约定是 ABI 的一部分,但 ABI 还包括每个类型的大小和最小对齐的规则(例如,long 在 x86-64 System V 中是 64 位,但 long 在 Windows 中是 32 位x64) 和结构打包规则。还有任何其他要求,例如用于异常堆栈展开的元数据,或用于相同目的的帧指针/堆栈帧布局规则,以及 GOT / PLT 如何用于动态链接等。请参阅Where is the x86-64 System V ABI documented?
  • 重要的是不仅要考虑未对齐的加载或存储的“延迟”,还要考虑它可能比对齐的加载或存储使用更多资源的事实,例如内部队列或移位器中的点移动未对齐的数据。即使硬件可能能够以与对齐加载或存储相同的延迟完成此类加载或存储,但它可能无法维持与对齐加载或存储相同的吞吐量,因为使用了更多资源。
  • @EricPostpischil:在现代 x86 上,这只是缓存行拆分的情况。 (或在 AMD 上,用于跨越 32 或 16 字节边界)。在高速缓存行中,加载端口可以处理未对齐加载的所有内容。但是,是的,缓存行拆分负载也有吞吐量成本。它们每个都占用两个 L1d 读取端口,将最大吞吐量减半。 Intel 上还有一个针对ld_blocks.no_sr 的性能计数器:[由于处理拆分访问的所有资源都在使用中,拆分加载操作被暂时阻止的次数];拆分加载缓冲区有限。
  • 在 ARM 或 MIPS 等其他 ISA 上可能是这样(其现代版本确实支持未对齐的负载),但这是一个[x86] 的问题。
【解决方案2】:

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

【讨论】:

    猜你喜欢
    • 2013-04-27
    • 2013-12-23
    • 1970-01-01
    • 2018-05-22
    • 2012-01-12
    • 2013-09-27
    • 1970-01-01
    • 2013-10-11
    • 1970-01-01
    相关资源
    最近更新 更多