对于单个点积,它只是一个垂直乘法和水平和(请参阅在 x86 上进行水平浮点向量求和的最快方法)。 hadd
花费 2 次洗牌 + 一次add
。当使用两个输入 = 相同的向量时,吞吐量几乎总是次优的。
// both elements = dot(x,y)
__m128d dot1(__m256d x, __m256d y) {
__m256d xy = _mm256_mul_pd(x, y);
__m128d xylow = _mm256_castps256_pd128(xy); // (__m128d)cast isn't portable
__m128d xyhigh = _mm256_extractf128_pd(xy, 1);
__m128d sum1 = _mm_add_pd(xylow, xyhigh);
__m128d swapped = _mm_shuffle_pd(sum1, sum1, 0b01); // or unpackhi
__m128d dotproduct = _mm_add_pd(sum1, swapped);
return dotproduct;
}
如果您只需要一个点积,这比 @hirschhornsalz 的单向量答案要好,在 Intel 上 1 shuffle uop,在 AMD Jaguar / Bulldozer-family / Ryzen 上取得更大的胜利,因为它立即缩小到 128b 而不是做一堆256b的东西。AMD 将 256b 的操作分成两个 128b 的 uop。
在并行执行 2 个或 4 个点积的情况下,它可能值得使用hadd
,您将它与 2 个不同的输入向量一起使用。如果您想要打包结果, Norbert 的dot
两对向量看起来是最佳的。vpermpd
即使将 AVX2用作车道交叉洗牌,我也看不出有任何方法可以做得更好。
当然,如果您真的想要更大dot
的(8double
秒或更多),请使用垂直add
(使用多个累加器来隐藏vaddps
延迟)并在最后进行水平求和。 如果可用,您也可以使用fma
。
haddpd
在内部将两种不同的方式混合在一起,xy
并将zw
其提供给 vertical addpd
,这就是我们无论如何都要手动做的事情。如果我们保持xy
并zw
分开,我们需要 2 次洗牌 + 2 次加法来获得一个点积(在单独的寄存器中)。因此,第一步将它们混洗在一起hadd
,我们节省了混洗的总数,只节省了添加数和微指令总数。
/* Norbert's version, for an Intel CPU:
__m256d temp = _mm256_hadd_pd( xy, zw ); // 2 shuffle + 1 add
__m128d hi128 = _mm256_extractf128_pd( temp, 1 ); // 1 shuffle (lane crossing, higher latency)
__m128d dotproduct = _mm_add_pd( (__m128d)temp, hi128 ); // 1 add
// 3 shuffle + 2 add
*/
但是对于vextractf128
非常便宜的 AMD 来说,256bhadd
的成本是 128b 的 2 倍hadd
,因此将每个 256b 产品分别缩小到 128b,然后与 128b hadd 结合使用是有意义的。
实际上,根据Agner Fog 的表格,haddpd xmm,xmm
在 Ryzen 上是 4 uops。(而 256b ymm 版本是 8 uops)。因此,如果数据正确,实际上最好在 Ryzen 上手动使用 2x vshufpd
+ 。vaddpd
可能不是:他的 Piledriver 数据有 3 uop haddpd xmm,xmm
,而它只有 4 uop 和一个内存操作数。对我来说,他们不能hadd
仅实现 3 个(或 6 个 ymm)微指令,这对我来说没有意义。
dot
对于将结果打包成一个4 s __m256d
,确切的问题是,我认为@hirschhornsalz 的答案对于英特尔 CPU 来说看起来非常好。我没有仔细研究过它,但是成对结合hadd
是很好的。 vperm2f128
在 Intel 上效率很高(但在 AMD 上相当糟糕:Ryzen 上 8 微指令,每 3c 吞吐量一个)。