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

埃莉诺拉

我有一个简单的循环,取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    $8, %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
玻色子

问题是该complex类型不是SIMD友好的。我从来都不喜欢这种complex类型的风扇,因为它是一个复合对象,通常不会映射到原始类型或硬件中的单个操作(某些情况下不是x86硬件)。

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

我们可以使用GCC的向量扩展名来简化语法。

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

然后我们可以删除数组和向量扩展的并集

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}

那就是四个乘法,一个加法和一个减法。

正如我在此答案中解释的那样,高效的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

本文收集自互联网,转载请注明来源。

如有侵权,请联系 [email protected] 删除。

编辑于
0

我来说两句

0 条评论
登录 后参与评论

相关文章