C语言 5位精度互补误差函数的快速实现

t1qtbnec  于 2024-01-06  发布在  其他
关注(0)|答案(2)|浏览(183)

[虽然这是一个自我回答的问题,但我很乐意投票并接受任何替代答案,无论是在相同的计算工作量下提供上级准确度,还是在保持相同准确度的情况下减少计算工作量。]
我以前有demonstrated如何计算互补误差函数erfcf(),其最大误差小于3个ulps。这可以作为其他函数的构建块,例如标准正态分布的CDF Φ(x)= 1/2 erfc(-λ 1/2 x)或高斯Q函数,Q(x)= 1-Φ(x)= 1/2 erfc(λ 1/2 x)。然而,对于某些使用情况,不需要完全精确到单精度的计算,而erfc()评估对总运行时间的贡献不可忽略。
文献提供了对互补误差函数的各种低精度近似,但它们要么限于完整输入域的子集,要么针对绝对误差进行了优化,要么计算过于复杂,例如,需要多次调用超越函数。如何才能实现erfcf()具有高性能和大约5位十进制数的相对精度 * 在整个输入域中 *?

6yt4nkrj

6yt4nkrj1#

以下假设平台符合IEEE-754(2008)浮点标准,在该平台上float被Map到IEEE-754 binary32,并且在32位整数和float之间使用相同的字节序。(如果需要,通过设置适当的命令行开关)保留IEEE-754语义。我使用的是带有开关-march=skylake-avx152 -O3 -fp-model=precise的Intel C/C++编译器。
由于互补误差函数关于(0,1)对称,因此可以关注正半平面中的函数输入。这里函数大致像exp(-x2)一样衰减,并且对于x > 10.5的参数,float计算下溢到零。如果将erfc(x)/ exp(-x2)绘制在[0,10.5]形状表明,用多项式近似有点困难,但应该很容易用有理函数近似,即,两个多项式的比率。一些初始实验表明,两个多项式的次数各为3应该足以达到五位数的精度。
虽然有许多工具可以生成多项式近似,但不幸的是,有理近似并非如此。我使用Remez算法的修改来生成初始极大极小近似R(x)= P(x)/Q(x)到erfc(x)/ exp(-x2),但必须进行相当广泛的启发式搜索,以获得一个近似值,该近似值提供 * 接近 * 误差峰值的等振荡,实现了10-5的相对误差,其余的差异对于我的需要来说可以忽略不计。
通过计算erfc(x)= exp(-x2)R(x),所获得的精度显然取决于平台expf()实现的精度。此功能的忠实全面的实现(最大误差<= 1ulp)是常见的。虽然英特尔编译器附带了一个高度精确的数学库,可以提供近乎正确的四舍五入实现(最大误差非常接近0.5 ulps),我也尝试了我自己的忠实四舍五入的替代my_expf(),误差更大,只观察到对fast_erfcf()精度的影响非常小。

  1. #include <stdlib.h>
  2. #include <stdio.h>
  3. #include <stdint.h>
  4. #include <string.h>
  5. #include <math.h>
  6. #define USE_FMA (1)
  7. #define USE_BUILTIN_EXP (0)
  8. #if !USE_BUILTIN_EXP
  9. float my_expf (float a);
  10. #endif // USE_BUILTIN_EXP
  11. /* Fast computation of the complementary error function. For argument x > 0
  12. erfc(x) = exp(-x*x) * P(x) / Q(x), where P(x) and Q(x) are polynomials.
  13. If expf() is faithfully rounded, the following error bounds should hold:
  14. Maximum relative error < 1.065e-5, maximum absolute error < 9.50e-6, and
  15. maximum ulp error < 176.5
  16. */
  17. float fast_erfcf (float x)
  18. {
  19. float a, c, e, p, q, r, s;
  20. a = fabsf (x);
  21. c = fminf (a, 10.5f);
  22. s = -c * c;
  23. #if USE_BUILTIN_EXP
  24. e = expf (s);
  25. #else // USE_BUILTIN_EXP
  26. e = my_expf (s);
  27. #endif // USE_BUILTIN_EXP
  28. #if USE_FMA
  29. q = 3.82346243e-1f; // 0x1.8785c6p-2
  30. p = -4.38094139e-5f; // -0x1.6f8000p-15
  31. q = fmaf (q, c, 1.30382288e+0f); // 0x1.4dc756p+0
  32. p = fmaf (p, c, 2.16852024e-1f); // 0x1.bc1ceap-3
  33. q = fmaf (q, c, 1.85278833e+0f); // 0x1.da5056p+0
  34. p = fmaf (p, c, 7.23953605e-1f); // 0x1.72aa0cp-1
  35. q = fmaf (q, c, 9.99991655e-1f); // 0x1.fffee8p-1
  36. p = fmaf (p, c, 1.00000000e+0f); // 0x1.000000p+0
  37. #else // USE_FMA
  38. q = 3.82346272e-1f; // 0x1.8785c8p-2f
  39. p = -4.38243151e-5f; // -0x1.6fa000p-15
  40. q = q * c + 1.30382371e+0f; // 0x1.4dc764p+0
  41. p = p * c + 2.16852218e-1f; // 0x1.bc1d04p-3
  42. q = q * c + 1.85278797e+0f; // 0x1.da5050p+0
  43. p = p * c + 7.23953605e-1f; // 0x1.72aa0cp-1
  44. q = q * c + 9.99991596e-1f; // 0x1.fffee6p-1
  45. p = p * c + 1.00000000e+0f; // 0x1.000000p+0
  46. #endif // USE_FMA
  47. r = e / q;
  48. r = r * p;
  49. if (x < 0.0f) r = 2.0f - r;
  50. if (isnan(x)) r = x + x;
  51. return r;
  52. }
  53. float uint32_as_float (uint32_t a)
  54. {
  55. float r;
  56. memcpy (&r, &a, sizeof r);
  57. return r;
  58. }
  59. /* Exponential function. Maximum error 0.86565 ulps */
  60. float my_expf (float a)
  61. {
  62. float f, r, j, s, t;
  63. int i;
  64. unsigned int ia;
  65. // exp(a) = 2**i * exp(f); i = rintf (a / log(2))
  66. j = fmaf (1.442695f, a, 12582912.f); // 0x1.715476p0 // log2(e)
  67. j = j - 12582912.f; // 0x1.8p23 // 2**23+2**22
  68. f = fmaf (j, -6.93145752e-1f, a); // -0x1.62e400p-1 // log_2_hi
  69. f = fmaf (j, -1.42860677e-6f, f); // -0x1.7f7d1cp-20 // log_2_lo
  70. i = (int)j;
  71. // approximate r = exp(f) on interval [-log(2)/2, +log(2)/2]
  72. r = 1.37805939e-3f; // 0x1.694000p-10
  73. r = fmaf (r, f, 8.37312452e-3f); // 0x1.125edcp-7
  74. r = fmaf (r, f, 4.16695364e-2f); // 0x1.555b5ap-5
  75. r = fmaf (r, f, 1.66664720e-1f); // 0x1.555450p-3
  76. r = fmaf (r, f, 4.99999851e-1f); // 0x1.fffff6p-2
  77. r = fmaf (r, f, 1.00000000e+0f); // 0x1.000000p+0
  78. r = fmaf (r, f, 1.00000000e+0f); // 0x1.000000p+0
  79. // exp(a) = 2**i * r
  80. ia = (i > 0) ? 0 : 0x83000000;
  81. s = uint32_as_float (0x7f000000 + ia);
  82. t = uint32_as_float ((i << 23) - ia);
  83. r = r * s;
  84. r = r * t;
  85. // handle special cases: severe overflow / underflow
  86. if (fabsf (a) >= 104.0f) r = (a < 0) ? 0.0f : INFINITY;
  87. return r;
  88. }
  89. uint32_t float_as_uint32 (float a)
  90. {
  91. uint32_t r;
  92. memcpy (&r, &a, sizeof r);
  93. return r;
  94. }
  95. uint64_t double_as_uint64 (double a)
  96. {
  97. uint64_t r;
  98. memcpy (&r, &a, sizeof r);
  99. return r;
  100. }
  101. double floatUlpErr (float res, double ref)
  102. {
  103. uint64_t refi, i, j, err;
  104. int expoRef;
  105. /* ulp error cannot be computed if either operand is NaN, infinity, zero */
  106. if (isnan (res) || isnan (ref) || isinf(res) || isinf (ref) ||
  107. (res == 0.0f) || (ref == 0.0)) {
  108. return 0.0;
  109. }
  110. i = ((int64_t)float_as_uint32 (res)) << 32;
  111. expoRef = (int)(((double_as_uint64 (ref) >> 52) & 0x7ff) - 1023);
  112. refi = double_as_uint64 (ref);
  113. if (expoRef >= 129) {
  114. j = (refi & 0x8000000000000000ULL) | 0x7fffffffffffffffULL;
  115. } else if (expoRef < -126) {
  116. j = ((refi << 11) | 0x8000000000000000ULL) >> 8;
  117. j = j >> (-(expoRef + 126));
  118. j = j | (refi & 0x8000000000000000ULL);
  119. } else {
  120. j = ((refi << 11) & 0x7fffffffffffffffULL) >> 8;
  121. j = j | ((uint64_t)(expoRef + 127) << 55);
  122. j = j | (refi & 0x8000000000000000ULL);
  123. }
  124. err = (i < j) ? (j - i) : (i - j);
  125. return err / 4294967296.0;
  126. }
  127. int main (void)
  128. {
  129. uint32_t argi, resi, refi, diff;
  130. float arg, res, reff, abserrloc = NAN, relerrloc = NAN, ulperrloc = NAN;
  131. double ref, relerr, abserr, ulperr;
  132. double maxabserr = 0, maxrelerr = 0, maxulperr = 0;
  133. argi = 0;
  134. do {
  135. arg = uint32_as_float (argi);
  136. ref = erfc ((double)arg);
  137. res = fast_erfcf (arg);
  138. reff = (float)ref;
  139. resi = float_as_uint32 (res);
  140. refi = float_as_uint32 (reff);
  141. ulperr = floatUlpErr (res, ref);
  142. if (ulperr > maxulperr) {
  143. ulperrloc = arg;
  144. maxulperr = ulperr;
  145. }
  146. abserr = fabs (res - ref);
  147. if (abserr > maxabserr) {
  148. abserrloc = arg;
  149. maxabserr = abserr;
  150. }
  151. if (fabs (ref) >= 0x1.0p-126) {
  152. relerr = fabs ((res - ref) / ref);
  153. if (relerr > maxrelerr) {
  154. relerrloc = arg;
  155. maxrelerr = relerr;
  156. }
  157. }
  158. diff = (resi > refi) ? (resi - refi) : (refi - resi);
  159. if (diff > 200) {
  160. printf ("diff=%u @ %15.8e : res=% 15.8e ref=% 15.8e\n",
  161. diff, arg, res, ref);
  162. return EXIT_FAILURE;
  163. }
  164. argi++;
  165. } while (argi);
  166. printf ("max rel err = %.6e @ % 15.8e\n"
  167. "max abs err = %.6e @ % 15.8e\n"
  168. "max ulp err = %.6e @ % 15.8e\n",
  169. maxrelerr, relerrloc,
  170. maxabserr, abserrloc,
  171. maxulperr, ulperrloc);
  172. return EXIT_SUCCESS;
  173. }

字符串

展开查看全部
exdqitrt

exdqitrt2#

OP提供了用于评估性能测试的范围的注解:1 ULP步骤中的0.0至10.5。
由于所有float [-1.6e-08...+1.6e-08]中约有40%返回值约为1.0,代码可以使用以下内容。对我来说,整体性能至少 * 翻了一番 *。

  1. float fast_erfcf(float x) {
  2. if (fabsf(x) <= 1.60e-8) { // or maybe a bit more.
  3. return 1.0;
  4. }
  5. ...
  6. }

字符串
由于erfc(x)float的大范围可以返回1.0,因此我建议进行这种预先测试。
优点包括:

  • 对于所有float的40%来说,速度要快得多。
  • erfc(x_near_0)预期返回1.0。OP的fast_erfcf(0.0)返回1.00000834。考虑到可以容忍的大ULP,可以接受。1.0仍然很好。
  • OP的fast_erfcf(small negative value)返回0.999991536。可以接受,因为可以容忍较大的ULP,但在可能的情况下,希望有erfc(x) >= erfc(next x towards + infinity)

此外:建议在[0.1.... 10.0]范围内对最感兴趣的值进行性能测试。
这个建议对OP没有多大帮助,所以把它移到这个答案的底部。
与其用my_expf(float a)执行 ex,不如用my_exp2f(float a)执行 2x(这会影响有理函数的常数)。
j = fmaf(1.442695f, a, 12582912.f);被删除。
使用base 2比base e 执行取幂稍微容易一些。
我估计代码会快几个百分点(~3%),而ULP结果不会有太大变化。
确实,这是一个微优化,但它实际上是免费的,因为它不会给链接答案的方法增加任何真实的复杂性。

展开查看全部

相关问题