【问题标题】:Are either Clang or GCC able to autovectorize manually unrolled loops?Clang 或 GCC 是否能够自动矢量化手动展开的循环?
【发布时间】:2016-12-06 12:07:35
【问题描述】:

我有一个代码风格的想法,用于编写特定类型的数值算法,您可以完全以与数据布局无关的方式编写算法。

即您的所有函数都采用(一个或多个)标量参数,并(通过指针)返回一个或多个标量返回值。因此,例如,如果您有一个采用 3d 浮点向量的函数,而不是采用具有三个成员的结构或 float[3] xyz,您采用 float x、float y、float z。

这个想法是您可以更改输入和输出数据的布局,即您可以使用数组结构与结构数据布局的数组、缓存效率的平铺布局、SIMD 与多核粒度等。 . 无需为所有数据布局组合重写所有代码。

该策略有一些明显的缺点:

  • 您不能在函数中使用 for 循环来使代码更紧凑
  • 您的函数在其签名中需要更多参数

...但是,如果您的数组很短,那么这些是可口的,并且可以省去您多次重写代码以使其速度更快的麻烦。

但特别是,我担心编译器可能无法接受像 x+=a; 这样的东西。 y+=b; z+=c; w+=d 并将其自动向量化为单个 SIMD 向量添加,如果您想在调用堆栈的底部执行 SIMD,而不是在内联函数堆栈的顶部执行 SIMD。

clang 和/或 gcc 是否能够在 C 和/或 C++ 代码中“重新滚动”手动展开的循环(可能在内联函数之后)并生成向量化机器代码?

【问题讨论】:

  • 这在 C 中是不可能的。选择一种语言。整个问题太宽泛了。我们不是讨论网站。
  • 嗨,Olaf,您的回答“否”很可能是正确的,并且不需要 4 个相同问题的副本。
  • 所以你故意重复发布?
  • 我不得不同意,这个问题应该只选择一种语言。
  • SIMD 内在函数在 C 和 C++ 中是相同的,选择一个没有任何区别。

标签: c++ c simd loop-unrolling auto-vectorization


【解决方案1】:

我写了一些代码来对我的想法做一个简单的测试:

// Compile using gcc -O4 main.c && objdump -d a.out

void add4(float x0, float x1, float x2, float x3, 
          float y0, float y1, float y2, float y3, 
          float* out0, float* out1, float* out2, float* out3) {
  // Non-inlined version of this uses xmm registers and four separate
  // SIMD operations
    *out0 = x0 + y0;
    *out1 = x1 + y1;
    *out2 = x2 + y2;
    *out3 = x3 + y3;
}
void sub4(float x0, float x1, float x2, float x3,
          float y0, float y1, float y2, float y3,
          float* out0, float* out1, float* out2, float* out3) {
    *out0 = x0 - y0;
    *out1 = x1 - y1;
    *out2 = x2 - y2;
    *out3 = x3 - y3;
}
void add4_then_sub4(float x0, float x1, float x2, float x3,
          float y0, float y1, float y2, float y3,
          float z0, float z1, float z2, float z3,
          float* out0, float* out1, float* out2, float* out3) {
    // In non-inlined version of this, add4 and sub4 get inlined.
    // xmm regiesters get re-used for the add and subtract,
    // but there is still no 4-way SIMD
  float temp0,temp1,temp2,temp3;
  // temp= x + y
  add4(x0,x1,x2,x3,
       y0,y1,y2,y3,
       &temp0,&temp1,&temp2,&temp3);
  // out = temp - z
  sub4(temp0,temp1,temp2,temp3,
       z0,z1,z2,z3,
       out0,out1,out2,out3);
}
void add4_then_sub4_arrays(const float x[4],
                                const float y[4],
                                const float z[4],
                                float out[4])
{
    // This is a stand-in for the main function below, but since the arrays are aguments,
    // they can't be optimized out of the non-inlined version of this function.
    // THIS version DOES compile into (I think) a bunch of non-aligned moves,
    // and a single vectorized add a single vectorized subtract
    add4_then_sub4(x[0],x[1],x[2],x[3],
            y[0],y[1],y[2],y[3],
            z[0],z[1],z[2],z[3],
            &out[0],&out[1],&out[2],&out[3]
            );
}

int main(int argc, char **argv) 
{
}

考虑为 add4_then_sub4_arrays 生成的程序集:

0000000000400600 <add4_then_sub4_arrays>:
  400600:       0f 57 c0                xorps  %xmm0,%xmm0
  400603:       0f 57 c9                xorps  %xmm1,%xmm1
  400606:       0f 12 06                movlps (%rsi),%xmm0
  400609:       0f 12 0f                movlps (%rdi),%xmm1
  40060c:       0f 16 46 08             movhps 0x8(%rsi),%xmm0
  400610:       0f 16 4f 08             movhps 0x8(%rdi),%xmm1
  400614:       0f 58 c1                addps  %xmm1,%xmm0
  400617:       0f 57 c9                xorps  %xmm1,%xmm1
  40061a:       0f 12 0a                movlps (%rdx),%xmm1
  40061d:       0f 16 4a 08             movhps 0x8(%rdx),%xmm1
  400621:       0f 5c c1                subps  %xmm1,%xmm0
  400624:       0f 13 01                movlps %xmm0,(%rcx)
  400627:       0f 17 41 08             movhps %xmm0,0x8(%rcx)
  40062b:       c3                      retq   
  40062c:       0f 1f 40 00             nopl   0x0(%rax)

数组没有对齐,所以移动操作比理想的要多得多,我不确定 xor 在那里做了什么,但确实有一个 4 路加法和一个 4 路减法根据需要。

所以答案是 gcc 至少有一些能力将标量浮点操作打包回 SIMD 操作。

更新:gcc-4.8 -O3 -march=native main.c &amp;&amp; objdump -d a.out 的代码更紧凑:

0000000000400600 <add4_then_sub4_arrays>:
  400600:       c5 f8 10 0e             vmovups (%rsi),%xmm1
  400604:       c5 f8 10 07             vmovups (%rdi),%xmm0
  400608:       c5 f0 58 c0             vaddps %xmm0,%xmm1,%xmm0
  40060c:       c5 f8 10 0a             vmovups (%rdx),%xmm1
  400610:       c5 f8 5c c1             vsubps %xmm1,%xmm0,%xmm0
  400614:       c5 f8 11 01             vmovups %xmm0,(%rcx)
  400618:       c3                      retq   
  400619:       0f 1f 80 00 00 00 00    nopl   0x0(%rax)

clang-4.0 -O3 -march=native main.c &amp;&amp; llvm-objdump -d a.out:

add4_then_sub4_arrays:
  4005e0:       c5 f8 10 07                                     vmovups (%rdi), %xmm0
  4005e4:       c5 f8 58 06                                     vaddps  (%rsi), %xmm0, %xmm0
  4005e8:       c5 f8 5c 02                                     vsubps  (%rdx), %xmm0, %xmm0
  4005ec:       c5 f8 11 01                                     vmovups %xmm0, (%rcx)
  4005f0:       c3                                              ret
  4005f1:       66 66 66 66 66 66 2e 0f 1f 84 00 00 00 00 00    nopw    %cs:(%rax,%rax)

【讨论】:

  • xor 用于打破movlps 对旧值的依赖
  • xorps + movlps 是movsd (%rdx), %xmm1 的脑残替代品。然后是连续字节的movhps?我勒个去?你用的是什么编译器,有什么设置?显然movups (%rdx), %xmm1 会更有效,尤其是在任何中途最近的 CPU 上。在一些相当老旧的 CPU 上,将不对齐的负载分成两半是一种合理的策略。
  • 赞成测试并表明您的想法的这种实现对于您测试的编译器+选项是不可行的。内存源数据的指令数增加 3 到 4 倍是荒谬的。 (并且所有这些 movlps + movhps 对将成为 shuffle 端口的瓶颈,因为它们是加载+混合指令。请参阅 agner.org/optimize 以获取指令表,以及 x86 tag wiki)。
  • 谢谢彼得!我将继续选择一个编译器并为架构找出正确的标志
【解决方案2】:

您的担心是正确的。没有编译器会自动向量化这 4 个添加。考虑到输入不连续和对齐,这根本不值得。将参数收集到 SIMD 寄存器的成本远高于保存向量加法的成本。

当然,编译器不能使用对齐流加载的原因是因为您将参数作为标量传递。

【讨论】:

  • 嗨!这个想法是你将这些标量参数线性地布置在函数之外。如果函数被内联,编译器具有数据布局,以及对值发生的相同定义,但不使用 for 循环表示。我正在研究一个更具体的例子。
  • 现在 SIMD 寄存器~确实经常被使用,即使它只是一个浮点运算。
  • 编译器可以并且确实对单个向量宽度操作进行向量化,如果传递了连续的指针并且函数被内联以便编译器知道这一点。尤其是clang很擅长这个。 (如果你愿意,我可以举个例子)。
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2018-12-16
  • 2019-04-12
  • 2011-12-29
  • 2019-04-05
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多