使用C标准数学库

时间:2016-06-17 23:10:38

标签: c algorithm math floating-point

标准C数学库不提供计算标准正态分布{1}的CDF的函数。但是,它确实提供了密切相关的函数:错误函数normcdf()和补充错误函数erf()。计算CDF的最快方法通常是通过误差函数,使用预定义的常量M_SQRT1_2来表示√½:

erfc()
显然,这在负半平面中遭受大量的减法消除,并且不适合大多数应用。由于使用double normcdf (double a) { return 0.5 + 0.5 * erf (M_SQRT1_2 * a); } 可以轻松避免取消问题,但erfc()的性能通常低于erf(),因此最常推荐的计算是:

double normcdf (double a) 
{
    return 0.5 * erfc (-M_SQRT1_2 * a);
}

一些测试显示,在负半平面中产生的最大ulp误差仍然相当大。使用精确到0.51 ulps的erfc()的双精度实现,可以观察到的错误 normcdf()中高达1705.44 ulps。这里的问题是erfc()输入中的计算误差被erfc()固有的指数缩放放大(见answer 用于解释由取幂引起的误差放大。)

下面的文章展示了当浮点操作数与任意精度常数相乘时如何实现(几乎)正确舍入的乘积,如√½:

Nicolas Brisebarre和Jean-Michel Muller,“正确舍入乘以任意精度常数”, IEEE Transactions on Computers ,Vol。 57,第2期,2008年2月,第165-174页

本文提倡的方法依赖于融合的乘法 - 加法运算,该运算可用于所有常见处理器架构的最新实现,并通过标准数学函数fma()在C中公开。这导致以下版本:

double normcdf (double a) 
{
    double SQRT_HALF_HI =  0x1.6a09e667f3bcd0p-01; //  7.0710678118654757e-01
    double SQRT_HALF_LO = -0x1.bdd3413b264560p-55; // -4.8336466567264567e-17

    return 0.5 * erfc (fma (-SQRT_HALF_HI, a, -SQRT_HALF_LO * a));
}

测试表明,与之前的版本相比,这将最大误差减少了一半左右。使用与以前相同的高度准确的erfc()实现,观察到的最大错误为842.71 ulps。这仍然远离提供基本数学函数的通常目标,误差最多为几个ulps。

是否有一种有效的方法可以准确计算normcdf(),并且只使用标准C数学库中可用的函数?

1 个答案:

答案 0 :(得分:2)

围绕问题中概述的方法的准确性限制的一种方法是使用双倍计算。这涉及以头/尾方式将-sqrt (0.5) * a计算为一对double变量hl。产品的高阶部分h将传递到erfc(),而低阶部分l则用于根据本地插入erfc()结果h处的互补误差函数的斜率。

erfc(x)的导数是-2 * exp(-x * x)/√π。但是,人们希望避免相当昂贵的exp(-x * x)计算。对于x>,known为{{3}}。 0,erfc(x)〜= 2 * exp(-x * x)/(√π*(x + sqrt(x * x + 4 /π))。因此,渐近地, erfc'(x)〜= -2 * x * erfc(x),接着是| l | «| h |,erfc(h + 1)〜= erfc(h) - 2 * h * l * erfc(h)。对后一项的否定很容易被归入l的计算中。一个人达到双精度的以下实现(使用IEEE-754 binary64):

double my_normcdf (double a)
{
    double h, l, r;
    const double SQRT_HALF_HI =  0x1.6a09e667f3bcd0p-01; //  7.0710678118654757e-01
    const double SQRT_HALF_LO = -0x1.bdd3413b264560p-55; // -4.8336466567264567e-17

    /* clamp input as normcdf(x) is either 0 or 1 asymptotically */
    if (fabs (a) > 38.625) a = (a < 0.0) ? -38.625 : 38.625;

    h = fma (-SQRT_HALF_HI, a, -SQRT_HALF_LO * a);
    l = fma (SQRT_HALF_LO, a, fma (SQRT_HALF_HI, a, h));
    r = erfc (h);
    if (h > 0.0) r = fma (2.0 * h * l, r, r);
    return 0.5 * r;
}

使用与之前相同的erfc()实现,观察到的最大错误为1.96 ulps。相应的单精度实现(使用IEEE-754 binary32)是:

float my_normcdff (float a)
{
    float h, l, r;
    const float SQRT_HALF_HI = 0x1.6a09e6p-01f; // 7.07106769e-1
    const float SQRT_HALF_LO = 0x1.9fcef4p-27f; // 1.21016175e-8

    /* clamp input as normcdf(x) is either 0 or 1 asymptotically */
    if (fabsf (a) > 14.171875f) a = (a < 0.0f) ? -14.171875f : 14.171875f;

    h = fmaf (-SQRT_HALF_HI, a, -SQRT_HALF_LO * a);
    l = fmaf (SQRT_HALF_LO, a, fmaf (SQRT_HALF_HI, a, h));
    r = erfcf (h);
    if (h > 0.0f) r = fmaf (2.0f * h * l, r, r);
    return 0.5f * r;
}