我的std::vector<double> X,Y
大小N
(N%16==0
},我想计算sum(X[i]*Y[i])
。这是 Fused Multiply和Add (FMA)的经典用例,它应该在支持AVX的处理器上快速运行。我知道我所有的目标CPU都是Intel,Haswell或更新。
如何让GCC发出该AVX代码? -mfma
是解决方案的一部分,但是我需要其他交换机吗?
并且std::vector<double>::operator[]
阻碍了这个吗?我知道我可以改变
size_t N = X.size();
double sum = 0.0;
for (size_t i = 0; i != N; ++i) sum += X[i] * Y[i];
到
size_t N = X.size();
double sum = 0.0;
double const* Xp = &X[0];
double const* Yp = &X[0];
for (size_t i = 0; i != N; ++i) sum += Xp[i] * Yp[i];
因此编译器可以发现&X[0]
在循环中没有变化。但这是否足够甚至是必要的?
当前的编译器是GCC 4.9.2,Debian 8,但如果需要可以升级到GCC 5.
答案 0 :(得分:3)
double foo(std::vector<double> &X, std::vector<double> &Y) {
size_t N = X.size();
double sum = 0.0;
for (size_t i = 0; i <N; ++i) sum += X[i] * Y[i];
return sum;
}
进入http://gcc.godbolt.org/并使用-O3 -mfma
查看GCC 4.9.2中的程序集,我看到了
.L3:
vmovsd (%rcx,%rax,8), %xmm1
vfmadd231sd (%rsi,%rax,8), %xmm1, %xmm0
addq $1, %rax
cmpq %rdx, %rax
jne .L3
所以它使用fma。但是,它不会对循环进行矢量化(s
中的sd
表示单个(即未打包),d
表示双浮点。“
要对循环进行矢量化,您需要启用关联数学,例如与-Ofast
。使用-Ofast -mavx2 -mfma
给出
.L8:
vmovupd (%rax,%rsi), %xmm2
addq $1, %r10
vinsertf128 $0x1, 16(%rax,%rsi), %ymm2, %ymm2
vfmadd231pd (%r12,%rsi), %ymm2, %ymm1
addq $32, %rsi
cmpq %r10, %rdi
ja .L8
所以现在它被矢量化了(pd
意味着打包的双打)。但是,它没有展开。这是目前GCC的限制。由于依赖关系链,您需要多次展开。如果你想让编译器为你做这个,那么考虑使用Clang,它会展开四次,否则用内在函数手动展开。
请注意,与GCC不同,Clang默认情况下不使用fma -mfma
。为了使用带有Clang的fma,请使用-ffp-contract=fast
(例如-O3 -mfma -ffp-contract=fast
)或#pragma STDC FP_CONTRACT ON
或启用关联数学,例如-Ofast
如果您想使用Clang对循环进行矢量化,那么您无论如何都要启用关联数学。
有关使用不同编译器启用fma的详细信息,请参阅Fused multiply add and default rounding modes和https://stackoverflow.com/a/34461738/2542702。
GCC创建了许多额外的代码来处理错位,而N
则不是8的倍数。您可以告诉编译器假设数组使用__builtin_assume_aligned
对齐,并且N是8使用N & -8
以下代码-Ofast -mavx2 -mfma
double foo2(double * __restrict X, double * __restrict Y, int N) {
X = (double*)__builtin_assume_aligned(X,32);
Y = (double*)__builtin_assume_aligned(Y,32);
double sum = 0.0;
for (int i = 0; i < (N &-8); ++i) sum += X[i] * Y[i];
return sum;
}
产生以下简单装配
andl $-8, %edx
jle .L4
subl $4, %edx
vxorpd %xmm0, %xmm0, %xmm0
shrl $2, %edx
xorl %ecx, %ecx
leal 1(%rdx), %eax
xorl %edx, %edx
.L3:
vmovapd (%rsi,%rdx), %ymm2
addl $1, %ecx
vfmadd231pd (%rdi,%rdx), %ymm2, %ymm0
addq $32, %rdx
cmpl %eax, %ecx
jb .L3
vhaddpd %ymm0, %ymm0, %ymm0
vperm2f128 $1, %ymm0, %ymm0, %ymm1
vaddpd %ymm1, %ymm0, %ymm0
vzeroupper
ret
.L4:
vxorpd %xmm0, %xmm0, %xmm0
ret
答案 1 :(得分:0)
我不确定这会让你一路走来,但我几乎可以肯定解决方案的很大一部分。
你必须将循环分为两个:0到N,步骤M> 1。我尝试使用16,8,4的M,并查看asm。并且内部循环为0到M.不要担心数学迭代器数学。 Gcc足够聪明。
Gcc应该展开内部循环,它们可以SIMD并且可以使用FMA。