0.1 + 0.2 == 0.3
-> false
0.1 + 0.2
-> 0.30000000000000004
Что происходит?
Ответы
Ответ 1
Это особенности вычислений на бинарных числах с плавающей точкой. В большинстве языко
программирования они основаны на стандарте IEEE 754. Числа в JavaScript, double в C++
C# и Java используют 64-битное представление. Источник проблемы кроется в том, что числ
выражены через степени двойки. В результате рациональные числа (такие как 0.1, то ест
1∕10), знаменатель которых не является степенью двойки, не могут быть выражены точно.
Число 0.1 в бинарном 64-битном формате выглядит следующим образом:
0.1000000000000000055511151231257827021181583404541015625 как десятичное число, или
0x1.999999999999ap-4 в шестнадцатиричной нотации чисел с плавающей точкой C99.
А как рациональное число, то есть 1∕10, может быть записано точно:
0.1 как число в десятичной нотации, или
0x1.99999999999999...p-4 в шестнадцатиричной нотации, где ... — бесконечная последовательност
девяток.
Константы 0.2 и 0.3 тоже будут выражены приблизительно. Ближайшее к 0.2 бинарно
число с плавающей точкой будет немного больше, чем рациональное число 0.2, а ближайше
к 0.3 — немного меньше. В результате сумма 0.1 и 0.2 оказывается больше, чем 0.3,
равенство оказывается неверным.
Обычно для сравнения чисел с плавающей точкой задают некоторое малое число epsilo
и сравнивают с ним модуль разницы между числами: abs(a - b) < epsilon. Если неравенств
верно, то числа a и b примерно равны.
При последовательных вычислениях ошибка накапливается. Часто от порядка вычислени
зависит точность результата. Нет единого универсального epsilon, который подходил б
для всех случаев.
Для вычислений с деньгами следует использовать специальные типы чисел, основанны
на десятичной системе, если они доступны, например, Decimal в C#, BigDecimal в Jav
и т.п. Они используют десятичное внутреннее представление, что позволяет работать
числами вроде 29.99 без округления. Правда вычисления на них гораздо медленее.
Рекомендуется к прочтению:
What Every Computer Scientist Should Know About Floating-Point Arithmetic — очен
подробное объяснение.
floating-point-gui.de — более краткое объяснение.
Ответ 2
На данный момент все ответы здесь затрагивают вопрос в сухих технических терминах
Я хотел бы дать объяснение так, чтоб было понятно не только технарям.
Представьте, что вы нарезаете пиццу. У вас есть роботизированный нож, который може
разрезать кусочки пиццы точно пополам. Он может вдвое сократить целую пиццу, или о
может сократить вдвое существующий срез, но в любом случае, сокращение пополам всегд
точное.
Если вы начинаете с целой пиццы, режете ее пополам и продолжите делать разрезы, в
можете разрезать пополам 53 раза перед срезом, который будет слишком мал. В этот момен
вы уже не можете вдвое уменьшить эту часть и должны либо включать, либо исключать е
как есть.
Как бы вы объединили все отрезанные части таким образом, чтобы сформировать одн
десятую (0,1) или одну пятую (0,2) пиццы? На самом деле подумайте об этом и попробуйт
разобраться. Вы даже можете попытаться использовать настоящую пиццу :)
Большинство опытных программистов, конечно же, знают реальный ответ, который заключаетс
в том, что нет возможности объединить кусочки точно в десятую или пятую часть пиццы
используя эти срезы, независимо от того, насколько мелко вы нарезаете их. Можно реализоват
довольно точное приближение, и если вы добавите аппроксимацию 0,1 с аппроксимацией 0,2
вы достаточно близко приблизитесь к 0,3, но это все еще только приближение. Далее боле
подробно об этом.
Для чисел с двойной точностью (это точность, которая позволяет вам повторять разрезат
пиццу 53 раза), цифры, ближайшие к 0,1 (аппроксимация) - это 0,0999999999999999916733273153113259468227624893188476562
и 0,1000000000000000055511151231257827021181583404541015625. Последнее немного ближ
к 0,1, чем первое, поэтому числовой синтаксический анализатор, учитывая ввод 0,1, выбере
последнее число.
(Разница между этими двумя числами - это «самый маленький срез», который мы должн
включить, что вводит смещение вверх, либо исключить, что приводит к смещению вниз. Технически
термин для этого наименьшего фрагмента - это ULP .)
В случае 0,2 цифры все одинаковы, просто увеличиваются в 2 раза. Опять же, предпочтени
будет отдано значению, которое немного выше 0,2.
Обратите внимание, что в обоих случаях аппроксимации для 0,1 и 0,2 имеет небольшо
смещение вверх. Если мы добавим достаточно много этих смещений, они будут сдвигать цифр
дальше и дальше от той, что нам требуется, а в случае 0,1 + 0,2, смещение достаточн
велико, чтобы получившееся число больше не было самым близким числом к 0,3.
В частности, 0,1 + 0,2 действительно составляет 0,100000000000000005551115123125782702118158340454101562
+ 0.200000000000000011102230246251565404236316680908203125 = 0,3000000000000000444089209850062616169452667236328125
тогда как число, самое близкое к 0,3, фактически составляет 0,2999999999999999988897769753748434595763683319091796875.
В качестве дополнения: вы можете рассмотреть возможность масштабирования ваших значений
чтобы избежать проблем с арифметикой с плавающей запятой (пример).
P.S. Некоторые языки программирования также предоставляют "кусачки для пиццы", которы
могут разделять фрагменты на точные десятки .
Комментариев нет:
Отправить комментарий