通常用erfcx
表示的(指数)缩放的互补误差函数在数学上定义为erfcx(x):= ex2 erfc(x)。它经常发生在物理以及化学中的扩散问题中。虽然某些数学环境(例如MATLAB和GNU Octave)提供了此功能,但C标准数学库中却不提供此功能,该库仅提供erf()
和erfc()
。
尽管可以直接基于数学定义来实现自己的erfcx()
,但这只能在有限的输入域上工作,因为在正半平面erfc()
中,中等幅度参数的下溢,而exp()
溢出(如this question中所述),例子。
为了与C一起使用,可以对Faadeeva package的响应中指出的那样,修改一些erfcx()
开源实现,例如this question中的一个。但是,这些实现通常不能为给定的浮点格式提供完整的准确性。例如,使用232个测试 vector 进行的测试显示,Faadeeva软件包提供的erfcx()
的最大误差为正半平面为8.41 ulps,负半平面为511.68 ulps。
准确实现的合理范围为4 ulps,对应于Intel's Vector Math库的LA概要文件中数学函数的精度范围,我发现这对于要求两者都具有良好准确性的非平凡数学函数实现是合理范围和良好的性能。
仅使用C标准数学库而无需外部库,如何精确实现erfcx()
和相应的单精度版本erfcxf()
?我们可以假定C的float
和double
类型映射到IEEE 754-2008 binary32
和binary64
浮点类型。可以假定对融合乘法加法运算(FMA)的硬件支持,因为此时所有主要处理器体系结构都支持该功能。
最佳答案
到目前为止,我发现的erfcx()
实现的最佳方法是基于以下论文:
M. M. Shepherd和J. G. Laframboise,“在0≤x (online)
本文提出了巧妙的变换,将缩放后的互补误差函数映射到一个紧迫的辅助函数,该函数适合于直接多项式逼近。为了提高性能,我尝试了转换的变体,但是所有这些都对准确性产生了负面影响。变换(x-K)/(x + K)中常数K的选择与核心近似的精度之间没有明显的关系。我凭经验确定了“最优”值,该值不同于本文中的值。
将核心近似值和中间结果的参数转换回erfcx
结果会导致额外的舍入误差。为了减轻它们对准确性的影响,我们需要应用补偿步骤,我在较早的question & answer regarding erfcf
中对此进行了详细介绍。 FMA的可用性大大简化了此任务。
生成的单精度代码如下所示:
/*
* Based on: M. M. Shepherd and J. G. Laframboise, "Chebyshev Approximation of
* (1+2x)exp(x^2)erfc x in 0 <= x < INF", Mathematics of Computation, Vol. 36,
* No. 153, January 1981, pp. 249-253.
*
*/
float my_erfcxf (float x)
{
float a, d, e, m, p, q, r, s, t;
a = fmaxf (x, 0.0f - x); // NaN-preserving absolute value computation
/* Compute q = (a-2)/(a+2) accurately. [0,INF) -> [-1,1] */
m = a - 2.0f;
p = a + 2.0f;
#if FAST_RCP_SSE
r = fast_recipf_sse (p);
#else
r = 1.0f / p;
#endif
q = m * r;
t = fmaf (q + 1.0f, -2.0f, a);
e = fmaf (q, -a, t);
q = fmaf (r, e, q);
/* Approximate (1+2*a)*exp(a*a)*erfc(a) as p(q)+1 for q in [-1,1] */
p = 0x1.f10000p-15f; // 5.92470169e-5
p = fmaf (p, q, 0x1.521cc6p-13f); // 1.61224554e-4
p = fmaf (p, q, -0x1.6b4ffep-12f); // -3.46481771e-4
p = fmaf (p, q, -0x1.6e2a7cp-10f); // -1.39681227e-3
p = fmaf (p, q, 0x1.3c1d7ep-10f); // 1.20588380e-3
p = fmaf (p, q, 0x1.1cc236p-07f); // 8.69014394e-3
p = fmaf (p, q, -0x1.069940p-07f); // -8.01387429e-3
p = fmaf (p, q, -0x1.bc1b6cp-05f); // -5.42122945e-2
p = fmaf (p, q, 0x1.4ff8acp-03f); // 1.64048523e-1
p = fmaf (p, q, -0x1.54081ap-03f); // -1.66031078e-1
p = fmaf (p, q, -0x1.7bf5cep-04f); // -9.27637145e-2
p = fmaf (p, q, 0x1.1ba03ap-02f); // 2.76978403e-1
/* Divide (1+p) by (1+2*a) ==> exp(a*a)*erfc(a) */
d = a + 0.5f;
#if FAST_RCP_SSE
r = fast_recipf_sse (d);
#else
r = 1.0f / d;
#endif
r = r * 0.5f;
q = fmaf (p, r, r); // q = (p+1)/(1+2*a)
t = q + q;
e = (p - q) + fmaf (t, -a, 1.0f); // residual: (p+1)-q*(1+2*a)
r = fmaf (e, r, q);
if (a > 0x1.fffffep127f) r = 0.0f; // 3.40282347e+38 // handle INF argument
/* Handle negative arguments: erfcx(x) = 2*exp(x*x) - erfcx(|x|) */
if (x < 0.0f) {
s = x * x;
d = fmaf (x, x, -s);
e = expf (s);
r = e - r;
r = fmaf (e, d + d, r);
r = r + e;
if (e > 0x1.fffffep127f) r = e; // 3.40282347e+38 // avoid creating NaN
}
return r;
}
在负半平面中此实现的最大误差将取决于标准数学库
expf()
的实现的准确性。在详尽的测试中,使用Intel编译器13.1.3.198版并使用/fp:strict
进行编译,我观察到正半平面的最大误差为2.00450 ulps,负半平面的最大误差为2.38412 ulps。我目前可以告诉的最好的情况是,对expf()
的全面实现将导致最大错误小于2.5 ulps。请注意,虽然代码包含两个除法,它们可能是缓慢的运算,但它们以倒数的特殊形式出现,因此适合在许多平台上使用快速倒数近似。根据实验,只要忠实地对等倒数进行四舍五入,对
erfcxf()
准确性的影响就可以忽略不计。即使是稍大的错误,例如在快速SSE版本中(最大错误/* Fast reciprocal approximation. HW approximation plus Newton iteration */
float fast_recipf_sse (float a)
{
__m128 t;
float e, r;
t = _mm_set_ss (a);
t = _mm_rcp_ss (t);
_mm_store_ss (&r, t);
e = fmaf (0.0f - a, r, 1.0f);
r = fmaf (e, r, r);
return r;
}
double 版本
erfcx()
在结构上与单精度版本erfcxf()
相同,但需要包含更多项的极小多项式逼近。在优化核心近似时,这会带来挑战,因为当搜索空间很大时,许多启发式方法都会失效。以下系数代表了我迄今为止最好的解决方案,并且肯定还有改进的余地。使用Intel编译器和/fp:strict
构建,并使用232个随机测试 vector ,在正半平面中观察到的最大误差为2.83788 ulps,在负半平面中观察到的最大误差为2.77856 ulps。double my_erfcx (double x)
{
double a, d, e, m, p, q, r, s, t;
a = fmax (x, 0.0 - x); // NaN preserving absolute value computation
/* Compute q = (a-4)/(a+4) accurately. [0,INF) -> [-1,1] */
m = a - 4.0;
p = a + 4.0;
r = 1.0 / p;
q = m * r;
t = fma (q + 1.0, -4.0, a);
e = fma (q, -a, t);
q = fma (r, e, q);
/* Approximate (1+2*a)*exp(a*a)*erfc(a) as p(q)+1 for q in [-1,1] */
p = 0x1.edcad78fc8044p-31; // 8.9820305531190140e-10
p = fma (p, q, 0x1.b1548f14735d1p-30); // 1.5764464777959401e-09
p = fma (p, q, -0x1.a1ad2e6c4a7a8p-27); // -1.2155985739342269e-08
p = fma (p, q, -0x1.1985b48f08574p-26); // -1.6386753783877791e-08
p = fma (p, q, 0x1.c6a8093ac4f83p-24); // 1.0585794011876720e-07
p = fma (p, q, 0x1.31c2b2b44b731p-24); // 7.1190423171700940e-08
p = fma (p, q, -0x1.b87373facb29fp-21); // -8.2040389712752056e-07
p = fma (p, q, 0x1.3fef1358803b7p-22); // 2.9796165315625938e-07
p = fma (p, q, 0x1.7eec072bb0be3p-18); // 5.7059822144459833e-06
p = fma (p, q, -0x1.78a680a741c4ap-17); // -1.1225056665965572e-05
p = fma (p, q, -0x1.9951f39295cf4p-16); // -2.4397380523258482e-05
p = fma (p, q, 0x1.3be1255ce180bp-13); // 1.5062307184282616e-04
p = fma (p, q, -0x1.a1df71176b791p-13); // -1.9925728768782324e-04
p = fma (p, q, -0x1.8d4aaa0099bc8p-11); // -7.5777369791018515e-04
p = fma (p, q, 0x1.49c673066c831p-8); // 5.0319701025945277e-03
p = fma (p, q, -0x1.0962386ea02b7p-6); // -1.6197733983519948e-02
p = fma (p, q, 0x1.3079edf465cc3p-5); // 3.7167515521269866e-02
p = fma (p, q, -0x1.0fb06dfedc4ccp-4); // -6.6330365820039094e-02
p = fma (p, q, 0x1.7fee004e266dfp-4); // 9.3732834999538536e-02
p = fma (p, q, -0x1.9ddb23c3e14d2p-4); // -1.0103906603588378e-01
p = fma (p, q, 0x1.16ecefcfa4865p-4); // 6.8097054254651804e-02
p = fma (p, q, 0x1.f7f5df66fc349p-7); // 1.5379652102610957e-02
p = fma (p, q, -0x1.1df1ad154a27fp-3); // -1.3962111684056208e-01
p = fma (p, q, 0x1.dd2c8b74febf6p-3); // 2.3299511862555250e-01
/* Divide (1+p) by (1+2*a) ==> exp(a*a)*erfc(a) */
d = a + 0.5;
r = 1.0 / d;
r = r * 0.5;
q = fma (p, r, r); // q = (p+1)/(1+2*a)
t = q + q;
e = (p - q) + fma (t, -a, 1.0); // residual: (p+1)-q*(1+2*a)
r = fma (e, r, q);
/* Handle argument of infinity */
if (a > 0x1.fffffffffffffp1023) r = 0.0;
/* Handle negative arguments: erfcx(x) = 2*exp(x*x) - erfcx(|x|) */
if (x < 0.0) {
s = x * x;
d = fma (x, x, -s);
e = exp (s);
r = e - r;
r = fma (e, d + d, r);
r = r + e;
if (e > 0x1.fffffffffffffp1023) r = e; // avoid creating NaN
}
return r;
}
关于c - 比例补偿误差函数erfcx()的精确计算,我们在Stack Overflow上找到一个类似的问题:https://stackoverflow.com/questions/39777360/