结合__restrict__和__attribute __((aligned(32)))

时间:2019-01-14 21:55:32

标签: c++ gcc x86 memory-alignment restrict-qualifier

我想确保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”编译的程序集 它充满了vmovup,这是未对齐的动作。

.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

即使对于沙桥,行为也相同:

.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)。 仍然是不结盟的举动。

1 个答案:

答案 0 :(得分:2)

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

有一种方法可以做到这一点,但这仅对gcc有用,对clang或ICC无效。

请参见How to tell GCC that a pointer argument is always double-word-aligned?,以了解适用于所有GNU C兼容编译器的__builtin_assume_aligned,以及How can I apply __attribute__(( aligned(32))) to an int *?,以获取有关__attribute__((aligned(32)))(适用于GCC)的更多详细信息。

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

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
    }
}

gcc, clang, and ICC output on the Godbolt compiler explorer)。


只要有编译时对齐保证,GCC和clang将使用movaps / vmovaps而不是ups。 (与从未使用movaps进行加载/存储的MSVC和ICC不同,对于在Core2 / K10或更旧版本上运行的任何内容,它都没有进行优化)。正如您所注意到的,它正在将-mavx256-split-unaligned-load / store效果应用于除Haswell(Why doesn't gcc resolve _mm256_loadu_pd as single vmovupd?)以外的其他调音,这是您的语法无效的另一个线索。

vmovups在对齐内存上使用时不会出现性能问题;在运行时对齐地址时,它在所有支持AVX的CPU上的性能与vmovaps相同。因此,实际上,您的-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之类的编译器上检查此优化情况,即使它们 do 知道有对齐保证也使用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之前,可以将存储在同一缓存行中的背对背存储合并到存储缓冲区中,因此这取决于您要测试的内容。

半相关:在Intel Sandybridge系列CPU上,像c[i] = a[i]+b[i]或STREAM triad这样的2倍负载+ 1倍存储内存访问模式将最接近maxing out total L1d cache load+store bandwidth。在SnB / IvB上,每个加载/存储256位向量需要2个周期,因此在加载的第二个周期内,存储地址oups需要时间在端口2或3上使用AGU。在Haswell及更高版本(256位宽的加载/存储端口)上,存储需要使用非索引寻址模式,以便它们可以在端口7上使用简单寻址模式存储AGU。

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

顺便说一句,英特尔最近宣布了Sunny Cove(Ice Lake的前身),每个时钟将有2x load + 2x store throughput,第二个向量洗牌ALU和5个问题/更名。太有趣了!编译器将需要至少展开2次循环,才能避免在每1个时钟循环分支上出现瓶颈。


脚注1 :这就是为什么(如果不使用AVX进行编译)会收到警告,并且gcc会忽略and rsp,-32,因为它假定RSP已经对齐。 (它实际上并没有溢出任何YMM规则,因此无论如何它都应该对此进行了优化,但是gcc在使用局部对齐或自动矢量化创建的对象时已经有此错漏优化的错误了一段时间。)

<source>:4:6: note: The ABI for passing parameters with 32-byte alignment has changed in GCC 4.6