通过0.0时减去浮点数出错

Error subtracting floating point numbers when passing through 0.0

本文关键字:浮点数 出错 0时 通过      更新时间:2023-10-16

以下程序:

#include <stdio.h>
int main()
{
    double val = 1.0;
    int i;
    for (i = 0; i < 10; i++)
    {
        val -= 0.2;
        printf("%g %sn", val, (val == 0.0 ? "zero" : "non-zero"));
    }
    return 0;
}

产生如下输出:

0.8 non-zero
0.6 non-zero
0.4 non-zero
0.2 non-zero
5.55112e-17 non-zero
-0.2 non-zero
-0.4 non-zero
-0.6 non-zero
-0.8 non-zero
-1 non-zero

谁能告诉我是什么导致错误时从0.2减去0.2?这是舍入误差还是别的什么?最重要的是,我如何避免这个错误?

编辑:看起来结论是不用担心,因为5.55112e-17非常接近于零(感谢@therefromhere提供的信息)。

这是因为浮点数不能以精确值存储在内存中。因此,在浮点值中使用==是不安全的。使用double将提高精度,但同样不是精确的。比较浮点值的正确方法是这样做:

<>之前Val ==目标;//不安全//这样做//其中EPS为合适的低值,如1e-7fab (val - target) <EPS;之前

编辑:正如在评论中指出的,问题的主要原因是0.2不能准确地存储。所以当你从某个值中减去它时,每次都会产生一些错误。如果你重复做这种浮点计算,那么在某一点上,错误将是明显的。我想说的是,所有的浮点值都不能存储,因为它们是无限的。轻微的错误值通常不会引起注意,但使用连续计算将导致更高的累积误差。

0.2不是双精度浮点数,因此将其舍入为最接近的双精度数,即:

            0.200000000000000011102230246251565404236316680908203125

这是相当笨拙的,所以让我们看看十六进制:

          0x0.33333333333334

现在,让我们看看当这个值反复从1.0中减去时会发生什么:

          0x1.00000000000000
        - 0x0.33333333333334
        --------------------
          0x0.cccccccccccccc

精确的结果不能用双精度表示,所以它被四舍五入,得到:

          0x0.ccccccccccccd

用十进制表示就是:

            0.8000000000000000444089209850062616169452667236328125

现在我们重复这个过程:

          0x0.ccccccccccccd
        - 0x0.33333333333334
        --------------------
          0x0.9999999999999c
rounds to 0x0.999999999999a
           (0.600000000000000088817841970012523233890533447265625 in decimal)
          0x0.999999999999a
        - 0x0.33333333333334
        --------------------
          0x0.6666666666666c
rounds to 0x0.6666666666666c
           (0.400000000000000077715611723760957829654216766357421875 in decimal)
          0x0.6666666666666c
        - 0x0.33333333333334
        --------------------
          0x0.33333333333338
rounds to 0x0.33333333333338
           (0.20000000000000006661338147750939242541790008544921875 in decimal)
          0x0.33333333333338
        - 0x0.33333333333334
        --------------------
          0x0.00000000000004
rounds to 0x0.00000000000004
           (0.000000000000000055511151231257827021181583404541015625 in decimal)
因此,我们看到,浮点运算所需的累加舍入会产生非常小的非零结果。舍入是微妙的,但它是确定的,不是魔法,也不是错误。

浮点运算不能准确地表示所有数字。因此,像您观察到的舍入误差是不可避免的。

一个可能的策略是使用定点格式,例如十进制或货币数据类型。这样的类型仍然不能表示所有的数字,但会像您期望的那样运行。

详细说明一下:如果浮点数的尾数以二进制编码(就像大多数现代fpu中的情况一样),那么只有数字1/2,1/4,1/8,1/16,…的和(倍数)可以精确地用尾数表示。0.2的值近似为1/8 + 1/16 + ....有些数字甚至更小,但0.2的精确值无法用有限的尾数达到。

您可以尝试以下操作:

 printf("%.20f", 0.2);

,你会(可能)看到你认为的0.2不是0.2,而是一个有微小差异的数字(实际上,在我的计算机上它打印的是0.20000000000000001110)。现在你明白为什么你永远达不到0了。

但是如果让val = 12.5并在循环中减去0.125,则可以得到0