假设有必要计算打包浮点数据的倒数或倒数平方根。两者都可以通过以下方式轻松完成:
__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编译解决方案更快,哪个更慢。
请回答以下几点:
欢迎任何绩效评估,测量和讨论。
以下是具有一次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迭代。
警告:我必须创造性地对原始结果进行整理......
答案 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那样为你提供正确舍入的单精度浮点数。