英特尔AVX:256位版本的点积,用于双精度浮点变量

时间:2012-05-04 18:21:57

标签: c++ performance simd avx

英特尔高级矢量扩展指令集(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向量,其中包含val1val0val2val0之间点积的结果,val3val0val4val0

也许这可以给出建议的提示?

3 个答案:

答案 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在内部将xyzw以两种不同的方式混合,并将其提供给垂直addpd,这就是我们手工做的事情。如果我们将xyzw分开,我们需要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 tableshaddpd 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吞吐量一个)。