互补错误函数erfcf()的可矢量化实现

时间:2016-03-13 04:59:27

标签: c algorithm math floating-point

互补误差函数 erfc 是与标准正态分布密切相关的特殊函数。它经常用于统计学和自然科学(例如扩散问题),其中需要考虑这种分布的“尾部”,因此使用误差函数 erf 是不合适的。 / p>

补充误差函数在ISO C99标准数学库中可用作函数erfcferfcerfcl;这些随后也被采用到ISO C ++中。因此,源代码可以很容易地在该库的开源实现中找到,例如在glibc中。

然而,许多现有的实现本质上是标量的,而现代处理器硬件是面向SIMD的(明确地,如在x86 CPU中,或隐式地,如在GPU中)。出于性能原因,因此非常需要可矢量化的实现。这意味着需要避免分支,除非作为选择分配的一部分。同样,未指示广泛使用表,因为并行查找通常效率低下。

如何构建单精度函数erfcf()的高效矢量化实现?在ulp中测量的准确度应与glibc的标量实现大致相同,其最大误差为3.12575 ulps(通过详尽测试确定)。可以假设融合乘法加法(FMA)的可用性,因为此时所有主要处理器架构(CPU和GPU)都提供它。虽然可以忽略浮点状态标志和errno的处理,但应根据ISO C的IEEE 754绑定处理非正规数,无穷大和NaN。

1 个答案:

答案 0 :(得分:3)

在研究了各种方法后,最合适的方法是下面提出的算法:

微米。 M. Shepherd和JG Laframboise," Chebyshev逼近(1 + 2 x )exp( x 2 )erfc x in0≤ x < ∞&#34。 计算数学,第36卷,第153期,1981年1月,第249-253页(online copy

本文的基本思想是创建一个近似(1 + 2 x )exp( x 2 )erfc(< em> x ),我们可以通过简单地除以(1 + 2 x )和erfc(x)来计算erfcx( x )乘以exp( - x 2 )。功能的紧密范围,功能值大致在[1,1.3],其一般&#34;平坦度&#34;非常适合多项式逼近。通过缩小近似区间进一步改善了这种方法的数值特性:原始参数 x q =( x - K)/转换( x + K),其中K是适当选择的常数,然后计算 p q ),其中 p 是一个多项式。

由于erfc - x = 2 - erfc x ,我们只需要考虑映射到区间[-1,1的区间[0,∞]通过这种转变。对于IEEE-754单精度,{em> x &gt; erfcf()消失(变为零) 10.0546875,所以只需要考虑 x ∈[0,10.0546875]。什么是&#34;最佳&#39;这个范围的K值是多少?我知道没有可以提供答案的数学分析,本文根据实验建议K = 3.75。

可以容易地确定,对于单精度计算,9度的极小极大多项式近似对于该一般附近的K的各种值是足够的。使用Remez算法系统地生成这样的近似,其中K以1/16的步长在1.5和4之间变化,对于K = {2,2.625,3.3125}观察到最低近似误差。其中,K = 2是最有利的选择,因为它有助于非常准确地计算( x - K)/( x + K),如图所示this question

值K = 2且 x 的输入域表明有必要使用来自my answer的变体4,但是曾经可以通过实验证明较便宜的变体5实现了这里的精度相同,这可能是由于 q 的近似函数的非常浅的斜率。 -0.5,这会导致参数 q 中的任何错误减少大约十倍。

由于erfc()的计算除了初始近似之外还需要后处理步骤,因此很明显,这两种计算的精度必须很高才能获得足够精确的最终结果。必须使用纠错技术。

观察到(1 + 2 x )exp( x 2 )erfc( x 2 )的多项式近似中最显着的系数(< em> x )的形式为(1 + s),其中 s &lt; 0.5。这意味着我们可以通过分割1来更准确地表示前导系数,并且仅在多项式中使用 s 。因此,不是计算多项式p(q),而是乘以倒数 r = 1 /(1 + 2 x ),它在数学上是等价的,但在数值上有利于计算核心近似为p( q )+ 1,并使用 p 计算fma (p, r, r)

通过从倒数 r 计算初始商 q 可以提高除法的准确性,计算残差 e = p +1 - q *(1 + 2 x ),然后使用 e 来申请修正 q = q +( e * r ),再次使用FMA。

指数具有误差放大属性,因此必须仔细执行 e - x 2 的计算。 FMA的可用性通常允许计算 - x 2 为双 - float s :s 。 e x 是它自己的导数,因此可以计算e s high :s low 为e s high + e s high * s low 。此计算可与前一中间结果 r 的乘法相结合,以产生 r = r * e s high + r * e s high * s low 。通过使用FMA,可以确保尽可能准确地计算最重要的术语 r * e s high

将上述步骤与一些简单的选择结合起来处理异常情况和否定参数,可以得到以下C代码:

float my_expf (float);

/*  
 * Based on: M. M. Shepherd and J. G. Laframboise, "Chebyshev Approximation of 
 * (1+2x)exp(x^2)erfc x in 0 <= x < INF", Mathematics of Computation, Vol. 36,
 * No. 153, January 1981, pp. 249-253.  
 */  
float my_erfcf (float x)
{
    float a, d, e, m, p, q, r, s, t;

    a = fabsf (x); 

    /* Compute q = (a-2)/(a+2) accurately. [0, 10.0546875] -> [-1, 0.66818] */
    m = a - 2.0f;
    p = a + 2.0f;
    r = 1.0f / p;
    q = m * r;
    t = fmaf (q + 1.0f, -2.0f, a); 
    e = fmaf (q, -a, t); 
    q = fmaf (r, e, q); 

    /* Approximate (1+2*a)*exp(a*a)*erfc(a) as p(q)+1 for q in [-1, 0.66818] */
    p =             -0x1.a48024p-12f;  // -4.01020574e-4
    p = fmaf (p, q, -0x1.42a172p-10f); // -1.23073824e-3
    p = fmaf (p, q,  0x1.585784p-10f); //  1.31355994e-3
    p = fmaf (p, q,  0x1.1ade24p-07f); //  8.63243826e-3
    p = fmaf (p, q, -0x1.081b72p-07f); // -8.05991236e-3
    p = fmaf (p, q, -0x1.bc0b94p-05f); // -5.42047396e-2
    p = fmaf (p, q,  0x1.4ffc40p-03f); //  1.64055347e-1
    p = fmaf (p, q, -0x1.540840p-03f); // -1.66031361e-1
    p = fmaf (p, q, -0x1.7bf612p-04f); // -9.27639678e-2
    p = fmaf (p, q,  0x1.1ba03ap-02f); //  2.76978403e-1

    /* Divide (1+p) by (1+2*a) ==> exp(a*a)*erfc(a) */
    t = a + a;
    d = t + 1.0f;
    r = 1.0f / d;
    q = fmaf (p, r, r); // q = (p+1)/(1+2*a)
    e = (p - q) + fmaf (q, -t, 1.0f); // (p+1) - q*(1+2*a)
    r = fmaf (e, r, q);

    /* Multiply by exp(-a*a) ==> erfc(a) */
    s = a * a; 
    e = my_expf (-s);  
    t = fmaf (a, -a, s);
    r = fmaf (r, e, r * e * t);

    /* Handle NaN arguments to erfc() */
    if (!(a <= 0x1.fffffep127f)) r = x + x;

    /* Clamp result for large arguments */
    if (a > 10.0546875f) r = 0.0f;

    /* Handle negative arguments to erfc() */
    if (x < 0.0f) r = 2.0f - r; 

    return r;
}

/* Compute exponential base e. Maximum ulp error = 0.87161 */
float my_expf (float a)
{
    float c, f, r;
    int i;

    // exp(a) = exp(i + f); i = rint (a / log(2)) 
    c = 0x1.800000p+23f; // 1.25829120e+7
    r = fmaf (0x1.715476p+0f, a, c) - c; // 1.44269502e+0
    f = fmaf (r, -0x1.62e400p-01f, a); // -6.93145752e-1 // log_2_hi 
    f = fmaf (r, -0x1.7f7d1cp-20f, f); // -1.42860677e-6 // log_2_lo
    i = (int)r;
    // approximate r = exp(f) on interval [-log(2)/2,+log(2)/2]
    r =             0x1.6a98dap-10f;  // 1.38319808e-3
    r = fmaf (r, f, 0x1.1272cap-07f); // 8.37550033e-3
    r = fmaf (r, f, 0x1.555a20p-05f); // 4.16689515e-2
    r = fmaf (r, f, 0x1.55542ep-03f); // 1.66664466e-1
    r = fmaf (r, f, 0x1.fffff6p-02f); // 4.99999851e-1
    r = fmaf (r, f, 0x1.000000p+00f); // 1.00000000e+0
    r = fmaf (r, f, 0x1.000000p+00f); // 1.00000000e+0
    // exp(a) = 2**i * exp(f);
    r = ldexpf (r, i);
    // handle special cases
    if (!(fabsf (a) < 104.0f)) {
        r = a + a; // handle NaNs
        if (a < 0.0f) r = 0.0f;
        if (a > 0.0f) r = 1e38f * 1e38f; // + INF
    }
    return r;
}

我在上面的代码中使用了我自己的expf()实现来隔离我的工作与不同计算平台上expf()实现的差异。但是任何最大误差接近0.5 ulp的expf()实现都应该可以正常工作。如上所示,即使用my_expf()时,my_erfcf()的最大误差为2.65712 ulps。提供了可矢量化expf()的可用性,上面的代码应该没有问题地进行矢量化。我用英特尔编译器13.1.3.198快速检查了一下。我在循环中调用了my_erfcf(),添加了#include <mathimf.h>,将my_expf()的调用替换为调用expf(),然后使用这些命令行开关编译:

/Qstd=c99 /O3 /QxCORE-AVX2 /fp:precise /Qfma /Qimf-precision:high:expf /Qvec_report=2

英特尔编译器报告该循环已经过矢量化,我通过检查反汇编的二进制代码对其进行了双重检查。

由于my_erfcf()仅使用倒数而不是完全除法,因此只要它们提供几乎正确的舍入结果,就可以使用快速倒数实现。对于在硬件中提供快速单精度倒数近似的处理器,可以通过将其与具有立方收敛的Halley迭代耦合来轻松实现。 x86处理器的这种方法的一个(标量)示例是:

/* Compute 1.0f / a almost correctly rounded. Halley iteration with cubic convergence */
float fast_recipf (float a)
{
    __m128 t;
    float e, r;
    t = _mm_set_ss (a);
    t = _mm_rcp_ss (t);
    _mm_store_ss (&r, t);
    e = fmaf (r, -a, 1.0f);
    e = fmaf (e, e, e);
    r = fmaf (e, r, r);
    return r;
}