Страницы

Поиск по вопросам

воскресенье, 24 ноября 2019 г.

Вычисления на числах с плавающей точкой не работают


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. Некоторые языки программирования также предоставляют "кусачки для пиццы", которы могут разделять фрагменты на точные десятки .

Комментариев нет:

Отправить комментарий