快速矢量化rsqrt和SSE / AVX的倒数取决于精度

时间:2015-07-22 06:15:21

标签: performance sse simd avx

假设有必要计算打包浮点数据的倒数或倒数平方根。两者都可以通过以下方式轻松完成:

__m128 recip_float4_ieee(__m128 x) { return _mm_div_ps(_mm_set1_ps(1.0f), x); }
__m128 rsqrt_float4_ieee(__m128 x) { return _mm_div_ps(_mm_set1_ps(1.0f), _mm_sqrt_ps(x)); }

这很好但很慢:根据the guide,它们在Sandy Bridge上进行了14和28次循环(吞吐量)。对应的AVX版本在Haswell上几乎占用相同的时间。

另一方面,可以使用以下版本:

__m128 recip_float4_half(__m128 x) { return _mm_rcp_ps(x); }
__m128 rsqrt_float4_half(__m128 x) { return _mm_rsqrt_ps(x); }

它们只需要一到两个时间周期(吞吐量),从而大大提升了性能。但是,它们非常接近:它们产生的结果相对误差小于1.5 * 2 ^ -12。鉴于单精度浮点数的机器epsilon是2 ^ -24,我们可以说这个近似值约为 half 精度。

似乎可以添加Newton-Raphson迭代以产生精度的结果(可能不如IEEE标准所要求的那样精确),见GCC,{{3 } {,ICC的讨论。从理论上讲,同样的方法可以用于双精度值,产生 half double 精度。

我有兴趣为float和double数据类型以及所有(half,single,double)精度实现此方法的实现。处理特殊情况(除以零,sqrt(-1),inf / nan等)不是必需的。另外,我不清楚这些例程中的哪一个比普通的IEEE编译解决方案更快,哪个更慢。

请回答以下几点:

  1. 在代码示例中使用内在函数。程序集依赖于编译器,因此不太有用。
  2. 对函数使用类似的命名约定。
  3. 实现例程,将单个SSE / AVX寄存器包含密集打包的float / double值作为输入。如果有相当大的性能提升,你也可以发布几个寄存器作为输入的例程(两个reg可能是可行的)。
  4. 如果两个SSE / AVX版本完全相同,不要将 _mm 更改为 _mm256 ,反之亦然。
  5. 欢迎任何绩效评估,测量和讨论。

    概要

    以下是具有一次NR迭代的单精度浮点数的版本:

    __m128 recip_float4_single(__m128 x) {
      __m128 res = _mm_rcp_ps(x);
      __m128 muls = _mm_mul_ps(x, _mm_mul_ps(res, res));
      return res =  _mm_sub_ps(_mm_add_ps(res, res), muls);
    }
    __m128 rsqrt_float4_single(__m128 x) {
      __m128 three = _mm_set1_ps(3.0f), half = _mm_set1_ps(0.5f);
      __m128 res = _mm_rsqrt_ps(x); 
      __m128 muls = _mm_mul_ps(_mm_mul_ps(x, res), res); 
      return res = _mm_mul_ps(_mm_mul_ps(half, res), _mm_sub_ps(three, muls)); 
    }
    

    LLVM解释了如何创建其他版本,并包含全面的理论性能分析。

    您可以在此处找到所有已实施的基准测试解决方案: Answer given by Peter Cordes

    Ivy Bridge获得的吞吐量结果如下所示。只有单注册SSE实现已经过基准测试。花费的时间以每次呼叫的周期给出。第一个数字用于半精度(无NR),第二个用于单精度(1个NR迭代),第三个用于2个NR迭代。

      float 上的
    1. recip 1,4 周期与 7 周期相比。
    2. float 上的
    3. rsqrt 需要 1,6 周期,而 14 周期。
    4. double 上的
    5. recip 需要 3,6,9 周期,而 14 周期。
    6. double 上的
    7. rsqrt 需要 3,8,13 个周期,而 28 周期。
    8. 警告:我必须创造性地对原始结果进行整理......

1 个答案:

答案 0 :(得分:12)

在实践中有很多算法的例子。例如:

Newton Raphson with SSE2 - can someone explain me these 3 lines有一个答案解释one of Intel's examples使用的迭代。

对于让我们说Haswell(可以在两个执行端口上进行FP,与之前的设计不同)的性能分析,我将在这里重现代码(每行一个操作)。有关指令吞吐量和延迟的表格,以及有关如何了解更多背景的文档,请参阅http://agner.org/optimize/

// execute (aka dispatch) on cycle 1, results ready on cycle 6
nr = _mm_rsqrt_ps( x );

// both of these execute on cycle 6, results ready on cycle 11
xnr = _mm_mul_ps( x, nr );         // dep on nr
half_nr = _mm_mul_ps( half, nr );  // dep on nr

// can execute on cycle 11, result ready on cycle 16
muls = _mm_mul_ps( xnr , nr );     // dep on xnr

// can execute on cycle 16, result ready on cycle 19
three_minus_muls = _mm_sub_ps( three, muls );  // dep on muls

// can execute on cycle 19, result ready on cycle 24
result = _mm_mul_ps( half_nr, three_minus_muls ); // dep on three_minus_muls
// result is an approximation of 1/sqrt(x), with ~22 to 23 bits of precision in the mantissa.

如果它不是依赖链的一部分,那么其他计算在这里重叠的空间很大。但是,如果代码的每次迭代的数据都依赖于前一次的数据,那么最好使用sqrtps的11周期延迟。或者即使每次循环迭代足够长,乱序执行也无法通过重叠独立迭代来隐藏它们。

要获得sqrt(x)而不是1/sqrt(x) ,请乘以x(如果x可以为零,则修正,例如通过屏蔽({ {1}})CMPPS结果为0.0)。最佳方法是将_mm_andn_ps替换为half_nr。这并没有延长dep链,因为half_xnr = _mm_mul_ps( half, xnr );可以在第11周期开始,但直到结束(第19周期)才需要。half_xnr。与可用的FMA相同:总延迟没有增加。

如果11位精度足够(没有牛顿迭代),Intel's optimization manual (section 11.12.3)建议使用rcpps(rsqrt(x)),这比乘以原始x更差,至少使用AVX。它可能会保存带有128位SSE的movdqa指令,但256b rcpps比256b mul或fma慢。 (并且它允许您在最后一步使用FMA而不是mul免费添加sqrt结果)。

此循环的SSE版本(不考虑任何移动指令)为6 uops。这意味着它应该具有Haswell的吞吐量每3个周期一个(假设两个执行端口可以处理FP mul,而rsqrt在FP add / sub的相对端口上)。在SnB / IvB(可能还有Nehalem)上,它应该具有每5个周期一个的吞吐量,因为mulps和rsqrtps竞争端口0.subps在port1上,这不是瓶颈

对于Haswell,我们可以使用FMA将减法与mul结合起来。但是,分频器/ sqrt单元宽度不超过256b,因此与其他所有器件不同,ymm regs上的divps / sqrtps / rsqrtps / rcpps需要额外的uops和额外的周期。

// vrsqrtps ymm has higher latency
// execute on cycle 1, results ready on cycle 8
nr = _mm256_rsqrt_ps( x );

// both of can execute on cycle 8, results ready on cycle 13
xnr = _mm256_mul_ps( x, nr );         // dep on nr
half_nr = _mm256_mul_ps( half, nr );  // dep on nr

// can execute on cycle 13, result ready on cycle 18
three_minus_muls = _mm256_fnmadd_ps( xnr, nr, three );  // -(xnr*nr) + 3

// can execute on cycle 18, result ready on cycle 23
result = _mm256_mul_ps( half_nr, three_minus_muls ); // dep on three_minus_muls

我们用FMA节省了3个周期。这可以通过使用2-cyle-slow 256b rsqrt来抵消,净延迟减少1个周期(相当于两倍宽)。 SnB / IvB AVX将是128b的24c延迟,256b的26c延迟。

256b FMA版本共使用7个uop。 (VRSQRTPS为3 uops,2为p0,1为p1 / 5.)256b mulps和fma都是单uop指令,两者都可以在端口0或端口1上运行。(p0仅在pre-pre Haswell的)。因此,如果OOO引擎将uop调度到最佳执行端口,则吞吐量应为:每3c一个。 (即来自rsqrt的shuffle uop总是进入p5,永远不会进入p1,它将占用mul / fma带宽。)至于与其他计算重叠(不仅仅是独立执行自身),它再次相当漂亮轻巧。只有7个带有23个周期的dep链的uops为其他东西留下了很大的空间,而那些uops位于重新排序的缓冲区中。

如果这是巨型dep链中的一步而没有其他任何事情发生(甚至不是独立的下一次迭代),那么256b vsqrtps是19周期延迟,吞吐量为每14个周期一个。 (Haswell的)。如果你仍然需要倒数,那么256b vdivps也有18-21c延迟,每14c吞吐量一个。因此对于普通sqrt,指令具有较低的延迟。对于recip sqrt,近似的迭代是较少的延迟。 (如果它与自身重叠,那么FAR会有更大的吞吐量。如果与其他不包含除法单位的东西重叠,sqrtps不是问题。)

sqrtps可以是吞吐量获胜与rsqrt +牛顿迭代,如果它是循环体的一部分,其中包含足够的其他非分割和非sqrt工作除法单位不饱和。

如果您需要sqrt(x),而不是1/sqrt(x) ,则尤其如此。例如在Haswell上使用AVX2,在一个适合L3缓存的浮点数组上的复制+ arcsinh循环,实现为fastlog(v + sqrt(v*v + 1)),与真实VSQRTPS或VRSQRTPS + Newton-Raphson迭代运行的吞吐量大致相同。 (即使使用very fast approximation for log(),总循环体大约为9 FMA / add / mul / convert操作,2布尔值加VSQRTPS ymm。仅使用fastlog(v2_plus_1 * rsqrt(v2_plus_1) + v2_plus_1)有一个加速,所以它没有内存带宽上的瓶颈,但它可能是延迟的瓶颈(因此乱序执行无法利用独立迭代的所有并行性)。

其他精度

对于半精度,没有指令在半浮点数上进行计算。您希望在使用转换说明加载/存储时动态转换。

对于双精度,没有rsqrtpd。据推测,我们的想法是,如果你不需要完全精确,你应该首先使用浮动。因此,您可以转换为浮动和返回,然后执行完全相同的算法,但使用pd而不是ps内在函数。或者,您可以将数据保持浮动一段时间。例如将两个ymm的双打寄存器转换成一个单独的ymm寄存器。

不幸的是,没有一条指令需要两个ymm的双精度寄存器并输出一个单独的ymm。你必须去ymm-> xmm两次,然后_mm256_insertf128_ps一个xmm到另一个的高128。但是,您可以将256b ymm向量提供给相同的算法。

如果你要在之后转换回加倍,那么分别对两个单独的128b寄存器进行rsqrt + Newton-Raphson迭代是有意义的。插入/提取的额外2 uops和256b rsqrt的额外2 uops开始累加,更不用说vinsertf128 / vextractf128的3周期延迟。计算将重叠,两个结果将分开几个周期。 (或者相隔1个周期,如果你有一个特殊版本的代码用于同时对2个输入进行交错操作)。

请记住,单精度的指数范围小于double,因此转换可以溢出到+ Inf或下溢到0.0。如果您不确定,请务必使用正常的_mm_sqrt_pd

使用AVX512F,有_mm512_rsqrt14_pd( __m512d a)。使用AVX512ER (KNL but not SKX or Cannonlake)_mm512_rsqrt28_pd。当然也存在_ps个版本。在更多情况下,14位尾数精度可能足以在没有牛顿迭代的情况下使用。

牛顿迭代仍然不会像常规sqrt那样为你提供正确舍入的单精度浮点数。