双舍入错误,即使使用 DBL_DIG

Double rounding error, even when using DBL_DIG

我正在尝试使用步长 0.3 生成一个介于 -10 和 10 之间的随机数(尽管我希望这些是任意值)并且我遇到双精度浮点精度问题。 Float.h 的 DBL_DIG 是指不发生舍入错误的最小精度 [编辑:这是错误的,请参阅 Eric Postpischil 的评论以了解 DBL_DIG] 的真实定义,但是当打印到这么多数字,我仍然看到舍入错误。

#include <stdio.h>
#include <float.h>
#include <stdlib.h>

int main()
{
  for (;;)
  {
    printf("%.*g\n", DBL_DIG, -10 + (rand() % (unsigned long)(20 / 0.3)) * 0.3);
  }
}

当我运行这个时,我得到这个输出:

8.3
-7
1.7
-6.1
-3.1
1.1
-3.4
-8.2
-9.1
-9.7
-7.6
-7.9
1.4
-2.5
-1.3
-8.8
2.6
6.2
3.8
-3.4
9.5
-7.6
-1.9
-0.0999999999999996
-2.2
5
3.2
2.9
-2.5
2.9
9.5
-4.6
6.2
0.799999999999999
-1.3
-7.3
-7.9

当然,一个简单的解决方案是 #define DBL_DIG 14,但我觉得这会浪费准确性。为什么会发生这种情况,我该如何防止这种情况发生?这不是 Is floating point math broken? 的副本,因为我问的是 DBL_DIG,以及如何找到不发生错误的最小准确度。

对于问题中的具体代码,我们可以在最后一刻使用整数值来避免过多的舍入误差:

printf("%.*g\n", DBL_DIG,
    (-100 + rand() % (unsigned long)(20 / 0.3) * 3.) / 10.);

这是通过将原始表达式中的每一项乘以 10(−10 因为 −100 和 .3 变为 3)然后将整个表达式除以 10 得到的。所以我们关心的所有值都在分子1是整数,浮点数正好表示(在其精度范围内)。

由于将精确计算整数值,因此在最后除以 10 时将只有一个舍入误差,结果将是 double 最接近所需值的值。

How many digits should I print to in order to avoid rounding error in most circumstances? (not just in my example above)

仅仅使用更多数字并不是一般情况下的解决方案。在大多数情况下,避免错误的一种方法是非常详细地了解浮点格式和算术,然后认真细致地编写代码。这种方法通常是好的,但并不总是成功,因为它通常是由人类实施的,尽管做出了所有相反的努力,他们仍然会继续犯错误。

脚注

1 考虑 (unsigned long)(20 / 0.3) 是一个较长的讨论,涉及对其他值和案例的意图和概括。

generate a random number between -10 and 10 with step 0.3
I would like the program to work with arbitrary values for the bounds and step size.
Why is this happening ....

问题的根源在于假设 typcial 实数(例如 string "0.3")可以精确编码为 double.

一个double可以精确编码大约264个不同的值。 0.3 不是其中之一。

而是使用最近的 double精确 值和最接近的 2 个如下所列:

0.29999999999999993338661852249060757458209991455078125
0.299999999999999988897769753748434595763683319091796875  (best 0.3)
0.3000000000000000444089209850062616169452667236328125

所以 OP 的代码正在尝试“-10 和 10,步长为 0.2999...”并打印出 "-0.0999999999999996""0.799999999999999" 正确"-0.1""0.8".


.... how do I prevent this happening?

  1. 以更有限的精度打印。

    // reduce the _bit_ output precision by about the root of steps
    #define LOG10_2 0.30102999566398119521373889472449
    int digits_less = lround(sqrt(20 / 0.3) * LOG10_2);
    for (int i = 0; i < 100; i++) {
      printf("%.*e\n", DBL_DIG - digits_less,
          -10 + (rand() % (unsigned long) (20 / 0.3)) * 0.3);
    }
    
    9.5000000000000e+00
    -3.7000000000000e+00
    8.6000000000000e+00
    5.9000000000000e+00
    ...
    -1.0000000000000e-01
    8.0000000000000e-01
    

OP 的代码实际上并没有执行“步骤”,因为它暗示了一个步长为 0.3 的循环。上面的 digits_less 是基于重复的“步骤”,否则 OP 的等式保证大约减少 1 位小数。精度的最佳降低取决于估计所有计算的潜在累积误差从"0.3"转换-->double 0.3(1/2位),除法(1/2位),乘法(1/2位)和加法(更复杂的位)。

  1. 等待下一个可能支持十进制浮点数的 C 版本。