英特尔高级矢量扩展指令集(AVX)在256位版本(YMM寄存器)中不提供双精度浮点变量的点积。 “为什么?”问题已在另一个论坛(here)和Stack Overflow(here)上进行了简要处理。但我面临的问题是如何以有效的方式用其他AVX指令替换这条缺失的指令?
对于单精度浮点变量(reference here),存在256位版本的点积:
__m256 _mm256_dp_ps(__m256 m1, __m256 m2, const int mask);
我们的想法是找到这个缺失指令的有效等价物:
__m256d _mm256_dp_pd(__m256d m1, __m256d m2, const int mask);
更具体地说,我想从__m128
(四个浮点数)转换为__m256d
(4个双打)的代码使用以下指令:
__m128 val0 = ...; // Four float values
__m128 val1 = ...; //
__m128 val2 = ...; //
__m128 val3 = ...; //
__m128 val4 = ...; //
__m128 res = _mm_or_ps( _mm_dp_ps(val1, val0, 0xF1),
_mm_or_ps( _mm_dp_ps(val2, val0, 0xF2),
_mm_or_ps( _mm_dp_ps(val3, val0, 0xF4),
_mm_dp_ps(val4, val0, 0xF8) )));
此代码的结果是包含四个浮点数的_m128
向量,其中包含val1
和val0
,val2
和val0
之间点积的结果,val3
和val0
,val4
和val0
。
也许这可以给出建议的提示?
答案 0 :(得分:24)
我会使用4 *双乘,然后是hadd
(不幸的是,在上半部分和下半部分只添加了2 * 2个浮点数),提取上半部分(一个shuffle应该同样工作,也许更快)并将其添加到下半部分。
结果是dotproduct
的低64位。
__m256d xy = _mm256_mul_pd( x, y );
__m256d temp = _mm256_hadd_pd( xy, xy );
__m128d hi128 = _mm256_extractf128_pd( temp, 1 );
__m128d dotproduct = _mm_add_pd( (__m128d)temp, hi128 );
编辑:
在了解了Norbert P.的想法后,我将这个版本扩展为一次制作4个点产品。
__m256d xy0 = _mm256_mul_pd( x[0], y[0] );
__m256d xy1 = _mm256_mul_pd( x[1], y[1] );
__m256d xy2 = _mm256_mul_pd( x[2], y[2] );
__m256d xy3 = _mm256_mul_pd( x[3], y[3] );
// low to high: xy00+xy01 xy10+xy11 xy02+xy03 xy12+xy13
__m256d temp01 = _mm256_hadd_pd( xy0, xy1 );
// low to high: xy20+xy21 xy30+xy31 xy22+xy23 xy32+xy33
__m256d temp23 = _mm256_hadd_pd( xy2, xy3 );
// low to high: xy02+xy03 xy12+xy13 xy20+xy21 xy30+xy31
__m256d swapped = _mm256_permute2f128_pd( temp01, temp23, 0x21 );
// low to high: xy00+xy01 xy10+xy11 xy22+xy23 xy32+xy33
__m256d blended = _mm256_blend_pd(temp01, temp23, 0b1100);
__m256d dotproduct = _mm256_add_pd( swapped, blended );
答案 1 :(得分:12)
我会延长drhirsch's answer同时执行两个点产品,省去了一些工作:
__m256d xy = _mm256_mul_pd( x, y );
__m256d zw = _mm256_mul_pd( z, w );
__m256d temp = _mm256_hadd_pd( xy, zw );
__m128d hi128 = _mm256_extractf128_pd( temp, 1 );
__m128d dotproduct = _mm_add_pd( (__m128d)temp, hi128 );
然后dot(x,y)
处于低位,而dot(z,w)
处于dotproduct
的高位。
答案 2 :(得分:2)
对于单个点积,它只是一个垂直乘法和水平和(见Fastest way to do horizontal float vector sum on x86)。 hadd
费用为2 shuffle + 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在英特尔的1次shuffle uop上的单向量回答要好,而在AMD Jaguar / Bulldozer-family / Ryzen上获得更大的胜利,因为它会立即缩小到128b做一堆256b的东西。 AMD将256b操作分成两个128b uops。
在你使用2个不同的输入向量并行处理2个或4个点产品的情况下,值得使用hadd
。如果你想要结果打包,Norbert的dot
两对矢量看起来是最佳的。即使用AVX2 vpermpd
作为一个交叉点的随机播放,我也没有办法做得更好。
当然,如果你真的想要一个更大的dot
(8个或更多double
个),请使用垂直add
(使用多个累加器隐藏vaddps
延迟)并在结束时进行水平求和。如果可用,您还可以使用fma
。
haddpd
在内部将xy
和zw
以两种不同的方式混合,并将其提供给垂直addpd
,这就是我们手工做的事情。如果我们将xy
和zw
分开,我们需要2次shuffle + 2次添加以获得点积(在单独的寄存器中)。因此,通过将它们与hadd
一起移动作为第一步,我们可以节省总洗牌次数,仅用于添加和总uop计数。
/* 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
*/
但对于AMD来说,vextractf128
非常便宜,而256b hadd
的成本是128b hadd
的2倍,那么将每个256b产品分别缩小到128b是有意义的。然后结合128b hadd。
实际上,根据Agner Fog's tables,haddpd xmm,xmm
在Ryzen上是4 uops。 (并且256b ymm版本是8 uops)。因此,如果数据是正确的,那么在Ryzen上手动使用2x vshufpd
+ vaddpd
实际上更好。它可能不是:他的Piledriver数据有3个uop haddpd xmm,xmm
,并且它只有4个uop和一个内存操作数。对我来说没有意义的是,他们无法将hadd
实现为只有3(或y为6)uops。
为了将4 dot
的结果打包到一个__m256d
中,确切的问题,我认为@hirschhornsalz的答案看起来非常适合英特尔CPU。我没有仔细研究它,但与hadd
成对组合是好的。 vperm2f128
对英特尔有效(但在AMD上非常糟糕:Ryzen上有8个uops,每3c吞吐量一个)。