标准正态分布的概率密度函数定义为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上。
答案 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。