如何在 gcc 中启用 sse3 自动向量化

How to enable sse3 autovectorization in gcc

我有一个简单的循环,它取 n 个复数的乘积。当我执行这个循环数百万次时,我希望它尽可能快。我知道可以使用 SSE3 和 gcc 内在函数快速完成此操作,但我对是否有可能让 gcc 自动矢量化代码感兴趣。这是一些示例代码

#include <complex.h>
complex float f(complex float x[], int n ) {
  complex float p = 1.0;
  for (int i = 0; i < n; i++)
    p *= x[i];
  return p;
}

您从 gcc -S -O3 -ffast-math 得到的程序集是:

        .file   "test.c"
        .section        .text.unlikely,"ax",@progbits
.LCOLDB2:
        .text
.LHOTB2:
        .p2align 4,,15
        .globl  f
        .type   f, @function
f:
.LFB0:
        .cfi_startproc
        testl   %esi, %esi
        jle     .L4
        leal    -1(%rsi), %eax
        pxor    %xmm2, %xmm2
        movss   .LC1(%rip), %xmm3
        leaq    8(%rdi,%rax,8), %rax
        .p2align 4,,10
        .p2align 3
.L3:
        movaps  %xmm3, %xmm5
        movaps  %xmm3, %xmm4
        movss   (%rdi), %xmm0
        addq    , %rdi
        movss   -4(%rdi), %xmm1
        mulss   %xmm0, %xmm5
        mulss   %xmm1, %xmm4
        cmpq    %rdi, %rax
        mulss   %xmm2, %xmm0
        mulss   %xmm2, %xmm1
        movaps  %xmm5, %xmm3
        movaps  %xmm4, %xmm2
        subss   %xmm1, %xmm3
        addss   %xmm0, %xmm2
        jne     .L3
        movaps  %xmm2, %xmm1
.L2:
        movss   %xmm3, -8(%rsp)
        movss   %xmm1, -4(%rsp)
        movq    -8(%rsp), %xmm0
        ret
.L4:
        movss   .LC1(%rip), %xmm3
        pxor    %xmm1, %xmm1
        jmp     .L2
        .cfi_endproc
.LFE0:
        .size   f, .-f
        .section        .text.unlikely
.LCOLDE2:
        .text
.LHOTE2:
        .section        .rodata.cst4,"aM",@progbits,4
        .align 4
.LC1:
        .long   1065353216
        .ident  "GCC: (Ubuntu 5.4.0-6ubuntu1~16.04.4) 5.4.0 20160609"
        .section        .note.GNU-stack,"",@progbits

我不是装配专家,但我已经成功了。我会发表评论,但它太大了:

cat test.s
    .file   "test.c"
    .text
    .p2align 4,,15
    .globl  f
    .type   f, @function
f:
.LFB0:
    .cfi_startproc
    testl   %esi, %esi
    jle     .L4
    leal    -1(%rsi), %eax
    pxor    %xmm0, %xmm0
    movss   .LC1(%rip), %xmm1
    leaq    8(%rdi,%rax,8), %rax
    .p2align 4,,10
    .p2align 3
.L3:
    movaps  %xmm1, %xmm4
    movss   (%rdi), %xmm3
    movss   4(%rdi), %xmm2
    mulss   %xmm3, %xmm1
    mulss   %xmm2, %xmm4
    addq    , %rdi
    mulss   %xmm0, %xmm2
    cmpq    %rdi, %rax
    mulss   %xmm3, %xmm0
    subss   %xmm2, %xmm1
    addss   %xmm4, %xmm0
    jne     .L3
.L1:
    movss   %xmm1, -8(%rsp)
    movss   %xmm0, -4(%rsp)
    movq    -8(%rsp), %xmm0
    ret
.L4:
    movss   .LC1(%rip), %xmm1
    pxor    %xmm0, %xmm0
    jmp     .L1
    .cfi_endproc
.LFE0:
    .size   f, .-f
    .section        .rodata.cst4,"aM",@progbits,4
    .align 4
.LC1:
    .long   1065353216
    .ident  "GCC: (Ubuntu 6.2.0-5ubuntu12) 6.2.0 20161005"
    .section        .note.GNU-stack,"",@progbits

我的编译命令是 gcc -S -O3 -ffast-math -ftree-vectorizer-verbose=3 -ftree-slp-vectorize -ftree-vectorize -msse3 test.c 你不需要所有的命令,因为在 -O3 中启用的很少。参考https://gcc.gnu.org/projects/tree-ssa/vectorization.html

虽然我没有答案,但我已尽力提供帮助。 当我也指定我的 cpu 架构(构建)时,我得到以下信息:

    .file   "test.c"
    .text
    .p2align 4,,15
    .globl  f
    .type   f, @function
f:
.LFB0:
    .cfi_startproc
    testl   %esi, %esi
    jle     .L4
    vmovss  .LC1(%rip), %xmm1
    leal    -1(%rsi), %eax
    vxorps  %xmm0, %xmm0, %xmm0
    leaq    8(%rdi,%rax,8), %rax
    .p2align 4,,10
    .p2align 3
.L3:
    vmovss  (%rdi), %xmm2
    vmovss  4(%rdi), %xmm3
    addq    , %rdi
    vmulss  %xmm3, %xmm0, %xmm4
    vmulss  %xmm2, %xmm0, %xmm0
    vfmadd231ss     %xmm3, %xmm1, %xmm0
    vfmsub132ss     %xmm2, %xmm4, %xmm1
    cmpq    %rdi, %rax
    jne     .L3
.L1:
    vmovss  %xmm1, -8(%rsp)
    vmovss  %xmm0, -4(%rsp)
    vmovq   -8(%rsp), %xmm0
    ret
.L4:
    vmovss  .LC1(%rip), %xmm1
    vxorps  %xmm0, %xmm0, %xmm0
    jmp     .L1
    .cfi_endproc
.LFE0:
    .size   f, .-f
    .section        .rodata.cst4,"aM",@progbits,4
    .align 4
.LC1:
    .long   1065353216
    .ident  "GCC: (Ubuntu 6.2.0-5ubuntu12) 6.2.0 20161005"
    .section        .note.GNU-stack,"",@progbits

现在的命令是 gcc -S -O3 -ffast-math -msse4 -march=haswell test.c,其中 haswell 是我的 i7 4770HQ cpu。请参阅 this 以获取您的 cpu.

因此,正如您所见,AVX 指令集出现在第二个版本中。

以下代码的示例基准:

$time ./a.out 
0.000000
real    0m0.684s
user    0m0.620s
sys     0m0.060s


#include <stdio.h>
#include <complex.h>
complex float f(complex float x[], long n ) {
  complex float p = 1.0;
  for (long i = 0; i < n; i++)
    p *= x[i];
  return p;
}

int main()
{
    static complex float x[200000000] = {0.0, 1.0, 2.0, 4.0, 5.0, 6.0};
    complex float p = f(x, 200000000);

    printf("%f", creal(p));

    return 0;
}

阵列是静态的,所以大部分都在磁盘上,即 ssd 硬盘驱动器。您可以将其分配到内存中以实现更快的处理速度。这是200M循环。二进制是1.5G所以大部分时间是IO。即使没有 -msse3 和 -march,CPU 也在燃烧它。您所需要的只是-ffast-math。这造成了很大的不同。

我将程序更改为以下内容:

#include <stdio.h>
#include <complex.h>
float f(float x[], long n ) {
    float p = 1.0;
    for (long i = 0; i < 8; i++) {
        p = p * x[i];
    }
    return p;
}

int main() {
    float x[8] = {1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0, 8.0};

    printf("%f\n", f(x, 8));

    return 0;
}

并使用 gcc -S -O3 -ffast-math -msse3 -mfpmath=sse -mavx -march=haswell test.c 编译,结果为:

f:
.LFB23:
    .cfi_startproc
    vmovups (%rdi), %ymm2
    vxorps  %xmm1, %xmm1, %xmm1
    vperm2f128      , %ymm1, %ymm2, %ymm0
    vmulps  %ymm2, %ymm0, %ymm0
    vperm2f128      , %ymm1, %ymm0, %ymm2
    vshufps , %ymm2, %ymm0, %ymm2
    vmulps  %ymm2, %ymm0, %ymm0
    vperm2f128      , %ymm1, %ymm0, %ymm1
    vpalignr        , %ymm0, %ymm1, %ymm1
    vmulps  %ymm1, %ymm0, %ymm0
    vzeroupper
    ret
    .cfi_endproc

所以在我看来,要强制 gcc 使用 SSE3,您需要以某种方式节点编码。 http://sci.tuomastonteri.fi/programming/sse 对你有用。

最后说明:如果您对 i 的上限值使用不同的值进行试验,您会看到生成了不同的指令。我认为这样做的原因是 gcc 不计算变量,所以你可能想使用能够编译时计算的 C++ 模板并执行它。

问题是 complex 类型对 SIMD 不友好。我从来都不是 complex 类型的粉丝,因为它是一个复合对象,通常不会映射到原始类型或硬件中的单一操作(当然不是 x86 硬件)。

为了使复杂算术 SIMD 友好,您需要同时对多个复数进行运算。对于 SSE,您需要一次对四个复数进行运算。

我们可以使用 GCC 的 vector extensions 来简化语法。

typedef float v4sf __attribute__ ((vector_size (16)));

然后我们可以delcare一个数组和向量扩展的并集

typedef union {
  v4sf v;
  float e[4];
} float4

最后我们像这样定义一个由四个复数组成的块

typedef struct {
  float4 x;
  float4 y;
} complex4;

其中 x 是四个实部,y 是四个虚部。

一旦有了这个,我们就可以像这样一次乘以 4 个复数

static complex4 complex4_mul(complex4 a, complex4 b) {
  return (complex4){a.x.v*b.x.v -a.y.v*b.y.v, a.y.v*b.x.v + a.x.v*b.y.v};
}

最后我们将您的函数修改为一次对四个复数进行运算。

complex4 f4(complex4 x[], int n) {
  v4sf one = {1,1,1,1};
  complex4 p = {one,one};
  for (int i = 0; i < n; i++) p = complex4_mul(p, x[i]);
  return p;
}

让我们看看汇编(Intel 语法)是否是最优的

.L3:
    movaps  xmm4, XMMWORD PTR [rsi]
    add     rsi, 32
    movaps  xmm1, XMMWORD PTR -16[rsi]
    cmp     rdx, rsi
    movaps  xmm2, xmm4
    movaps  xmm5, xmm1
    mulps   xmm1, xmm3
    mulps   xmm2, xmm3
    mulps   xmm5, xmm0
    mulps   xmm0, xmm4
    subps   xmm2, xmm5
    addps   xmm0, xmm1
    movaps  xmm3, xmm2
    jne     .L3

正好是四次 4 位乘法、一次 4 位加法和一次 4 位减法。变量 p 保留在寄存器中,只有数组 x 像我们想要的那样从内存中加载。

让我们看看复数乘积的代数

{a, bi}*{c, di} = {(ac - bd),(bc + ad)i}

正好是四次乘法、一次加法和一次减法。

正如我在此 answer 中解释的那样,高效的 SIMD 在代数上通常与标量算法相同。所以我们用四个 4 宽的乘法、加法和减法替换了四个 1 宽的乘法、加法和减法。这是使用 4 宽 SIMD 所能做的最好的事情:一个的价格是四个。

请注意,这不需要 SSE 之外的任何指令,并且没有额外的 SSE 指令(FMA4 除外)会更好。所以在 64 位系统上你可以用 -O3.

编译

使用 AVX 将其扩展为 8 宽 SIMD 是微不足道的。

使用 GCC 的矢量扩展的一个主要优点是您无需任何额外的努力即可获得 FMA。例如。如果你用 -O3 -mfma4 编译,主循环是

.L3:
    vmovaps xmm0, XMMWORD PTR 16[rsi]
    add     rsi, 32
    vmulps  xmm1, xmm0, xmm2
    vmulps  xmm0, xmm0, xmm3
    vfmsubps        xmm1, xmm3, XMMWORD PTR -32[rsi], xmm1
    vmovaps xmm3, xmm1
    vfmaddps        xmm2, xmm2, XMMWORD PTR -32[rsi], xmm0
    cmp     rdx, rsi
    jne     .L3