Ошибка вычитания чисел с плавающей запятой при прохождении через 0.0 - PullRequest
2 голосов
/ 21 апреля 2011

Следующая программа:

#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 ]

3 голосов
/ 21 апреля 2011

Это потому, что числа с плавающей запятой не могут быть сохранены в памяти в точное значение. Поэтому никогда не безопасно использовать == в значениях с плавающей запятой. Использование double увеличит точность, но опять же это не будет точным. Правильный способ сравнить значение с плавающей запятой - сделать что-то вроде этого:

val == target;   // not safe

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

РЕДАКТИРОВАТЬ: Как указано в комментариях, основная причина проблемы заключается в том, что 0.2 не может быть сохранен точно. Поэтому, когда вы вычитаете его из некоторого значения, каждый раз вызывая некоторую ошибку. Если вы выполняете этот тип вычисления с плавающей точкой несколько раз, то в определенный момент ошибка будет заметна. Я пытаюсь сказать, что все значения с плавающей запятой не могут быть сохранены, поскольку их бесконечно много. Небольшое неправильное значение обычно не заметно, но использование этого последовательного вычисления приведет к большей кумулятивной ошибке.

2 голосов
/ 22 апреля 2011

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)

Таким образом, мы видим, что накопленное округление, требуемое арифметикой с плавающей запятой, дает очень маленький ненулевой результат, который вы наблюдаете. Округление является тонким, но оно детерминированное, а не волшебство и не ошибка. Стоит потратить время на изучение.

1 голос
/ 21 апреля 2011

Арифметика с плавающей точкой не может точно представлять все числа. Таким образом, ошибки округления, которые вы наблюдаете, неизбежны.

Одной из возможных стратегий является использование формата с фиксированной запятой, например, Десятичный или валютный тип данных. Такие типы по-прежнему не могут представлять все числа, но будут вести себя так, как вы ожидаете для этого примера.

1 голос
/ 21 апреля 2011

Для уточнения: если мантисса числа с плавающей запятой закодирована в двоичном виде (как в большинстве современных 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 в цикле, вы можете достичь нуля.

...