C ++ Как избежать арифметической ошибки с плавающей запятой

Я пишу цикл, который увеличивается с плавающей запятой, но я столкнулся с арифметической проблемой с плавающей запятой, показанной в следующем примере:

for(float value = -2.0; value <= 2.0; value += 0.2)
    std::cout << value << std::endl;

Вот результат:

-2
-1.8
-1.6
-1.4
-1.2
-1
-0.8
-0.6
-0.4
-0.2
1.46031e-07
0.2
0.4
0.6
0.8
1
1.2
1.4
1.6
1.8

Почему именно я получаю 1.46031e-07 вместо 0? Я знаю, что это как-то связано с ошибками с плавающей запятой, но я не могу понять, почему это происходит и что мне делать, чтобы этого не произошло (если есть способ). Может ли кто-нибудь объяснить (или указать мне ссылку), что поможет мне понять? Любой вклад приветствуется. Спасибо!


person Barney    schedule 13.02.2013    source источник
comment
Как избежать арифметической ошибки с плавающей запятой - не можете, извините.   -  person    schedule 13.02.2013
comment
об этом много раз спрашивали и отвечали   -  person mechanical_meat    schedule 13.02.2013
comment
0.2 не может быть точно представлен float (при условии арифметики с плавающей запятой IEEE754). Вы можете увидеть это, если увеличите точность вывода: пример.   -  person Mankarse    schedule 13.02.2013


Ответы (6)


Это связано с тем, что числа с плавающей запятой имеют только определенную дискретную точность.

0,2 на самом деле не 0,2, но внутренне представлено как немного другое число.

Вот почему вы видите разницу.

Это обычное дело во всех вычислениях с плавающей запятой, и вы действительно не можете этого избежать.

person Srikant Krishna    schedule 13.02.2013
comment
Важно отметить, что, хотя 0,2 не может быть точно представлено как число с плавающей запятой, -2,0 и 2,0 могут. Я указываю на это только для того, чтобы избежать впечатления, что математика с плавающей запятой произвольна и капризна. Все, что происходит, это то, что float и double используют основание 2, а 0,2 эквивалентно 1/5, что не может быть представлено как конечное число с основанием 2. -2, 2.0, 0.5, 0.25, -.375 и 178432 все могут быть представлены точно. - person Bruce Dawson; 18.10.2015

Как уже говорили все, это связано с тем, что действительные числа представляют собой бесконечное и несчетное множество, в то время как представления с плавающей запятой используют конечное число битов. Числа с плавающей запятой могут быть только приближенными к действительным числам и даже во многих простых случаях не точны из-за их определения. Как вы теперь видели, 0.2 на самом деле не 0.2, а очень близкое к нему число. Добавляя их к value, вы накапливаете ошибку на каждом шаге.

В качестве альтернативы попробуйте использовать ints для своей итерации и разделить результат, чтобы вернуть его в нужном вам домене:

for (int value = -20; value <= 20; value += 2) {
  std::cout << (value / 10.f) << std::endl;
}

Для меня это дает:

-2
-1.8
-1.6
-1.4
-1.2
-1
-0.8
-0.6
-0.4
-0.2
0
0.2
0.4
0.6
0.8
1
1.2
1.4
1.6
1.8
2
person Joseph Mansfield    schedule 13.02.2013
comment
Я не могу поверить, что это не принятый ответ, поскольку этот и еще один ответ - единственные, которые дают решение. - person Celeritas; 13.02.2013
comment
Это напоминает мне известные проблемы, связанные с [ULP] (randomascii.wordpress.com/2012/02/25/) - person Fabien R; 30.08.2014

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

person bstamour    schedule 13.02.2013
comment
+1 за ссылку на эту статью. Для тех, кому нужен HTML вместо PDF: docs.oracle.com/ cd / E19957-01 / 806-3568 / ncg_goldberg.html - person ajp15243; 13.02.2013

Давайте сделаем свой цикл, но с повышенной точностью вывода.

код:

for(float value = -2.0; value <= 2.0; value += 0.2)
    std::cout << std::setprecision(100) << value << std::endl;

вывод:

-2
-1.7999999523162841796875
-1.599999904632568359375
-1.3999998569488525390625
-1.19999980926513671875
-0.999999821186065673828125
-0.79999983310699462890625
-0.599999845027923583984375
-0.3999998569488525390625
-0.19999985396862030029296875
1.460313825418779742904007434844970703125e-07
0.20000015199184417724609375
0.400000154972076416015625
0.6000001430511474609375
0.800000131130218505859375
1.00000011920928955078125
1.20000016689300537109375
1.40000021457672119140625
1.60000026226043701171875
1.80000030994415283203125
person Bill Lynch    schedule 13.02.2013

Используйте целые числа и разделите их:

for(int value = -20; value <= 20; value += 2)
    std::cout << (value/10.0) << std::endl;
person QuentinUK    schedule 13.02.2013
comment
+1, но ... разделив value на 10.0, вы предлагаете компилятору вычислить с двойной точностью, а затем преобразовать в одинарную точность (программа OP, которую вы пытаетесь эмулировать, имеет переменную с одинарной точностью). Бывает так, что это дает тот же результат, что и прямое деление с одинарной точностью. Но поскольку причина, по которой он дает одинаковые результаты, нетривиальна, компилятор почти наверняка сгенерирует код для деления с двойной точностью с последующим преобразованием из двойной точности в одинарную. По этой причине value / 10.0f было бы немного лучше. - person Pascal Cuoq; 14.02.2013
comment
Я только что проверил, и GCC действительно генерирует деление с одинарной точностью для float r = f / 10.0;. Я впечатлен (и мой предыдущий комментарий теряет свою ценность). - person Pascal Cuoq; 14.02.2013

Узнайте о представлении с плавающей запятой из книги по алгоритмам или через Интернет. Есть много ресурсов.

На данный момент кажется, что то, что вы хотите, - это какой-то способ получить ноль, когда это что-то очень близкое к нулю. и все мы знаем, что мы называем этот процесс «округлением». :) так почему бы вам не использовать его при печати этих чисел. Функция printf обеспечивает хорошие возможности форматирования для такого рода вещей. проверьте таблицы по следующей ссылке, если вы не знаете, как форматировать с помощью printf. (вы можете использовать форматирование для округления и правильного отображения чисел) printf ref: http://www.cplusplus.com/reference/cstdio/printf/?kw=printf

-- редактировать --

возможно, некоторые из вас знают, что согласно математике 1.99999999 .... то же самое, что и 2.0. Единственная разница в представлении. Но номер такой же.

ваша проблема с плавающей запятой немного похожа на эту. (это только для вашего пояснения. ваша проблема не такая, как 1.9999 .... вещь.)

person Deamonpog    schedule 13.02.2013