通过0.0时减去浮点数时出错

时间:2011-04-21 09:18:37

标签: c++ c floating-point rounding-error

以下计划:

#include <stdio.h>

int main()
{
    double val = 1.0;
    int i;

    for (i = 0; i < 10; i++)
    {
        val -= 0.2;
        printf("%g %s\n", 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获取该信息)。

4 个答案:

答案 0 :(得分:3)

因为浮点数不能以精确值存储在内存中。因此,在浮点值中使用==永远不安全。使用double会增加精度,但这又不准确。比较浮点值的正确方法是执行以下操作:

val == target;   // not safe

// instead do this
// where EPS is some suitable low value like 1e-7
fabs(val - target) < EPS; 

编辑:正如评论中所指出的,问题的主要原因是0.2无法准确存储。因此,当您从某个值中减去它时,每次都会导致一些错误。如果你反复进行这种浮点计算,那么在某些时候错误会很明显。我想说的是,所有浮点值都无法存储,因为它们有无穷大。略微错误的值通常不会引人注意,但使用它是连续计算会导致更高的累积误差。

答案 1 :(得分: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)

因此,我们看到浮点运算所需的累积舍入会产生您正在观察的非常小的非零结果。舍入是微妙的,但它是确定性的,而不是魔法,而不是错误。值得花时间学习。

答案 2 :(得分:1)

详细说明一下:如果浮点数的尾数以二进制编码(大多数现代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,你可以达到零。

答案 3 :(得分:1)

浮点运算不能完全代表所有数字。因此,像你观察到的舍入错误是不可避免的。

一种可能的策略是使用固定点格式,例如小数或货币数据类型。这些类型仍然不能代表所有数字,但会表现得像你期望的那样。