29

英特尔高级矢量扩展 (AVX)在 256 位版本(YMM 寄存器)中不为双精度浮点变量提供点积。“为什么?” 这个问题已经在另一个论坛(这里)和 Stack Overflow(这里)上得到了非常简短的处理。但我面临的问题是如何以有效的方式用其他 AVX 指令替换这个缺失的指令?

单精度浮点变量存在 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之间的点积结果。val1val0val2val0val3val0val4val0

也许这可以为建议提供提示?

4

3 回答 3

26

我会使用 4*double 乘法,然后使用 a hadd(不幸的是,它只在上半部分和下半部分添加了 2*2 浮点数),提取上半部分(随机播放应该同样有效,也许更快)并将其添加到下半部分。

结果在dotproduct.

__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 );
于 2012-05-04T18:42:44.320 回答
12

我会扩展drhirsch 的答案以同时执行两个点积,节省一些工作:

__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

于 2012-05-05T05:06:35.720 回答
6

对于单个点积,它只是一个垂直乘法和水平和(请参阅在 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,这就是我们无论如何都要手动做的事情。如果我们保持xyzw分开,我们需要 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 吞吐量一个)。

于 2017-11-22T23:24:15.303 回答