The standard C math library does not provide a function to compute the CDF of the standard normal distribution, normcdf()
. It does, however, offer closely related functions: the error function, erf()
and the complementary error function, erfc()
. The fastest way to compute the CDF is often via the error function, using the predefined constant M_SQRT1_2 to represent √½:
double normcdf (double a)
{
return 0.5 + 0.5 * erf (M_SQRT1_2 * a);
}
Clearly, this suffers from massive subtractive cancellation in the negative half-plane and is not suitable for the majority of applications. Since the cancellation issue is easily avoided by use of erfc()
, which however is typically of somewhat lower performance than erf()
, the most frequently recommended computation is:
double normcdf (double a)
{
return 0.5 * erfc (-M_SQRT1_2 * a);
}
Some testing shows that the maximum ulp error incurred in the negative half-plane is still rather large however. Using a double-precision implementation of erfc()
accurate to 0.51 ulps, one can observe errors of
up to 1705.44 ulps in normcdf()
. The issue here is that the computational error in the input to erfc()
is magnified by the exponential scaling that is inherent to erfc()
(See this answer
for an explanation of error magnification caused by exponentiation).
The following paper shows how one can achieve (nearly) correctly rounded, products when multiplying floating-point operands with arbitrary-precision constants such as √½:
Nicolas Brisebarre and Jean-Michel Muller, "Correctly rounded multiplication by arbitrary precision constants", IEEE Transactions on Computers, Vol. 57, No. 2, February 2008, pp. 165-174
The method advocated by the paper relies on the fused multiply-add operation, which is available on recent implementations of all common processor architectures, and is exposed in C via the standard math function fma()
. This leads to the following version:
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));
}
Tests show that this cuts the maximum error roughly in half compared to the previous version. Using the same highly-accurate erfc()
implementation as before, the maximum observed error is 842.71 ulps. This is still far away from the usual goal of providing basic mathematical functions with an error of at most a few ulps.
Is there an efficient method that allows for the accurate computation of normcdf()
, and that uses only functions available in the standard C math library?