为什么GCC或Clang在使用快速数学时不优化到1指令的倒数



有人知道为什么GCC/Clang在下面的代码示例中不会使用test1来简单地使用RCPPS指令时使用快速数学选项吗?是否有另一个编译器标志来生成此代码?

typedef float float4 __attribute__((vector_size(16)));
float4 test1(float4 v)
{
    return 1.0f / v;
}

您可以在这里看到编译后的输出:https://goo.gl/jXsqat

因为RCPPS的精度float的除法精度低很多

启用该优化的选项不适合作为-ffast-math的一部分。

gcc手册的x86目标选项说实际上有一个选项(与-ffast-math)确实让gcc使用它们(与Newton-Raphson迭代-快速矢量化rsqrt和SSE/AVX的倒数取决于精度/Newton Raphson与SSE2 -有人能解释我这3行- SIMD和标量基本上每条指令具有相同的性能,牛顿迭代数学是相同的):

  • -mrecip该选项支持使用RCPSS和RSQRTSS指令(以及它们的矢量化变体RCPPS和RSQRTPS),并附带一个附加功能牛顿-拉夫森步来提高精度,而不是DIVSS和SQRTSS(以及它们的矢量化变体)用于单精度浮点数参数。这些指令仅在以下情况下生成-funsafe-math-optimizations与- limited -math-only和-fno-trapping-math一起启用。注意,虽然序列的吞吐量大于非互易指令的吞吐量,则序列的精度最多可以降低2个ulp(即1.0的倒数等于0.99999994)

注意GCC在RSQRTSS(或RSQRTPS)方面已经使用- fast-math(或上述选项组合)实现了1.0f/sqrtf(x),不需要-mrecip

还需要注意的是,GCC发出的上述序列带有额外的Newton-Raphson步骤,用于向量化的单浮点除法和矢量化sqrtf(x)已经使用- fast-math(或上述选项)组合),不需要-mrecip。

  • -mrecip=opt

此选项控制可以使用哪些互反估计指令。Opt是一个逗号分隔的选项列表,前面可能有差一个!'来反转选项:

’all’
      Enable all estimate instructions.
‘default’
    Enable the default instructions, equivalent to -mrecip.
‘none’
    Disable all estimate instructions, equivalent to -mno-recip.
‘div’
    Enable the approximation for scalar division.
‘vec-div’
    Enable the approximation for vectorized division.
‘sqrt’
    Enable the approximation for scalar square root.
‘vec-sqrt’
    Enable the approximation for vectorized square root. 

因此,例如-mrecip=all,!SQRT允许所有的倒数近似,除了平方根。

请注意,英特尔的新Skylake设计进一步提高了FP分区性能,达到8-11c延迟,1/3c吞吐量。(或者对于256b向量每5c吞吐量一个,但对于vdivps的延迟相同)。他们拓宽了分隔线,因此AVX vdivps ymm现在与128b向量的延迟相同。

(SnB到Haswell以大约两倍的延迟/食谱吞吐量执行256bdiv和sqrt,所以他们显然只有128b宽的除法。)Skylake还对这两种操作进行了更多的流水线操作,因此大约有4个div操作可以在飞行中进行。SQRT也更快。

所以几年后,一旦Skylake广泛传播,只有当你需要多次除以相同的东西时,才值得做rcppsrcpps和几个fma可能具有稍高的吞吐量,但延迟更差。此外,vdivps只是一个单一的向上;因此,更多的执行资源将可用于在分割的同时发生的事情。

AVX512的初始实现将是什么样子还有待观察。假设rcpps和Newton-Raphson迭代的几个fma将是一个胜利,如果FP划分性能是一个瓶颈。如果上层吞吐量是一个瓶颈,并且在除数运行期间有大量其他工作要做,那么vdivps zmm可能仍然是好的(当然,除非重复使用相同的除数)。

浮点除法和浮点乘法有更多关于FP吞吐量和延迟的信息。

我在我的一个应用程序中试验了一个浮点数学密集型热路径,并发现了类似的东西。我通常不看编译器发出的指令,所以我有点惊讶,并深入研究了数学细节。

下面是gcc生成的指令集,由我用进位计算注释:

test1(float __vector): ; xmm0               = a
    rcpps   xmm1, xmm0 ; xmm1 = 1 / xmm0    = 1/a
    mulps   xmm0, xmm1 ; xmm0 = xmm0 * xmm1 = a * 1/a
    mulps   xmm0, xmm1 ; xmm0 = xmm0 * xmm1 = a * (1/a)^2
    addps   xmm1, xmm1 ; xmm1 = xmm1 + xmm1 = 2 * (1/a)
    subps   xmm1, xmm0 ; xmm1 = xmm1 - xmm0 = 2 * (1/a) - a * (1/a)^2
    movaps  xmm0, xmm1 ; xmm0 = xmm1        = 2 * (1/a) - a * (1/a)^2
    ret

这是怎么回事?为什么要浪费额外的4条指令来计算一个数学上等同于倒数的表达式呢?

嗯,rcpps指令只是一个近似的倒数。其他算术指令(mulps, addps, subps)精确到单精度。我们把r(x)写成近似倒数函数。最后变成了y = 2*r(a) - a*r(a)^2。如果我们用eps作为相对误差替换r(x) = (1 + eps) * (1/x),我们得到:
y = 2 * (1 + eps) * (1/a) - a * (1 + eps)^2 * (1/a)^2
  = (2 + 2*eps - (1 + eps)^2) * (1/a)
  = (2 + 2*eps - (1 + 2*eps + eps^2)) * (1/a)
  = (1 - eps^2) * (1/a)

rcpps的相对误差小于1.5 * 2^-12,所以eps <= 1.5 * 2^-12,所以:

eps^2 <= 2.25 * 2^-24
      <  1.5  * 2^-23
因此,通过执行这些额外的指令,我们从12位精度提高到23位精度。注意,单个精度浮点数有24位精度,所以我们几乎在这里得到了完整的精度。

那么,这只是一些神奇的指令序列,碰巧能让我们获得额外的精度吗?不完全是。它是基于牛顿的方法(我收集被称为牛顿-拉夫森的人与汇编工作很多)。

牛顿法是一种寻根法。给定某个函数f(x),它找到f(x) = 0的近似解,通过从近似解x_0开始并迭代改进它。牛顿迭代的表达式为:

x_n+1 = x_n - f(x_n) / f'(x_n)

在我们的例子中,我们可以将寻找a的倒数1/a重新表述为寻找函数f(x) = a*x - 1的根,并求导f'(x) = a。将其代入牛顿迭代方程,得到:

x_n+1 = x_n - (a*x_n - 1) / a

两个观察:

  1. 在这种情况下,牛顿迭代实际上给了我们一个精确的结果,而不仅仅是一个更好的近似值。这是有道理的,因为牛顿的方法是在x_n周围对f进行线性近似。在这种情况下,f是完全线性的,所以近似是完美的。然而…

  2. 计算牛顿迭代需要我们除以a,这是我们试图近似的精确计算。这就造成了一个循环问题。我们通过修改牛顿迭代来打破这个循环,使用我们的近似倒数x_n来除以a:

    x_n+1 =  x_n - (a*x_n - 1) * x_n
          ~= x_n - (a*x_n - 1) / a
    

这个迭代可以很好地工作,但从向量数学的角度来看它不是很好:它需要减去1。要在矢量数学中做到这一点,需要准备一个带有1序列的矢量寄存器。这需要一个额外的指令和一个额外的寄存器。

我们可以重写迭代来避免这种情况:

x_n+1 = x_n - (a*x_n - 1) * x_n
      = x_n - (a*x_n^2 - x_n)
      = 2*x_n - a*x_n^2

现在替换x_0 = r(a),我们从上面恢复表达式:

y = x_1 = 2*r(a) - a*r(a)^2

相关内容

  • 没有找到相关文章

最新更新