128
0.1 + 0.2 == 0.3
-> false
0.1 + 0.2
-> 0.30000000000000004

Что происходит?

Kromster
  • 13,809
Kyubey
  • 32,103
  • 17
    Ещё один "каноничный" вопрос для закрытия остальных вопросов как дубликатов. Перевод-компиляция QA Is floating point math broken? Имеет ответ с подробным рассмотрением аппаратной стороны проблемы, но он мне не по зубам. Если есть желающие перевести, дерзайте. :) – Kyubey Apr 17 '15 at 20:34
  • Вот ещё по теме. – VladD Apr 17 '15 at 20:54
  • раз уж это вопрос-ответ-перевод из community wiki EnSO - может быть его сделать "общим" и в RuSO? –  Apr 20 '15 at 17:42
  • 5
    @PashaPash Если все переводы делать общими, то никто переводить не будет. :) Перевод — это тоже работа. Да и "каноничные" QA далеко не всегда общие, это скорее исторически сложилось из-за автоматической конвертации в прошлом. – Kyubey Apr 20 '15 at 22:51
  • @Athari: почему вы ссылку на оригинальный вопрос не приводите в самом тексте вопроса? С вас уже пример берут – jfs Apr 22 '15 at 20:25
  • @jfs Я взял в привычку указывать ссылку в первом комментарии (здесь меня igumnov подло опередил на 3 секунды :) ). Поэтому вопросов, откуда что взялось, возникать не должно. Но можно и в тексте приводить, да. – Kyubey Apr 22 '15 at 20:31
  • 1
    @Athari: комментарий не является частью вопроса, например, автор вопроса не должен размещать информацию, необходимую для ответа в комментариях. Справочное руководство рекомендует указывать как ссылку так и автора, иначе это похоже на плагиаризм. – jfs Apr 22 '15 at 20:50
  • @Discord надо бы попробовать – Nick Volynkin Jul 10 '15 at 06:45
  • 1
  • Не стоит копировать вопросы с enSO, не убедившись в отсутствии существующего перевода –  Apr 23 '16 at 10:26
  • Дело в том, что данная операция с плавающей точкой будет не точна. Без округления смысла нет использовать. – Kirill Apr 21 '16 at 13:40
  • ассоциация: http://stackoverflow.com/questions/588004/ – Nofate Feb 27 '17 at 16:49
  • https://www.youtube.com/watch?v=e7Wukn56-O4 – Yauheni May 14 '20 at 08:45
  • Числа с плавающей запятой нельзя использовать в программировании: EEE754 угрожает человечеству. – Виктор. Mar 09 '23 at 22:16
  • @Виктор Многовато воды и плохо отформатированного текста. И я так и не уловил, как конкретно автор предлагает решать проблему "IEEE 754 слишком неточно" кроме как "давайте сядем и подумаем". – Kyubey Mar 12 '23 at 14:15
  • 1
    хахах, общее количество голосов составляет 123! – Petəíŕd  The  Spring  Wizard May 10 '23 at 18:14

2 Answers2

148

Это особенности вычислений на бинарных числах с плавающей точкой. В большинстве языков программирования они основаны на стандарте IEEE 754. Числа в JavaScript, double в C++, C# и Java используют 64-битное представление. Источник проблемы кроется в том, что числа выражены через степени двойки. В результате рациональные числа (такие как 0.1, то есть 110), знаменатель которых не является степенью двойки, не могут быть выражены точно.

Число 0.1 в бинарном 64-битном формате выглядит следующим образом:

А как рациональное число, то есть 110, может быть записано точно:

  • 0.1 как число в десятичной нотации, или
  • 0x1.99999999999999...p-4 в шестнадцатиричной нотации, где ... — бесконечная последовательность девяток.

Константы 0.2 и 0.3 тоже будут выражены приблизительно. Ближайшее к 0.2 бинарное число с плавающей точкой будет немного больше, чем рациональное число 0.2, а ближайшее к 0.3 — немного меньше. В результате сумма 0.1 и 0.2 оказывается больше, чем 0.3, и равенство оказывается неверным.

Обычно для сравнения чисел с плавающей точкой задают некоторое малое число epsilon и сравнивают с ним модуль разницы между числами: abs(a - b) < epsilon. Если неравенство верно, то числа a и b примерно равны.

При последовательных вычислениях ошибка накапливается. Часто от порядка вычислений зависит точность результата. Нет единого универсального epsilon, который подходил бы для всех случаев.

Для вычислений с деньгами следует использовать специальные типы чисел, основанные на десятичной системе, если они доступны, например, Decimal в C#, BigDecimal в Java и т.п. Они используют десятичное внутреннее представление, что позволяет работать с числами вроде 29.99 без округления. Правда вычисления на них гораздо медленее.

Рекомендуется к прочтению:

Kyubey
  • 32,103
54

На данный момент все ответы здесь затрагивают вопрос в сухих технических терминах. Я хотел бы дать объяснение так, чтоб было понятно не только технарям.

Представьте, что вы нарезаете пиццу. У вас есть роботизированный нож, который может разрезать кусочки пиццы точно пополам. Он может вдвое сократить целую пиццу, или он может сократить вдвое существующий срез, но в любом случае, сокращение пополам всегда точное.

Если вы начинаете с целой пиццы, режете ее пополам и продолжите делать разрезы, вы можете разрезать пополам 53 раза перед срезом, который будет слишком мал. В этот момент вы уже не можете вдвое уменьшить эту часть и должны либо включать, либо исключать ее как есть.

Как бы вы объединили все отрезанные части таким образом, чтобы сформировать одну десятую (0,1) или одну пятую (0,2) пиццы? На самом деле подумайте об этом и попробуйте разобраться. Вы даже можете попытаться использовать настоящую пиццу :)

Большинство опытных программистов, конечно же, знают реальный ответ, который заключается в том, что нет возможности объединить кусочки точно в десятую или пятую часть пиццы, используя эти срезы, независимо от того, насколько мелко вы нарезаете их. Можно реализовать довольно точное приближение, и если вы добавите аппроксимацию 0,1 с аппроксимацией 0,2, вы достаточно близко приблизитесь к 0,3, но это все еще только приближение. Далее более подробно об этом.

Для чисел с двойной точностью (это точность, которая позволяет вам повторять разрезать пиццу 53 раза), цифры, ближайшие к 0,1 (аппроксимация) - это 0,09999999999999999167332731531132594682276248931884765625 и 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,1000000000000000055511151231257827021181583404541015625 + 0.200000000000000011102230246251565404236316680908203125 = 0,3000000000000000444089209850062616169452667236328125, тогда как число, самое близкое к 0,3, фактически составляет 0,2999999999999999988897769753748434595763683319091796875.

В качестве дополнения: вы можете рассмотреть возможность масштабирования ваших значений, чтобы избежать проблем с арифметикой с плавающей запятой (пример).

P.S. Некоторые языки программирования также предоставляют "кусачки для пиццы", которые могут разделять фрагменты на точные десятки .

Kromster
  • 13,809
Alex M
  • 757
  • @Kromster did my best. Дайте знать, если все еще есть грубые неточности – Alex M May 12 '18 at 18:24
  • 2
    Спасибо за реакцию. +1 и поправил пару мелочей. – Kromster May 12 '18 at 20:12