Matt Scarpino给出了一个很好的解释(虽然他承认他不确定它是最优算法,但我感谢他),感谢他如何将两个复杂的双倍乘以英特尔' s AVX内在函数。这是他的方法,我已经验证过:
__m256d vec1 = _mm256_setr_pd(4.0, 5.0, 13.0, 6.0);
__m256d vec2 = _mm256_setr_pd(9.0, 3.0, 6.0, 7.0);
__m256d neg = _mm256_setr_pd(1.0, -1.0, 1.0, -1.0);
/* Step 1: Multiply vec1 and vec2 */
__m256d vec3 = _mm256_mul_pd(vec1, vec2);
/* Step 2: Switch the real and imaginary elements of vec2 */
vec2 = _mm256_permute_pd(vec2, 0x5);
/* Step 3: Negate the imaginary elements of vec2 */
vec2 = _mm256_mul_pd(vec2, neg);
/* Step 4: Multiply vec1 and the modified vec2 */
__m256d vec4 = _mm256_mul_pd(vec1, vec2);
/* Horizontally subtract the elements in vec3 and vec4 */
vec1 = _mm256_hsub_pd(vec3, vec4);
/* Display the elements of the result vector */
double* res = (double*)&vec1;
printf("%lf %lf %lf %lf\n", res[0], res[1], res[2], res[3]);
我的问题是我想要两个复杂的双打。我尝试使用马特的技术:
struct cmplx a;
struct cmplx b;
a.r = 2.5341;
a.i = 1.843;
b.r = 1.3941;
b.i = 0.93;
__m256d zzs = squareZ(a, b);
double* res = (double*) &zzs;
printf("\nA: %f + %f, B: %f + %f\n", res[0], res[1], res[2], res[3]);
使用Haskell的复杂算法,我已经验证结果是正确的除了,正如您所看到的, B 的真实部分:
A: 3.025014 + 9.340693, B: 0.000000 + 2.593026
所以我真的有两个问题:是否有更好的(更简单和/或更快)的方法来与AVX内在函数对齐两个复杂的双精度?如果没有,我如何修改Matt的代码呢?
答案 0 :(得分:8)
理想情况下,将数据存储在单独的实数和虚数数组中,因此您只需加载实部和虚部的连续向量。这使得它可以自由地进行交叉乘法(只需使用不同的寄存器/变量),而不必在向量内乱码。
您可以相当便宜地在交错的double complex
样式和SIMD友好的单独矢量样式之间进行转换,这取决于AVX车道内洗牌的变幻莫测。例如如果您不关心临时矢量中数据的实际顺序,那么非常便宜地使用unpacklo / unpackhi shuffle来解交错或在一个通道内重新交错。
实际上这样做是非常便宜的,因为单个复杂的乘法运行它在某种程度上超过了Matt的代码(甚至是经过调整的版本),特别是在支持的CPU上FMA。这需要以4个复数双精度(2个结果向量)的组生成结果。
如果你一次只需要生成一个结果向量,我还想出了一个替代Matt的算法,可以使用FMA(实际上是FMADDSUB)并避免单独的符号变换insn < /强>
只要你使用-ffast-math
,gcc就可以将简单的复数乘法标量循环自动矢量化为非常好的代码。它像我建议的那样解开。
#include <complex.h>
// even with -ffast-math -ffp-contract=fast, clang doesn't manage to use vfmaddsubpd, instead using vmulpd and vaddsubpd :(
// gcc does use FMA though.
// auto-vectorizes with a lot of extra shuffles
void cmul(double complex *restrict dst,
const double complex *restrict A, const double complex *restrict B)
{ // clang and gcc change strategy slightly for i<1 or i<2, vs. i<4
for (int i=0; i<4 ; i++) {
dst[i] = A[i] * B[i];
}
}
查看Godbolt compiler explorer上的asm。我不确定铿锵有多好;它使用了大量64b-> 128b VMODDDUP broadcast-loads。此表单仅在Intel CPU上的加载端口中处理(请参阅Agner Fog's insn tables),但它仍然需要执行大量操作。如前所述,gcc在乘以/ FMA之前使用4个VPERMPD shuffle在通道内重新排序,然后在将它们与VSHUFPD组合之前再对4个VPERMPD进行重新排序。这是4次复杂乘法的8次额外洗牌。
将gcc的版本转换回内在函数并删除冗余shuffle可提供最佳代码。 (gcc显然希望它的临时代码符合A B C D顺序,而不是由VUNPCKLPD (_mm256_unpacklo_pd
)的内部行为导致的A C B D顺序。
我将代码on Godbolt与Matt代码的调整版本放在一起。因此,您可以使用不同的编译器选项,以及不同的编译器版本。
// multiplies 4 complex doubles each from A and B, storing the result in dst[0..3]
void cmul_manualvec(double complex *restrict dst,
const double complex *restrict A, const double complex *restrict B)
{
// low element first, little-endian style
__m256d A0 = _mm256_loadu_pd((double*)A); // [A0r A0i A1r A1i ] // [a b c d ]
__m256d A2 = _mm256_loadu_pd((double*)(A+2)); // [e f g h ]
__m256d realA = _mm256_unpacklo_pd(A0, A2); // [A0r A2r A1r A3r ] // [a e c g ]
__m256d imagA = _mm256_unpackhi_pd(A0, A2); // [A0i A2i A1i A3i ] // [b f d h ]
// the in-lane behaviour of this interleaving is matched by the same in-lane behaviour when we recombine.
__m256d B0 = _mm256_loadu_pd((double*)B); // [m n o p]
__m256d B2 = _mm256_loadu_pd((double*)(B+2)); // [q r s t]
__m256d realB = _mm256_unpacklo_pd(B0, B2); // [m q o s]
__m256d imagB = _mm256_unpackhi_pd(B0, B2); // [n r p t]
// desired: real=rArB - iAiB, imag=rAiB + rBiA
__m256d realprod = _mm256_mul_pd(realA, realB);
__m256d imagprod = _mm256_mul_pd(imagA, imagB);
__m256d rAiB = _mm256_mul_pd(realA, imagB);
__m256d rBiA = _mm256_mul_pd(realB, imagA);
// gcc and clang will contract these nto FMA. (clang needs -ffp-contract=fast)
// Doing it manually would remove the option to compile for non-FMA targets
__m256d real = _mm256_sub_pd(realprod, imagprod); // [D0r D2r | D1r D3r]
__m256d imag = _mm256_add_pd(rAiB, rBiA); // [D0i D2i | D1i D3i]
// interleave the separate real and imaginary vectors back into packed format
__m256d dst0 = _mm256_shuffle_pd(real, imag, 0b0000); // [D0r D0i | D1r D1i]
__m256d dst2 = _mm256_shuffle_pd(real, imag, 0b1111); // [D2r D2i | D3r D3i]
_mm256_storeu_pd((double*)dst, dst0);
_mm256_storeu_pd((double*)(dst+2), dst2);
}
Godbolt asm output: gcc6.2 -O3 -ffast-math -ffp-contract=fast -march=haswell
vmovupd ymm0, YMMWORD PTR [rsi+32]
vmovupd ymm3, YMMWORD PTR [rsi]
vmovupd ymm1, YMMWORD PTR [rdx]
vunpcklpd ymm5, ymm3, ymm0
vunpckhpd ymm3, ymm3, ymm0
vmovupd ymm0, YMMWORD PTR [rdx+32]
vunpcklpd ymm4, ymm1, ymm0
vunpckhpd ymm1, ymm1, ymm0
vmulpd ymm2, ymm1, ymm3
vmulpd ymm0, ymm4, ymm3
vfmsub231pd ymm2, ymm4, ymm5 # separate mul/sub contracted into FMA
vfmadd231pd ymm0, ymm1, ymm5
vunpcklpd ymm1, ymm2, ymm0
vunpckhpd ymm0, ymm2, ymm0
vmovupd YMMWORD PTR [rdi], ymm1
vmovupd YMMWORD PTR [rdi+32], ymm0
vzeroupper
ret
对于4个复数乘法(2对输入向量),我的代码使用:
因此,对于吞吐量,这是洗牌端口的完全瓶颈。 (每个时钟吞吐量1次洗牌,而英特尔Haswell及更高版本每个时钟2次总MUL / FMA / ADD)。这就是为什么打包存储非常糟糕:shuffle的吞吐量有限,并且花费更多的指令改组而不是数学运算并不好。
Matt Scarpino's代码与我的小调整(重复做4次复数乘法)。 (请参阅下文,了解我一次更有效地生成一个向量的重写)。
-0.0
)。Matt代码的主要优点是它只能同时使用一个向量宽度的复数乘法,而不需要你有4个数据输入向量。它的延迟稍低。但请注意,我的版本不需要2对输入向量来自连续内存,或者根本不相关。它们在处理时混合在一起,但结果是2个独立的32B向量。
我调整过的Matt代码版本几乎和没有FMA的CPU一样好(就像4次一样的版本)(只需花费额外的VXORPD),但是当它阻止我们服用时会更糟糕FMA的优势。此外,它永远不会以非打包形式提供结果,因此您不能将分离的形式用作另一个乘法的输入并跳过改组。
如果您有时会平方而不是将两个不同的复数相乘,请不要使用它。这就像Matt的算法一样,常见的子表达式消除并不简化。
我还没有输入C内在函数,只是计算了数据运动。由于所有洗牌都在车道内,我只会显示低车道。使用256b版本的相关说明在两个通道中执行相同的随机播放。他们分开。
// MULTIPLY: for each AVX lane: am-bn, an+bm
r i r i
a b c d // input vectors: a + b*i, etc.
m n o p
<强>算法强>:
bm bn
bn bm
。 (或者在mul之前用shuffle创建n m
)a a
使用fmaddsubpd生成最终结果:[a|a]*[m|n] -/+ [bn|bm]
。
是的,SSE / AVX有ADDSUBPD交替减去/添加偶数/奇数元素(按此顺序,可能是因为这个用例)。 FMA包括减去和添加的FMADDSUB132PD(反之,加法和减法的FMSUBADD)。
每4个结果:6x shuffle,2x mul,2xfmaddsub。因此,除非我出错了,它与解交织方法一样有效(当不是相同的数字时)。 Skylake等待时间= 10c = 1 + 4 + 1以创建bn bm
(与1个周期重叠以创建a a
),+ 4(FMA)。所以它的延迟比Matt的低一个周期。
在Bulldozer系列中,将两个输入混合到第一个mul将是一个胜利,因此mul-> fmaddsub关键路径保持在FMA域内(1个周期的低延迟)。另一种方式是通过过早地执行movsldup(a b)并延迟mulpd来帮助阻止愚蠢的编译器发生资源冲突。 (但是,在一个循环中,许多迭代将在飞行中并且在洗牌端口上出现瓶颈。)
对于平方来说,这仍然比Matt更好(仍然保存XOR,并且可以使用FMA),但我们不会保存任何随机播放:
// SQUARING: for each AVX lane: aa-bb, 2*ab
// ab bb // movshdup + mul
// bb ab // ^ -> shufpd
// a a // movsldup
// aa-bb ab+ab // fmaddsubpd : [a|a]*[a|b] -/+ [bb|ab]
// per 4 results: 6x shuffle, 2x mul, 2xfmaddsub
我也玩过(a+b) * (a+b) = aa+2ab+bb
或(r-i)*(r+i) = rr - ii
之类的一些可能性,但没有到达任何地方。步骤之间的舍入意味着FP数学没有完全取消,所以做这样的事情甚至不会产生完全相同的结果。
答案 1 :(得分:3)
TL:DR :只需使用我的其他答案中的代码,两个输入都相同。编译器在冗余方面做得很好。
Squaring略微简化了数学运算:rAiB和rBiA不再需要两种不同的交叉积,而是相同。但它仍然需要加倍,所以基本上我们最终得到2 mul + 1 FMA + 1加,而不是2 mul + 2 FMA。
使用SIMD不友好的交错存储格式,它为解交织方法提供了很大的推动力,因为只有一个输入可以随机播放。 Matt的方法根本没有任何好处,因为它计算了具有相同向量的两个交叉乘积。
使用我的其他答案中的cmul_manualvec():
// squares 4 complex doubles from A[0..3], storing the result in dst[0..3]
void csquare_manual(double complex *restrict dst,
const double complex *restrict A) {
cmul_manualvec(dst, A, A);
}
gcc和clang非常聪明,可以优化两次使用相同输入的冗余,因此无需使用内在函数制作自定义版本。 clang在标量自动矢量化版本上做得不好,所以不要使用它。我没有看到有关此asm输出(from Godbolt)的任何内容:
clang3.9 -O3 -ffast-math -ffp-contract=fast -march=haswell
vmovupd ymm0, ymmword ptr [rsi]
vmovupd ymm1, ymmword ptr [rsi + 32]
vunpcklpd ymm2, ymm0, ymm1
vunpckhpd ymm0, ymm0, ymm1 # doing this shuffle first would let the first multiply start a cycle earlier. Silly compiler.
vmulpd ymm1, ymm0, ymm0 # imag*imag
vfmsub231pd ymm1, ymm2, ymm2 # real*real - imag*imag
vaddpd ymm0, ymm0, ymm0 # imag+imag = 2*imag
vmulpd ymm0, ymm2, ymm0 # 2*imag * real
vunpcklpd ymm2, ymm1, ymm0
vunpckhpd ymm0, ymm1, ymm0
vmovupd ymmword ptr [rdi], ymm2
vmovupd ymmword ptr [rdi + 32], ymm0
vzeroupper
ret
可能不同的指令排序会更好,可能会减少资源冲突。例如将实数向量加倍,因为它首先被解包,所以VADDPD可以在imag * imag VMULPD之前更快地开始一个周期。但是,在C源中重新排序行通常不会直接转换为asm重新排序,因为现代编译器是复杂的动物。 (IIRC,gcc并没有特别尝试安排x86的指令,因为无序执行主要隐藏了这些效果。)
无论如何,每4个复杂的正方形:
马特的版本仍然是6次洗牌,其他一切都是。