__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)); }
这个代码可以正常工作但速度较慢:根据指南,在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到2个周期的时间(吞吐量),从而大大提高性能。然而,它们非常近似:它们产生的结果相对误差小于1.5 * 2^-12。考虑到单精度浮点数的机器epsilon为2^−24,我们可以说这种近似具有约半精度。
看起来可以添加牛顿-拉弗森迭代以产生带有单精度的结果(可能不如IEEE标准要求的那么精确),请参见GCC,ICC,以及LLVM上的讨论。理论上,相同的方法可以用于双精度值,产生半精度或单精度或双精度。
我对此方法的实现很感兴趣,包括浮点和双精度数据类型以及所有(半精度、单精度、双精度)精度。处理特殊情况(除零、sqrt(-1)、inf / nan等)并不是必要的。此外,我不清楚这些例程中哪些比微不足道的IEEE兼容解决方案更快,哪些会更慢。
以下是答案的一些小限制,请注意:
- 在代码示例中使用内部函数。汇编语言依赖于编译器,因此不太有用。
- 使用类似的函数命名约定。
- 实现以单个SSE / AVX寄存器为输入的紧密打包的float / double值的例程。如果有相当大的性能提升,您还可以发布采用几个寄存器作为输入的例程(两个寄存器可能可行)。
- 如果它们绝对相等,不要同时发布SSE / AVX版本,直到更改_mm为_mm256或反之亦然。
总结:
以下是单精度浮点数的版本,其中包含一个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));
}
Peter Cordes提供的答案解释了如何创建其他版本,并包含了彻底的理论性能分析。
您可以在这里找到所有实现的解决方案和基准测试结果:recip_rsqrt_benchmark。
在Ivy Bridge上获得的吞吐量结果如下所示。仅对单寄存器SSE实现进行了基准测试。时间以每次调用的周期数给出。第一个数字是半精度(无NR),第二个数字是单精度(1个NR迭代),第三个数字是2个NR迭代。
- float 上的 recip 需要 1, 4 个周期,而不是 7 个周期。
- float 上的 rsqrt 需要 1, 6 个周期,而不是 14 个周期。
- double 上的 recip 需要 3, 6, 9 个周期,而不是 14 个周期。
- double 上的 rsqrt 需要 3, 8, 13 个周期,而不是 28 个周期。
警告:我必须创造性地四舍五入原始结果...
_mm256_rsqrt_ps
替换为上一次迭代的最终结果。我链接的SO问题中有写出漂亮数学格式的迭代公式。它比vsqrtps ymm / vdivps ymm
具有更好的吞吐量,但延迟略低。 - Peter Cordesvrcpps
),给定迭代公式或特别是公式的SSE实现。只需找到依赖关系,看看哪些可以同时执行即可。 - Peter Cordes