我有一个简单的循环,取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
答案 0 :(得分:2)
问题是complex
类型不是SIMD友好的。我从来不是complex
类型的粉丝,因为它是一个复合对象,通常不会映射到硬件中的基本类型或单个操作(当然不会使用x86硬件)。
为了使复杂的算法SIMD友好,您需要同时操作多个复数。对于SSE,您需要同时操作四个复数。
我们可以使用GCC&#39; 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;
}
让我们看一下程序集(英特尔语法),看看它是否是最佳的
.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,你可以做到最好:四个以1的价格。
请注意,这不需要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
答案 1 :(得分:1)
我不是装配专家,但我管理过以下内容。我会评论,但它太大了:
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 $8, %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 $8, %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 $33, %ymm1, %ymm2, %ymm0
vmulps %ymm2, %ymm0, %ymm0
vperm2f128 $33, %ymm1, %ymm0, %ymm2
vshufps $78, %ymm2, %ymm0, %ymm2
vmulps %ymm2, %ymm0, %ymm0
vperm2f128 $33, %ymm1, %ymm0, %ymm1
vpalignr $4, %ymm0, %ymm1, %ymm1
vmulps %ymm1, %ymm0, %ymm0
vzeroupper
ret
.cfi_endproc
所以在我看来,迫使gcc使用SSE3你的节点以某种方式编码。 http://sci.tuomastonteri.fi/programming/sse对您有用。
最后的注释:如果您尝试使用不同的上限值,您将看到生成了不同的指令。我认为这样做的原因是gcc不评估变量,所以你可能想要使用能够进行编译时计算的C ++模板。