【发布时间】:2025-10-18 08:45:01
【问题描述】:
标准 C 数学库不提供计算标准正态分布的 CDF 的函数,normcdf()。然而,它确实提供了密切相关的函数:误差函数erf() 和互补误差函数erfc()。计算 CDF 最快的方法通常是通过误差函数,使用预定义的常数 M_SQRT1_2 来表示 √½:
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,卷。 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 数学库中可用的函数?
【问题讨论】:
-
您是否使用
long double提供的精度高于double的系统?如果是这样,使用erfl()和erfcl()是否提供任何帮助? -
@JonathanLeffler 我目前使用的平台要么不支持
long double,要么将long double映射到double。否则,将long double映射到 80 位扩展精度、双倍精度或四倍精度,我的期望是基于erfcl的简单公式将提供精确到双精度的结果,但我没有现在证明这一点的方式。另一方面,即使假设通过erfl()进行的计算映射到完整的 IEEE-754 四倍精度,大量取消也会导致负半平面的结果不准确。
标签: c algorithm math floating-point