使用C标准数学库精确计算标准正态分布的PDF

时间:2016-09-15 22:51:30

标签: c math floating-point floating-accuracy

标准正态分布的概率密度函数定义为e -x 2 / 2 /√(2π)。这可以以简单的方式呈现为C代码。示例单精度实现可能是:

float my_normpdff (float a)
{
    return 0x1.988454p-2f * my_expf (-0.5f * a * a); /* 1/sqrt(2*pi) */
}

虽然此代码没有过早下溢,但存在准确性问题,因为计算 2 / 2时产生的错误会被随后的取幂放大。人们可以通过针对更高精度参考的测试轻松证明这一点。确切的错误会因所使用的exp()expf()实施的准确性而有所不同;对于忠实的舍入求幂函数,人们通常会观察到IEEE-754 binary32单精度约为2 6 ulps的最大误差,对于IEEE-约为2 9 ulps 754 binary64双精度。

如何以合理有效的方式解决准确性问题?一个简单的方法是采用更高精度的中间计算,例如对double实现使用float计算。但是,如果不能轻易获得更高精度的浮点运算,这种方法对double实现不起作用,如果float算术比double算术要贵,float实现可能效率低。 {{1}}计算,例如在许多GPU上。

1 个答案:

答案 0 :(得分:4)

通过使用有限数量的双float或双 - double计算,可以有效,高效地解决问题中提出的准确性问题,并通过使用融合乘法来促进-add(FMA)操作。

此操作自C99以后通过标准数学函数fmaf(a,b,c)fma(a,b,c)可用,它们计算a * b + c,而无需舍入中间产品 。虽然这些功能直接映射到几乎所有现代处理器上的快速硬件操作,但它们可能会在较旧的平台上使用仿真代码,在这种情况下它们可能会非常慢。

这允许使用两次操作计算产品的正常精度的两倍,从而产生一对头:尾对的原生精度数字:

prod_hi = a * b            // head
prod_lo = FMA (a, b, -hi)  // tail

结果的高阶位可以传递给取幂,而低阶位用于通过线性插值提高结果的准确性,利用e x的事实是它自己的衍生物:

e = exp (prod_hi) + exp (prod_hi) * prod_lo  // exp (a*b)

这使我们能够消除天真实现的大部分错误。另一个次要的计算误差源是用于表示常数1 /√(2π)的有限精度。这可以通过使用head:tail表示来提高常量精度的两倍,并计算:

r = FMA (const_hi, x, const_lo * x)  // const * x

下面的文章指出,这种技术甚至可以导致一些任意精度常数的正确舍入:

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

结合这两种技术,并处理涉及NaN的一些极端情况,我们得出基于IEEE-754 float的以下binary32实现:

float my_normpdff (float a)
{
    const float RCP_SQRT_2PI_HI =  0x1.988454p-02f; /* 1/sqrt(2*pi), msbs */
    const float RCP_SQRT_2PI_LO = -0x1.857936p-27f; /* 1/sqrt(2*pi), lsbs */
    float ah, sh, sl, ea;

    ah = -0.5f * a;
    sh = a * ah;
    sl = fmaf (a, ah, 0.5f * a * a); /* don't flip "sign bit" of NaN argument */
    ea = expf (sh);
    if (ea != 0.0f) ea = fmaf (sl, ea, ea); /* avoid creation of NaN */
    return fmaf (RCP_SQRT_2PI_HI, ea, RCP_SQRT_2PI_LO * ea);
}

基于IEEE-754 double的相应binary64实现看起来几乎相同,只是使用了不同的常量值:

double my_normpdf (double a)
{
    const double RCP_SQRT_2PI_HI =  0x1.9884533d436510p-02; /* 1/sqrt(2*pi), msbs */
    const double RCP_SQRT_2PI_LO = -0x1.cbc0d30ebfd150p-56; /* 1/sqrt(2*pi), lsbs */
    double ah, sh, sl, ea;

    ah = -0.5 * a;
    sh = a * ah;
    sl = fma (a, ah, 0.5 * a * a); /* don't flip "sign bit" of NaN argument */
    ea = exp (sh);
    if (ea != 0.0) ea = fma (sl, ea, ea); /* avoid creation of NaN */
    return fma (RCP_SQRT_2PI_HI, ea, RCP_SQRT_2PI_LO * ea);
}

这些实现的准确性分别取决于标准数学函数expf()exp()的准确性。在C数学库提供忠实圆形版本的情况下,上述两种实现中任何一种的最大误差通常小于2.5 ulps。