1

我想确保 gcc 知道:

  1. 指针指向不重叠的内存块
  2. 指针有 32 字节对齐

以下是正确的吗?

template<typename T, typename T2>
void f(const  T* __restrict__ __attribute__((aligned(32))) x,
       T2* __restrict__ __attribute__((aligned(32))) out) {}

谢谢。

更新:

我尝试使用一次读取和大量写入来使 CPU 端口饱和以进行写入。我希望这将使对齐动作的性能提升更加显着。

但是该程序集仍然使用未对齐的移动而不是对齐的移动。

代码(也在godbolt.org 上

int square(const  float* __restrict__ __attribute__((aligned(32))) x,
           const int size,
           float* __restrict__ __attribute__((aligned(32))) out0,
           float* __restrict__ __attribute__((aligned(32))) out1,
           float* __restrict__ __attribute__((aligned(32))) out2,
           float* __restrict__ __attribute__((aligned(32))) out3,
           float* __restrict__ __attribute__((aligned(32))) out4) {
    for (int i = 0; i < size; ++i) {
        out0[i] = x[i];
        out1[i] = x[i] * x[i];
        out2[i] = x[i] * x[i] * x[i];
        out3[i] = x[i] * x[i] * x[i] * x[i];
        out4[i] = x[i] * x[i] * x[i] * x[i] * x[i];
    }
}

使用 gcc 8.2 和“-march=haswell -O3”编译的程序集充满了 vmovups,它们是未对齐的移动。

.L3:
        vmovups ymm1, YMMWORD PTR [rbx+rax]
        vmulps  ymm0, ymm1, ymm1
        vmovups YMMWORD PTR [r14+rax], ymm0
        vmulps  ymm0, ymm1, ymm0
        vmovups YMMWORD PTR [r15+rax], ymm0
        vmulps  ymm0, ymm1, ymm0
        vmovups YMMWORD PTR [r12+rax], ymm0
        vmulps  ymm0, ymm1, ymm0
        vmovups YMMWORD PTR [rbp+0+rax], ymm0
        add     rax, 32
        cmp     rax, rdx
        jne     .L3
        and     r13d, -8
        vzeroupper

即使对于 sandybridge 也有相同的行为:

.L3:
        vmovups xmm2, XMMWORD PTR [rbx+rax]
        vinsertf128     ymm1, ymm2, XMMWORD PTR [rbx+16+rax], 0x1
        vmulps  ymm0, ymm1, ymm1
        vmovups XMMWORD PTR [r14+rax], xmm0
        vextractf128    XMMWORD PTR [r14+16+rax], ymm0, 0x1
        vmulps  ymm0, ymm1, ymm0
        vmovups XMMWORD PTR [r13+0+rax], xmm0
        vextractf128    XMMWORD PTR [r13+16+rax], ymm0, 0x1
        vmulps  ymm0, ymm1, ymm0
        vmovups XMMWORD PTR [r12+rax], xmm0
        vextractf128    XMMWORD PTR [r12+16+rax], ymm0, 0x1
        vmulps  ymm0, ymm1, ymm0
        vmovups XMMWORD PTR [rbp+0+rax], xmm0
        vextractf128    XMMWORD PTR [rbp+16+rax], ymm0, 0x1
        add     rax, 32
        cmp     rax, rdx
        jne     .L3
        and     r15d, -8
        vzeroupper

使用加法而不是乘法(Godbolt)。仍然不结盟的动作。

4

1 回答 1

3

不, usingfloat *__attribute__((aligned(32))) x意味着指针本身存储在对齐的内存中,而不是指向对齐的内存。1

有一种方法可以做到这一点,但它只对 gcc 有帮助,对 clang 或 ICC 没有帮助。

请参阅如何告诉 GCC 指针参数始终是双字对齐的?适用__builtin_assume_aligned于所有 GNU C 兼容的编译器,以及如何将 __attribute__((aligned(32))) 应用于 int *?有关 的更多详细信息__attribute__((aligned(32))),它适用于 GCC。

我使用__restrict而不是__restrict__因为 C99 的 C++ 扩展名restrict可移植到所有主流 x86 C++ 编译器,包括 MSVC。

typedef float aligned32_float __attribute__((aligned(32)));

void prod(const aligned32_float  * __restrict x,
          const aligned32_float  * __restrict y,
          int size,
          aligned32_float* __restrict out0)
{
    size &= -16ULL;

#if 0   // this works for clang, ICC, and GCC
    x = (const float*)__builtin_assume_aligned(x, 32);  // have to cast the result in C++
    y = (const float*)__builtin_assume_aligned(y, 32);
    out0 = (float*)__builtin_assume_aligned(out0, 32);
#endif

    for (int i = 0; i < size; ++i) {
        out0[i] = x[i] * y[i];  // auto-vectorized with a memory operand for mulps
      // note clang using two separate movups loads
      // instead of a memory operand for mulps
    }
}

Godbolt 编译器资源管理器上的 gcc、clang 和 ICC 输出)。


GCC 和 clang 将使用movaps/vmovaps而不是ups任何时候它具有编译时对齐保证。(与从不movaps用于加载/存储的 MSVC 和 ICC 不同,在 Core2 / K10 或更早版本上运行的任何东西都错过了优化)。正如您所注意到的,它正在将-mavx256-split-unaligned-load/store效果应用于 Haswell 以外的调整(为什么 gcc 不将 _mm256_loadu_pd 解析为单个 vmovupd?)。这是您的语法不起作用的另一个线索。

vmovups在对齐内存上使用时不是性能问题;vmovaps当地址在运行时对齐时,它在所有支持 AVX 的 CPU 上执行相同。-march=haswell所以在实践中,你的输出没有真正的问题。只有在 Nehalem 和 Bulldozer 之前的较旧 CPU 总是解码movups为多个微指令。

告诉编译器对齐保证的真正好处(这些天)是编译器有时会为启动/清理循环发出额外的代码以达到对齐边界。或者没有 AVX,编译器无法将负载折叠到内存操作数中,mulps除非它是对齐的。

一个很好的测试用例是out0[i] = x[i] * y[i],其中负载结果只需要一次。out0[i] *= x[i]。知道对齐启用movaps/ mulps xmm0, [rsi],否则它是 2x movups+ mulps。您甚至可以在 ICC 或 MSVC 之类的编译器上检查这种优化,movups即使它们知道它们有对齐保证,它们也会使用,但是当它们可以将负载折叠到 ALU 操作中时,它们仍然会生成需要对齐的代码。

这似乎__builtin_assume_aligned是唯一真正可移植(对于 GNU C 编译器)的方法。您可以做一些技巧,例如将指针传递给struct aligned_floats { alignas(32) float f[8]; };,但这使用起来很麻烦,除非您实际上通过该类型的对象访问内存,否则编译器不会假设对齐。(例如,将指向该指针的指针投射回float *


我尝试使用一次读取和大量写入来使 CPU 端口饱和以进行写入。

使用超过 4 个输出流可能会导致缓存中出现更多的冲突未命中,从而造成伤害。例如,Skylake 的 L2 缓存只有 4 路。但是 L1d 是 8 路的,所以对于小缓冲区可能没问题。

如果您想使存储端口 uop 吞吐量饱和,请使用更窄的存储(例如标量),而不是每个 uop 需要更多带宽的宽 SIMD 存储。到同一缓存行的背靠背存储可能能够在提交到 L1d 之前合并到存储缓冲区中,因此这取决于您要测试的内容。

半相关:2x 负载 + 1x 存储内存访问模式(如c[i] = a[i]+b[i]STREAM 三元组)将最接近英特尔 Sandybridge 系列 CPU 上的总 L1d 缓存负载+存储带宽。在 SnB/IvB 上,256 位向量每次加载/存储需要 2 个周期,从而为存储地址 uop 在第二个加载周期中使用端口 2 或 3 上的 AGU 留出时间。在 Haswell 及更高版本(256 位宽加载/存储端口)上,存储需要使用非索引寻址模式,以便它们可以在端口 7 上使用简单寻址模式存储 AGU。

但是 AMD CPU 每个时钟最多可以执行 2 个内存操作,最多一个是存储,因此它们会通过复制和操作存储 = 加载模式来最大化。

顺便说一句,英特尔最近宣布了 Sunny Cove(Ice Lake 的继任者),每个时钟将具有2 倍的负载 + 2 倍的存储吞吐量、第二个向量 shuffle ALU 和 5 范围的问题/重命名。所以这很有趣!编译器需要将循环展开至少 2 次,以免在每时钟 1 个循环分支上成为瓶颈。


脚注 1:这就是为什么(如果你在没有 AVX 的情况下编译),你会得到一个警告,并且 gcc 省略了,and rsp,-32因为它假设 RSP 已经对齐。(它实际上并没有溢出任何 YMM regs,所以无论如何它应该已经优化了这个,但是 gcc 已经有一段时间了这个错过优化的错误,使用本地或自动矢量化创建的具有额外对齐的对象。)

<source>:4:6: note: The ABI for passing parameters with 32-byte alignment has changed in GCC 4.6
于 2019-01-15T06:14:12.510 回答