Математика с плавающей точкой нарушена?

2983

Рассмотрим следующий код:

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

Почему эти неточности случаются?

Катон Джонстон
источник
127
Переменные с плавающей точкой обычно имеют такое поведение. Это связано с тем, как они хранятся в оборудовании. Для получения дополнительной информации ознакомьтесь со статьей Википедии о числах с плавающей запятой .
Бен С
62
JavaScript обрабатывает десятичные числа как числа с плавающей точкой , что означает, что такие операции, как сложение, могут быть подвержены ошибке округления. Возможно, вы захотите взглянуть на эту статью: Что должен знать каждый учёный-компьютерщик об арифметике с плавающей точкой
Matt B
4
Просто для информации, ВСЕ числовые типы в javascript - это двойные коды IEEE-754.
Гари Уиллоуби
6
Поскольку JavaScript использует стандарт IEEE 754 для Math, он использует 64-разрядные числа с плавающей запятой. Это приводит к точности ошибку при выполнении с плавающей запятой (десятичные) расчетов, в общем, из - за компьютеры , работающими в базе 2 в то время как десятичная Base 10 .
Пардип Джайн
36
0.30000000000000004.com
Кенорб

Ответы:

2253

Двоичная математика с плавающей запятой такая. В большинстве языков программирования он основан на стандарте IEEE 754 . Суть проблемы заключается в том, что числа представлены в этом формате как целое число, умноженное на два; рациональные числа (например 0.1, то есть 1/10), знаменатель которых не является степенью двойки, не могут быть точно представлены.

Ибо 0.1в стандартном binary64формате представление может быть записано точно так же, как

  • 0.1000000000000000055511151231257827021181583404541015625 в десятичном или
  • 0x1.999999999999ap-4в C99 hexfloat нотации .

Напротив, рациональное число 0.1, то есть 1/10, может быть записано точно так же, как

  • 0.1 в десятичном или
  • 0x1.99999999999999...p-4в аналоге шестигранной нотации С99, где ...представляет бесконечную последовательность из 9-х.

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

Достаточно всеобъемлющим подходом к решению арифметических операций с плавающей точкой является то, что должен знать каждый компьютерный специалист об арифметике с плавающей точкой . Для более легкого для понимания объяснения, см . Плавающую точку- gui.de .

Примечание: все позиционные (базовые N) системы счисления точно решают эту проблему

Обычные старые десятичные числа (основание 10) имеют те же проблемы, поэтому такие числа, как 1/3, заканчиваются как 0.333333333 ...

Вы только что наткнулись на число (3/10), которое легко представить с помощью десятичной системы, но не соответствует двоичной системе. Он также идет в обе стороны (в некоторой степени): 1/16 - это уродливое десятичное число (0,0625), но в двоичном виде оно выглядит так же аккуратно, как 10 000-е в десятичном (0,0001) ** - если бы мы были в Привычка использовать систему счисления с базой 2 в нашей повседневной жизни - вы даже посмотрите на это число и инстинктивно поймете, что можете прийти туда, вдвое уменьшив что-то вдвое, снова и снова и снова.

** Конечно, это не совсем то, как числа с плавающей точкой хранятся в памяти (они используют форму научной записи). Тем не менее, это иллюстрирует тот факт, что двоичные ошибки точности с плавающей точкой имеют тенденцию возникать, потому что числа «реального мира», с которыми мы обычно заинтересованы работать, часто имеют степень десяти - но только потому, что мы используем десятичную систему счисления день - Cегодня. По этой же причине мы будем говорить такие вещи, как 71% вместо «5 из каждых 7» (71% - это приблизительное значение, поскольку 5/7 нельзя точно представить ни одним десятичным числом).

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

Примечание стороны: работа с плавающими в программировании

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

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

Как не делатьif (x == y) { ... }

Вместо этого делай if (abs(x - y) < myToleranceValue) { ... }.

где absабсолютное значение myToleranceValueдолжен быть выбран для вашего конкретного приложения - и он будет во многом зависеть от того, сколько «места для маневра» вы готовы предоставить, и какое наибольшее число вы собираетесь сравнивать (из-за потери точности) ). Остерегайтесь констант стиля «эпсилон» на своем языке. Они не должны использоваться в качестве значений допуска.

Daniel Scott
источник
181
Я думаю, что «некоторая константа ошибки» более правильная, чем «Эпсилон», потому что нет «Эпсилона», который можно было бы использовать во всех случаях. Различные эпсилоны должны использоваться в разных ситуациях. И машина эпсилон почти никогда не является хорошей константой в использовании.
Rotsor
34
Не совсем верно, что вся математика с плавающей точкой основана на стандарте IEEE [754]. Например, все еще используются некоторые системы, которые имеют старую шестнадцатеричную FP от IBM, и есть графические карты, которые не поддерживают арифметику IEEE-754. Однако это справедливо для разумного приближения.
Стивен Кэнон
19
Cray угробил соответствие IEEE-754 по скорости. Java также ослабила свою приверженность в качестве оптимизации.
Арт Тейлор
28
Я думаю, что вы должны добавить кое-что к этому ответу о том, как всегда должны выполняться вычисления на деньгах с арифметикой с фиксированной точкой на целых числах , потому что деньги квантуются. (Возможно, имеет смысл проводить расчеты внутреннего учета в крошечных долях цента, или какова бы ни была ваша наименьшая денежная единица - это часто помогает, например, уменьшить ошибку округления при преобразовании «29,99 долларов в месяц» в дневную ставку - но это должно все еще быть арифметикой с фиксированной запятой.)
zwol
18
Интересный факт: эти 0,1, которые не были точно представлены в двоичной форме с плавающей запятой, вызвали печально известную ошибку в программном обеспечении «Патриот», в результате которой во время первой войны в Ираке погибли 28 человек.
ЛВП
603

Перспектива разработчика оборудования

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

1. Обзор

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

2. Стандарты

Большинство процессоров соответствуют стандарту IEEE-754, но некоторые используют денормализованные или другие стандарты. Например, в IEEE-754 есть денормализованный режим, который позволяет представлять очень маленькие числа с плавающей запятой за счет точности. Следующее, однако, будет охватывать нормализованный режим IEEE-754, который является типичным режимом работы.

В стандарте IEEE-754 разработчикам аппаратного обеспечения разрешается любое значение error / epsilon, если в последнем месте оно составляет менее половины одного модуля, а результат должен составлять менее половины одного модуля в последнем месте. место для одной операции. Это объясняет, почему при повторных операциях ошибки складываются. Для двойной точности IEEE-754 это 54-й бит, поскольку 53 бита используются для представления числовой части (нормализованной), также называемой мантиссой, числа с плавающей запятой (например, 5.3 в 5.3e5). В следующих разделах более подробно рассматриваются причины аппаратной ошибки при различных операциях с плавающей запятой.

3. Причина ошибки округления в делении

Основной причиной ошибки в делении с плавающей запятой являются алгоритмы деления, используемые для вычисления отношения. Большинство компьютерных систем расчета деление используя умножение на инверсию, в основном Z=X/Y,Z = X * (1/Y), Деление вычисляется итеративно, то есть каждый цикл вычисляет некоторые биты частного до тех пор, пока не будет достигнута желаемая точность, которая для IEEE-754 равна нулю с ошибкой менее одной единицы в последнем месте. Таблица обратных значений Y (1 / Y) называется таблицей выбора коэффициентов (QST) при медленном делении, а размер в битах таблицы коэффициентов выбора обычно равен ширине радиуса или числу битов. коэффициент, вычисляемый в каждой итерации, плюс несколько защитных битов. Для стандарта IEEE-754 с двойной точностью (64-битная) это будет размер радиуса делителя плюс несколько защитных битов k, где k>=2. Так, например, типичная таблица выбора коэффициента для делителя, который вычисляет 2 бита отношения за раз (основание 4), будет 2+2= 4битами (плюс несколько необязательных битов).

3.1 Ошибка округления деления: аппроксимация взаимности

То, какие обратные величины находятся в таблице коэффициентов выбора, зависит от метода деления : медленное деление, такое как деление СТО, или быстрое деление, такое как деление Гольдшмидта; каждая запись модифицируется в соответствии с алгоритмом деления в попытке получить минимально возможную ошибку. В любом случае, однако, все обратные величины являются приблизительнымифактического взаимного и ввести некоторый элемент ошибки. И методы с медленным, и с быстрым делением вычисляют частное итеративно, т. Е. Некоторое количество битов частного вычисляется на каждом шаге, затем результат вычитается из делимого, и делитель повторяет шаги, пока ошибка не станет меньше половины одного блок на последнем месте. Методы медленного деления вычисляют фиксированное количество цифр отношения на каждом шаге и, как правило, дешевле в построении, а методы быстрого деления вычисляют переменное количество цифр на шаг и, как правило, стоят дороже. Самая важная часть методов деления состоит в том, что большинство из них полагаются на повторное умножение на приближение обратной величины, поэтому они подвержены ошибкам.

4. Ошибки округления в других операциях: усечение

Другой причиной ошибок округления во всех операциях являются различные режимы усечения окончательного ответа, которые допускает IEEE-754. Есть усечение, округление к нулю, округление до ближайшего (по умолчанию), округление вниз и округление вверх. Все методы вводят элемент ошибки менее чем на одну единицу в последнем месте для одной операции. Со временем и повторяющимися операциями усечение также добавляет к результирующей ошибке. Эта ошибка усечения особенно проблематична в возведении в степень, которое включает в себя некоторую форму повторного умножения.

5. Повторные операции

Поскольку аппаратное обеспечение, которое выполняет вычисления с плавающей запятой, должно давать только результат с ошибкой менее половины одного блока в последнем месте для одной операции, ошибка будет расти по сравнению с повторяющимися операциями, если их не наблюдать. Это причина того, что в вычислениях, которые требуют ограниченной ошибки, математики используют такие методы, как использование четного округленного до ближайшего числа в последнем месте IEEE-754, потому что со временем ошибки с большей вероятностью отменяют друг друга out и Interval Arithmetic в сочетании с вариациями режимов округления IEEE 754прогнозировать ошибки округления и исправлять их. Из-за его низкой относительной погрешности по сравнению с другими режимами округления, округление до ближайшей четной цифры (на последнем месте) является режимом округления по умолчанию IEEE-754.

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

6. Резюме

Короче говоря, основной причиной ошибок в операциях с плавающей запятой является сочетание усечения в аппаратных средствах и усечение обратной величины в случае деления. Поскольку стандарт IEEE-754 требует только ошибки менее половины одного блока в последнем месте для одной операции, ошибки с плавающей запятой при повторных операциях будут складываться, если не будут исправлены.

KernelPanik
источник
8
(3) неправильно. Ошибка округления в делении составляет не менее одной единицы в последнем месте, но не более половины единицы в последнем месте.
gnasher729
6
@ gnasher729 Хороший улов. Большинство основных операций также имеют ошибку менее 1/2 от одного устройства в последнем месте, используя режим округления IEEE по умолчанию. Отредактировал объяснение, а также отметил, что ошибка может быть больше, чем 1/2 от одного ulp, но меньше, чем 1 ulp, если пользователь отменяет режим округления по умолчанию (это особенно верно для встроенных систем).
KernelPanik
39
(1) с плавающей запятой числа не имеют ошибок. Каждое значение с плавающей запятой - это именно то, что есть. Большинство (но не все) с плавающей точкой операции дают неточные результаты. Например, нет двоичного значения с плавающей запятой, которое в точности равно 1,0 / 10,0. Некоторые операции (например, 1,0 + 1,0) действительно дают точные результаты, с другой стороны.
Соломон Слоу
19
«Основной причиной ошибки в делении с плавающей запятой являются алгоритмы деления, используемые для вычисления частного», - это очень обманчивая вещь. Для деления, соответствующего стандарту IEEE-754, единственной причиной ошибки в делении с плавающей запятой является невозможность точного представления результата в формате результата; один и тот же результат вычисляется независимо от используемого алгоритма.
Стивен Кэнон
6
@Matt Извините за поздний ответ. В основном это связано с проблемами ресурсов / времени и компромиссами. Есть способ сделать длинное деление / более «нормальное» деление, это называется SRT Division с основанием два. Однако это многократно сдвигает и вычитает делитель из делимого и занимает много тактов, поскольку вычисляет только один бит фактора за такт. Мы используем таблицы обратных ссылок, чтобы мы могли вычислить больше битов коэффициента за цикл и сделать эффективный компромисс между производительностью и скоростью.
KernelPanik
464

Когда вы конвертируете .1 или 1/10 в основание 2 (двоичное), вы получаете повторяющийся шаблон после десятичной точки, точно так же, как пытаетесь представить 1/3 в основании 10. Значение не является точным, и поэтому вы не можете сделать точная математика с использованием обычных методов с плавающей запятой.

Джоэл Коухорн
источник
133
Отличный и короткий ответ. Повторяющийся узор выглядит как 0.00011001100110011001100110011001100110011001100110011 ...
Константин Чернов
4
Это не объясняет, почему не используется лучший алгоритм, который не конвертируется в двоичные файлы.
Дмитрий Зайцев
12
Потому что производительность. Использование двоичного кода происходит в несколько тысяч раз быстрее, потому что оно является родным для машины.
Джоэл Коухорн
7
Есть методы, которые дают точные десятичные значения. BCD (двоичное кодированное десятичное число) или различные другие формы десятичного числа. Тем не менее, они оба медленнее (много медленнее) и занимают больше памяти, чем при использовании двоичной плавающей запятой. (Например, упакованный BCD хранит 2 десятичных цифры в байте. Это 100 возможных значений в байте, которые могут хранить 256 возможных значений, или 100/256, что приводит к потере около 60% возможных значений байта.)
Дункан C
16
@Jacksonkr ты все еще думаешь о Base-10. Компьютеры базы-2.
Джоэл Коухорн
308

Большинство ответов здесь обращаются к этому вопросу в очень сухих, технических терминах. Я хотел бы рассмотреть это в терминах, которые могут понять нормальные люди.

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

Этот резак для пиццы имеет очень тонкие движения, и если вы начнете с целой пиццы, а затем разделите ее пополам и продолжите каждый раз делить на два наименьших среза, вы можете сделать разделение пополам на 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,299999999999999988897769753748434595763683319091796875.


PS Некоторые языки программирования также предоставляют нарезчики пиццы, которые могут разбивать ломтики на ровные десятые доли . Хотя такие ножницы для пиццы являются редкостью, если у вас есть доступ к одному, вам следует использовать его, когда важно иметь возможность получить ровно одну десятую или одну пятую части.

(Первоначально опубликовано на Quora.)

Крис Шут-Янг
источник
3
Обратите внимание, что есть некоторые языки, которые включают точную математику. Одним из примеров является схема, например, через GNU Guile. См. Draketo.de/english/exact-math-to-the-rescue - они сохраняют математику в виде дроби и только в конце отрезаются .
Арне Бабенхаузерхайде
5
@FloatingRock На самом деле, очень немногие основные языки программирования имеют встроенные рациональные числа. Арне, как и я, является интриганкой, так что это вещи, которые мы избалованы.
Крис Джестер-Янг
5
@ArneBabenhauserheide Я думаю, стоит добавить, что это будет работать только с рациональными числами. Так что, если вы выполняете некоторую математику с иррациональными числами, такими как число пи, вам придется хранить ее как кратное число числа пи. Конечно, любое вычисление с использованием числа pi не может быть представлено как точное десятичное число.
Aidiakapi
13
@connexo Хорошо. Как бы вы запрограммировали ротатор для пиццы на 36 градусов? Что такое 36 градусов? (Подсказка: если вы можете определить это точно, у вас также есть резак для пиццы с точностью до десятой доли.) Другими словами, вы не можете иметь 1/360 (градус) или 1 / 10 (36 градусов) только с двоичной плавающей точкой.
Крис Шутер-Янг
12
@connexo Кроме того, «каждый идиот» не может вращать пиццу ровно на 36 градусов. Люди слишком склонны к ошибкам, чтобы делать что-то более точное.
Крис Шутер-Янг
212

Ошибки округления с плавающей точкой. 0,1 нельзя представить так же точно в базе-2, как в базе-10, из-за отсутствующего простого множителя 5. Точно так же, как 1/3 занимает бесконечное число цифр для представления в десятичной форме, но равно «0,1» в базе-3, 0.1 занимает бесконечное количество цифр в base-2, а не в base-10. И у компьютеров нет бесконечного количества памяти.

Девин Жанпьер
источник
133
компьютерам не нужно бесконечное количество памяти, чтобы получить правильные значения 0,1 + 0,2 = 0,3
Pacerier
23
@Pacerier Конечно, они могут использовать два целых числа с неограниченной точностью для представления дроби или использовать кавычки. Это определенное понятие «двоичный» или «десятичный» делает это невозможным - идея, что у вас есть последовательность двоичных / десятичных цифр и где-то там точка радиуса. Чтобы получить точные рациональные результаты, нам нужен лучший формат.
Девин Жанпьер
15
@Pacerier: ни двоичная, ни десятичная с плавающей точкой не могут точно хранить 1/3 или 1/13. Десятичные типы с плавающей точкой могут точно представлять значения в форме M / 10 ^ E, но они менее точны, чем двоичные числа одинакового размера, когда речь идет о представлении большинства других дробей . Во многих приложениях более полезно иметь более высокую точность с произвольными дробями, чем иметь идеальную точность с несколькими «особыми».
суперкат
13
@Pacerier Они делают, если они хранят числа как двоичные числа с плавающей запятой, что было точкой ответа.
Марк Амери
3
@chux: Разница в точности между двоичными и десятичными типами невелика, но разница 10: 1 в точности для наилучшего и наихудшего случаев для десятичных типов намного больше, чем разница 2: 1 для двоичных типов. Мне любопытно, построил ли кто-нибудь аппаратное или письменное программное обеспечение для эффективной работы с любым из десятичных типов, поскольку ни один из них не поддается эффективной реализации в аппаратном или программном обеспечении.
суперкат
121

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

Например:

var result = 1.0 + 2.0;     // result === 3.0 returns true

... вместо:

var result = 0.1 + 0.2;     // result === 0.3 returns false

Выражение 0.1 + 0.2 === 0.3возвращается falseв JavaScript, но, к счастью, целочисленная арифметика с плавающей точкой является точной, поэтому ошибок масштабирования можно избежать с помощью масштабирования.

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


1 Дуглас Крокфорд: JavaScript: Хорошие части : Приложение A - Ужасные части (стр. 105) .

Даниэль Вассалло
источник
3
Проблема в том, что само преобразование неточно. 16,08 * 100 = 1607,999999999998. Нужно ли прибегать к разделению числа и преобразованию отдельно (как в 16 * 100 + 08 = 1608)?
Джейсон
38
Решение здесь состоит в том, чтобы делать все ваши вычисления в целых числах, а затем делить на вашу пропорцию (в нашем случае 100) и округлять только при представлении данных. Это гарантирует, что ваши расчеты всегда будут точными.
Дэвид Гранадо
16
Просто немного придираться: целочисленная арифметика точна только с плавающей точкой до точки (каламбур). Если число больше 0x1p53 (если использовать шестнадцатеричную нотацию Java 7 с плавающей запятой, = 9007199254740992), то ulp равен 2 в этой точке, поэтому 0x1p53 + 1 округляется до 0x1p53 (а 0x1p53 + 3 округляется до 0x1p53 + 4, из-за округления до четности). :-D Но, конечно, если ваше число меньше 9 квадриллионов, у вас все будет хорошо. :-P
Крис Шестер-Янг
2
Джейсон, тебе нужно просто округлить результат (int) (16.08 * 100 + 0.5)
Михаил Семенов
@CodyBugstein « Итак, как вы получаете .1 + .2, чтобы показать .3? » Напишите пользовательскую функцию печати, чтобы поместить десятичное число, где вы хотите.
RonJohn
113

Мой ответ довольно длинный, поэтому я разделил его на три части. Поскольку вопрос касается математики с плавающей точкой, я сделал упор на том, что на самом деле делает машина. Я также определил двойную (64-битную) точность, но аргумент одинаково применим к любой арифметике с плавающей запятой.

преамбула

IEEE 754 с двойной точностью в двоичном формате с плавающей точкой (binary64) число представляет собой число вида

значение = (-1) ^ с * (1.м 51 м 50 ... м 2 м 1 м 0 ) 2 * 2 e-1023

в 64 битах:

  • Первый бит является знаковым битом : 1если число отрицательное, 0иначе 1 .
  • Следующие 11 битов являются показателем степени , который смещен на 1023. Другими словами, после считывания показательных битов из числа с двойной точностью, 1023 необходимо вычесть, чтобы получить степень двух.
  • Остальные 52 бита являются значимыми (или мантиссами). В мантиссе «подразумеваемый» 1.всегда 2 опускается, поскольку старший бит любого двоичного значения равен 1.

1 - IEEE 754 позволяет концепцию знакового нуля - +0и -0трактуется по- разному: 1 / (+0)положительная бесконечность; 1 / (-0)отрицательная бесконечность. Для нулевых значений биты мантиссы и экспоненты равны нулю. Примечание: нулевые значения (+0 и -0) явно не классифицируются как денормальные 2 .

2 - Это не относится к ненормальным числам , у которых показатель смещения равен нулю (и подразумевается 0.). Диапазон чисел с двойной точностью точности d min ≤ | x | ≤ d max , где d min (наименьшее представимое ненулевое число) составляет 2 -1023 - 51 (≈ 4.94 * 10 -324 ), а d max (наибольшее денормальное число, для которого мантисса состоит полностью из 1s) составляет 2 -1023 + 1 - 2 -1023 - 51 (≈ 2,225 * 10 -308 ).


Превращение числа с двойной точностью в двоичное

Существует много онлайн-конвертеров для преобразования числа с плавающей запятой двойной точности в двоичное (например, на сайте binaryconvert.com ), но здесь приведен пример кода C # для получения представления IEEE 754 для числа двойной точности (я разделяю эти три части двоеточиями ( :) :

public static string BinaryRepresentation(double value)
{
    long valueInLongType = BitConverter.DoubleToInt64Bits(value);
    string bits = Convert.ToString(valueInLongType, 2);
    string leadingZeros = new string('0', 64 - bits.Length);
    string binaryRepresentation = leadingZeros + bits;

    string sign = binaryRepresentation[0].ToString();
    string exponent = binaryRepresentation.Substring(1, 11);
    string mantissa = binaryRepresentation.Substring(12);

    return string.Format("{0}:{1}:{2}", sign, exponent, mantissa);
}

Приступая к делу: оригинальный вопрос

(Перейти к нижней части для версии TL; DR)

Катон Джонстон (задающий вопрос) спросил, почему 0,1 + 0,2! = 0,3.

Написанные в двоичном виде (с двоеточиями, разделяющими три части), представления значений IEEE 754:

0.1 => 0:01111111011:1001100110011001100110011001100110011001100110011010
0.2 => 0:01111111100:1001100110011001100110011001100110011001100110011010

Обратите внимание, что мантисса состоит из повторяющихся цифр 0011. Это является ключом к тому, почему есть какая-либо ошибка в вычислениях - 0,1, 0,2 и 0,3 не могут быть представлены в двоичном виде точно в конечном количестве двоичных разрядов, больше чем 1/9, 1/3 или 1/7 могут быть представлены точно в десятичные цифры .

Также обратите внимание, что мы можем уменьшить мощность в показателе степени на 52 и сместить точку в двоичном представлении вправо на 52 места (очень похоже на 10 -3 * 1,23 == 10 -5 * 123). Это тогда позволяет нам представить двоичное представление как точное значение, которое оно представляет в форме a * 2 p . где «а» является целым числом.

Преобразование показателей степени в десятичное, удаление смещения и повторное добавление подразумеваемых 1(в квадратных скобках) значений 0,1 и 0,2:

0.1 => 2^-4 * [1].1001100110011001100110011001100110011001100110011010
0.2 => 2^-3 * [1].1001100110011001100110011001100110011001100110011010
or
0.1 => 2^-56 * 7205759403792794 = 0.1000000000000000055511151231257827021181583404541015625
0.2 => 2^-55 * 7205759403792794 = 0.200000000000000011102230246251565404236316680908203125

Чтобы добавить два числа, показатель должен быть одинаковым, то есть:

0.1 => 2^-3 *  0.1100110011001100110011001100110011001100110011001101(0)
0.2 => 2^-3 *  1.1001100110011001100110011001100110011001100110011010
sum =  2^-3 * 10.0110011001100110011001100110011001100110011001100111
or
0.1 => 2^-55 * 3602879701896397  = 0.1000000000000000055511151231257827021181583404541015625
0.2 => 2^-55 * 7205759403792794  = 0.200000000000000011102230246251565404236316680908203125
sum =  2^-55 * 10808639105689191 = 0.3000000000000000166533453693773481063544750213623046875

Поскольку сумма не имеет вид 2 n * 1. {bbb}, мы увеличиваем показатель степени на единицу и сдвигаем десятичную ( двоичную ) точку, чтобы получить:

sum = 2^-2  * 1.0011001100110011001100110011001100110011001100110011(1)
    = 2^-54 * 5404319552844595.5 = 0.3000000000000000166533453693773481063544750213623046875

Теперь в мантиссе 53 бита (53-я в квадратных скобках в строке выше). Режим округления по умолчанию для IEEE 754 - « Округление до ближайшего », т. Е. Если число x попадает между двумя значениями a и b , выбирается значение, где младший значащий бит равен нулю.

a = 2^-54 * 5404319552844595 = 0.299999999999999988897769753748434595763683319091796875
  = 2^-2  * 1.0011001100110011001100110011001100110011001100110011

x = 2^-2  * 1.0011001100110011001100110011001100110011001100110011(1)

b = 2^-2  * 1.0011001100110011001100110011001100110011001100110100
  = 2^-54 * 5404319552844596 = 0.3000000000000000444089209850062616169452667236328125

Обратите внимание, что a и b отличаются только последним битом; ...0011+ 1= ...0100. В этом случае значение с наименьшим значащим нулевым битом равно b , поэтому сумма равна:

sum = 2^-2  * 1.0011001100110011001100110011001100110011001100110100
    = 2^-54 * 5404319552844596 = 0.3000000000000000444089209850062616169452667236328125

тогда как двоичное представление 0,3:

0.3 => 2^-2  * 1.0011001100110011001100110011001100110011001100110011
    =  2^-54 * 5404319552844595 = 0.299999999999999988897769753748434595763683319091796875

который отличается только от двоичного представления суммы 0,1 и 0,2 на 2 -54 .

Двоичное представление 0,1 и 0,2 является наиболее точным представлением чисел, допустимым IEEE 754. Добавление этого представления из-за режима округления по умолчанию приводит к значению, которое отличается только младшим значащим битом.

TL; DR

Запись 0.1 + 0.2в двоичном представлении IEEE 754 (с двоеточиями, разделяющими три части) и сравнение с ним 0.3, это (я поместил отдельные биты в квадратные скобки):

0.1 + 0.2 => 0:01111111101:0011001100110011001100110011001100110011001100110[100]
0.3       => 0:01111111101:0011001100110011001100110011001100110011001100110[011]

Преобразованные обратно в десятичные, эти значения:

0.1 + 0.2 => 0.300000000000000044408920985006...
0.3       => 0.299999999999999988897769753748...

Разница составляет ровно 2 -54 , что составляет ~ 5.5511151231258 × 10 -17 - незначительно (для многих приложений) по сравнению с исходными значениями.

Сравнение последних нескольких битов числа с плавающей запятой по своей природе опасно, и каждый, кто прочитает знаменитую статью « Что должен знать каждый компьютерный специалист об арифметике с плавающей запятой » (которая охватывает все основные части этого ответа).

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

Вай Ха Ли
источник
14
Мой ответ был отклонен вскоре после публикации. С тех пор я внес много изменений (в том числе явно отмечая повторяющиеся биты при записи 0.1 и 0.2 в двоичном формате, которые я опускал в оригинале). На случай, если избиратель увидит это, не могли бы вы дать мне обратную связь, чтобы я мог улучшить свой ответ? Я чувствую, что мой ответ добавляет что-то новое, поскольку обработка суммы в IEEE 754 не описывается в других ответах таким же образом. В то время как «То, что должен знать каждый специалист по информатике ...», охватывает примерно один и тот же материал, мой ответ касается конкретно случая 0,1 + 0,2.
Вай Ха Ли
57

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

Если бы компьютер работал в базе 10, 0.1было бы 1 x 10⁻¹, 0.2будет 2 x 10⁻¹и 0.3будет 3 x 10⁻¹. Целочисленная математика проста и точна, поэтому добавление 0.1 + 0.2, очевидно, приведет к 0.3.

Компьютеры обычно не работают в базе 10, они работают в базе 2. Вы все еще можете получить точные результаты для некоторых значений, например, 0.5есть 1 x 2⁻¹и 0.25есть 1 x 2⁻², и добавление их приводит к 3 x 2⁻², или 0.75. Точно.

Проблема возникает с числами, которые могут быть представлены точно в базе 10, но не в базе 2. Эти числа должны быть округлены до их ближайшего эквивалента. Предполагая, что очень распространенный IEEE 64-битный формат с плавающей запятой, ближайший номер к 0.1является 3602879701896397 x 2⁻⁵⁵, и ближайший номер к 0.2является 7205759403792794 x 2⁻⁵⁵; сложение их вместе приводит 10808639105689191 x 2⁻⁵⁵к точному десятичному значению 0.3000000000000000444089209850062616169452667236328125. Числа с плавающей точкой обычно округляются для отображения.

Марк Рэнсом
источник
2
@Mark Спасибо за это понятное объяснение, но тогда возникает вопрос, почему 0,1 + 0,4 точно добавляет до 0,5 (по крайней мере, в Python 3). Также, каков наилучший способ проверить равенство при использовании плавающих в Python 3?
Пчегур
2
@ user2417881 Операции IEEE с плавающей запятой имеют правила округления для каждой операции, и иногда округление может дать точный ответ, даже если два числа немного отклонены. Детали слишком длинны для комментариев, и я все равно не эксперт в них. Как вы видите в этом ответе, 0.5 является одним из немногих десятичных знаков, которые могут быть представлены в двоичном формате, но это просто совпадение. Для проверки равенства см. Stackoverflow.com/questions/5595425/… .
Марк Рэнсом
1
@ user2417881 Ваш вопрос заинтриговал меня, поэтому я превратил его в полный вопрос и ответ: stackoverflow.com/q/48374522/5987
Марк Рэнсом
47

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

Сжатие бесконечного числа действительных чисел в конечное число бит требует приблизительного представления. Хотя целых чисел бесконечно много, в большинстве программ результат целочисленных вычислений может храниться в 32 битах. Напротив, при любом фиксированном количестве битов большинство вычислений с действительными числами будут давать величины, которые невозможно точно представить с использованием такого количества битов. Поэтому результат вычисления с плавающей точкой часто должен быть округлен, чтобы соответствовать его конечному представлению. Эта ошибка округления является характерной особенностью вычисления с плавающей точкой.

Бретт Дэниел
источник
33

Мой обходной путь:

function add(a, b, precision) {
    var x = Math.pow(10, precision || 2);
    return (Math.round(a * x) + Math.round(b * x)) / x;
}

Точность относится к числу цифр, которые вы хотите сохранить после десятичной точки во время сложения.

Justineo
источник
30

Было опубликовано много хороших ответов, но я хотел бы добавить еще один.

Не все числа могут быть представлены с помощью поплавков / двойников Например, число «0,2» будет представлена как «0,200000003» в одинарной точности в стандарте IEEE754 флоат точки.

Модель для хранения реальных чисел под капотом представляет числа с плавающей точкой в ​​виде

введите описание изображения здесь

Даже если вы можете 0.2легко печатать , FLT_RADIXи DBL_RADIX2; не 10 для компьютера с FPU, который использует «Стандарт IEEE для двоичной арифметики с плавающей запятой (ISO / IEEE Std 754-1985)».

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

bruziuz
источник
28

Немного статистики, связанной с этим знаменитым вопросом двойной точности.

При сложении всех значений ( a + b ) с шагом 0,1 (от 0,1 до 100) вероятность ошибки точности составляет ~ 15% . Обратите внимание, что ошибка может привести к чуть большим или меньшим значениям. Вот некоторые примеры:

0.1 + 0.2 = 0.30000000000000004 (BIGGER)
0.1 + 0.7 = 0.7999999999999999 (SMALLER)
...
1.7 + 1.9 = 3.5999999999999996 (SMALLER)
1.7 + 2.2 = 3.9000000000000004 (BIGGER)
...
3.2 + 3.6 = 6.800000000000001 (BIGGER)
3.2 + 4.4 = 7.6000000000000005 (BIGGER)

При вычитании всех значений ( a - b, где a> b ) с шагом 0,1 (от 100 до 0,1) мы имеем ~ 34% вероятности погрешности точности . Вот некоторые примеры:

0.6 - 0.2 = 0.39999999999999997 (SMALLER)
0.5 - 0.4 = 0.09999999999999998 (SMALLER)
...
2.1 - 0.2 = 1.9000000000000001 (BIGGER)
2.0 - 1.9 = 0.10000000000000009 (BIGGER)
...
100 - 99.9 = 0.09999999999999432 (SMALLER)
100 - 99.8 = 0.20000000000000284 (BIGGER)

* 15% и 34% действительно огромны, поэтому всегда используйте BigDecimal, когда точность имеет большое значение. С двумя десятичными цифрами (шаг 0.01) ситуация ухудшается немного больше (18% и 36%).

Костас Халкиас
источник
28

Нет, не разбито, но большинство десятичных дробей должно быть аппроксимировано

Резюме

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

Даже простые числа, такие как 0,01, 0,02, 0,03, 0,04 ... 0,24, не могут быть представлены в виде двоичных дробей. Если вы подсчитаете 0,01, 0,02, 0,03 ..., то только до 0,25 вы получите первую дробь, представимую в базе 2 . Если вы попробуете это с использованием FP, ваш 0.01 был бы немного не таким, поэтому единственный способ добавить 25 из них до точного 0.25 потребовал бы длинной цепочки причинно-следственных связей, включающей защитные биты и округление. Трудно предсказать, поэтому мы вскидываем руки и говорим: «FP - это неточно», но это не совсем так.

Мы постоянно даем оборудованию FP что-то, что кажется простым в базе 10, но является повторяющейся дробью в базе 2.

Как это случилось?

Когда мы пишем в десятичном виде, каждая дробь (в частности, каждый завершающий десятичный) является рациональным числом вида

           а / (2 н х 5 м )

В двоичном коде мы получаем только 2 n член, то есть:

           а / 2 н

Таким образом , в десятичной системе , мы не можем представить 1 / 3 . Поскольку основание 10 включает в себя 2 в качестве простого множителя, каждое число, которое мы можем записать в виде двоичной дроби, также может быть записано в виде дробной базы 10. Однако вряд ли все, что мы пишем как дробь с основанием 10, представимо в двоичном виде. В диапазоне от 0,01, 0,02, 0,03 до 0,99 в нашем формате FP могут быть представлены только три числа: 0,25, 0,50 и 0,75, поскольку они равны 1/4, 1/2 и 3/4, все числа с простым множителем, использующим только 2 n член.

В базе 10 мы не можем представить 1 / 3 . Но в двоичном коде, мы не можем сделать +1 / +10 или +1 / +3 .

Таким образом, хотя каждая двоичная дробь может быть записана в десятичном виде, обратное неверно. И фактически большинство десятичных дробей повторяется в двоичном формате.

Имея дело с этим

Разработчикам, как правило, дают указание делать < сравнения epsilon , лучше посоветовать округлить до целочисленных значений (в библиотеке C: round () и roundf (), т. Е. Остаться в формате FP), а затем сравнить. Округление до определенной длины десятичной дроби решает большинство проблем с выводом.

Кроме того, в реальных проблемах с сокращением чисел (проблемы, которые были изобретены FP на ранних, ужасно дорогих компьютерах) физические константы вселенной и все другие измерения известны лишь относительно небольшому числу значащих цифр, поэтому все проблемное пространство был "неточным" в любом случае. Точность FP не является проблемой в такого рода приложениях.

Вся проблема действительно возникает, когда люди пытаются использовать FP для подсчета бобов. Это работает для этого, но только если вы придерживаетесь целочисленных значений, что побеждает смысл его использования. Вот почему у нас есть все эти библиотеки программного обеспечения с десятичной дробью.

Мне нравится ответ Криса на «Пиццу» , потому что он описывает реальную проблему, а не просто обычные пометки о «неточности». Если бы FP были просто «неточными», мы могли бы это исправить и сделали бы это десятилетия назад. Причина, по которой мы этого не делаем, заключается в том, что формат FP компактен и быстр, и это лучший способ сократить множество чисел. Кроме того, это наследие космической эры и гонки вооружений и ранних попыток решить большие проблемы с очень медленными компьютерами с использованием небольших систем памяти. (Иногда отдельные магнитные сердечники для 1-битного хранилища, но это уже другая история. )

Вывод

Если вы просто подсчитываете бины в банке, программные решения, которые в первую очередь используют десятичные строковые представления, работают превосходно. Но вы не можете делать квантовую хромодинамику или аэродинамику таким образом.

DigitalRoss
источник
Округление до ближайшего целого числа не является безопасным способом решения проблемы сравнения во всех случаях. Округления 0,4999998 и 0,500001 округляются до различных целых чисел, поэтому вокруг каждой точки отсечения округления существует «опасная зона». (Я знаю, что эти десятичные строки, вероятно, не совсем представимы как двоичные числа с плавающей точкой IEEE.)
Питер Кордес
1
Кроме того, хотя с плавающей запятой это «устаревший» формат, он очень хорошо спроектирован. Я не знаю ничего, что кто-то изменил бы, если бы сейчас его переделывали. Чем больше я узнаю об этом, тем больше я думаю, что он действительно хорошо продуман. Например, смещенный показатель означает, что последовательные двоичные числа с плавающей запятой имеют последовательные целочисленные представления, так что вы можете реализовать nextafter()с целочисленным приращением или уменьшением двоичное представление числа с плавающей запятой IEEE. Кроме того, вы можете сравнить числа с плавающей точкой как целые числа и получить правильный ответ, за исключением случаев, когда они оба отрицательны (из-за величины знака против дополнения 2).
Питер Кордес
Я не согласен, числа с плавающей точкой должны храниться как десятичные числа, а не как двоичные, и все проблемы решены.
Ронен Фестингер
Не должно ли " x / (2 ^ n + 5 ^ n) " быть " x / (2 ^ n * 5 ^ n) "?
Вай Ха Ли
@RonenFestinger - как насчет 1/3?
Стивен С.
19

Вы пробовали решение для клейкой ленты?

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

 if( (n * 0.1) < 100.0 ) { return n * 0.1 - 0.000000000000001 ;}
                    else { return n * 0.1 + 0.000000000000001 ;}    

У меня была такая же проблема в научном симуляционном проекте на c #, и я могу вам сказать, что если вы проигнорируете эффект бабочки, он превратится в большого толстого дракона и укусит вас в а **

workoverflow
источник
19

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

parseFloat((0.1 + 0.2).toFixed(10)) => Will return 0.3

Позвольте мне объяснить, почему это лучшее решение. Как уже упоминалось в ответах выше, для решения проблемы рекомендуется использовать готовую функцию toFixed () Javascript. Но, скорее всего, вы столкнетесь с некоторыми проблемами.

Представьте , что вы собираетесь сложить два числа с плавающей точкой , как 0.2и 0.7здесь: 0.2 + 0.7 = 0.8999999999999999.

Ваш ожидаемый результат 0.9означает, что в этом случае вам нужен результат с точностью до 1 цифры. Таким образом, вы должны были использовать, (0.2 + 0.7).tofixed(1) но вы не можете просто дать определенный параметр toFixed (), так как он зависит от заданного числа, например

`0.22 + 0.7 = 0.9199999999999999`

В этом примере вам нужна точность в 2 цифры, так и должно быть toFixed(2), так какой же параметр должен соответствовать каждому заданному числу с плавающей точкой?

Вы можете сказать, пусть это будет 10 в каждой ситуации:

(0.2 + 0.7).toFixed(10) => Result will be "0.9000000000"

Черт! Что вы собираетесь делать с этими нежелательными нулями после 9? Пришло время преобразовать это в float, чтобы сделать это, как вы хотите:

parseFloat((0.2 + 0.7).toFixed(10)) => Result will be 0.9

Теперь, когда вы нашли решение, лучше предложить его в виде такой функции:

function floatify(number){
           return parseFloat((number).toFixed(10));
        }

Давайте попробуем сами:

function floatify(number){
       return parseFloat((number).toFixed(10));
    }
 
function addUp(){
  var number1 = +$("#number1").val();
  var number2 = +$("#number2").val();
  var unexpectedResult = number1 + number2;
  var expectedResult = floatify(number1 + number2);
  $("#unexpectedResult").text(unexpectedResult);
  $("#expectedResult").text(expectedResult);
}
addUp();
input{
  width: 50px;
}
#expectedResult{
color: green;
}
#unexpectedResult{
color: red;
}
<script src="https://ajax.googleapis.com/ajax/libs/jquery/2.1.1/jquery.min.js"></script>
<input id="number1" value="0.2" onclick="addUp()" onkeyup="addUp()"/> +
<input id="number2" value="0.7" onclick="addUp()" onkeyup="addUp()"/> =
<p>Expected Result: <span id="expectedResult"></span></p>
<p>Unexpected Result: <span id="unexpectedResult"></span></p>

Вы можете использовать это так:

var x = 0.2 + 0.7;
floatify(x);  => Result: 0.9

Как предполагает W3SCHOOLS, есть и другое решение, вы можете умножить и разделить, чтобы решить проблему выше:

var x = (0.2 * 10 + 0.1 * 10) / 10;       // x will be 0.3

Имейте в виду, что (0.2 + 0.1) * 10 / 10это не будет работать вообще, хотя кажется, что это то же самое! Я предпочитаю первое решение, так как я могу применить его как функцию, которая преобразует входной float в точный выходной float.

Мухаммед Мусави
источник
это сделало меня настоящей головной болью. Я суммирую 12 чисел с плавающей запятой, затем показываю сумму и среднее, если эти числа. использование toFixed () может исправить суммирование 2 чисел, но при суммировании нескольких чисел скачок будет значительным.
Нурягды Мустапаев
@Nuryagdy Mustapayev Я не понял твоего намерения, так как я проверил, прежде чем ты суммируешь 12 чисел с плавающей запятой, затем используй функцию floatify () для результата, затем делай с ней что хочешь, я не заметил никаких проблем с его использованием.
Мухаммед Мусави
Я просто говорю в моей ситуации, когда у меня есть около 20 параметров и 20 формул, где результат каждой формулы зависит от других, это решение не помогло.
Нурягды Мустапаев
16

Эти странные числа появляются, потому что компьютеры используют двоичную (основание 2) систему счисления для целей расчета, а мы используем десятичную (основание 10).

Существует большинство дробных чисел, которые нельзя точно представить ни в двоичном, ни в десятичном виде, ни в обоих. Результат - округленное (но точное) число результатов.

Пиюш С528
источник
Я совсем не понимаю твой второй абзац.
Наэ
1
@Nae Я бы перевел второй абзац как «Большинство фракций не может быть представлено точно в десятичном или двоичном виде. Поэтому большинство результатов будет округлено - хотя они все равно будут точными до количества бит / цифр, присущих представлению быть использованным."
Стив Саммит
15

Многие из многочисленных дубликатов этого вопроса спрашивают о влиянии округления с плавающей запятой на конкретные числа. На практике легче понять, как это работает, глядя на точные результаты вычислений, а не просто читая об этом. Некоторые языки обеспечивают способы сделать это - такие как преобразование floatили doubleв BigDecimalв Java.

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

Применяя его к числам в вопросе, рассматривается как двойное число:

0,1 преобразуется в 0,1000000000000000055511151231257827021181583404541015625,

0.2 преобразуется в 0.200000000000000011102230246251565404236316680908203125,

0,3 преобразуется в 0,299999999999999988897769753748434595763683319091796875 и

0.30000000000000004 преобразуется в 0.3000000000000000444089209850062616169452667236328125.

При добавлении первых двух чисел вручную или в десятичном калькуляторе, таком как Калькулятор полной точности , отображается точная сумма фактических входных значений: 0,3000000000000000166533453693773481063544750213623046875.

Если бы оно было округлено до эквивалента 0,3, ошибка округления была бы равна 0,0000000000000000277555756156289135105907917022705078125. Округление до эквивалента 0,30000000000000004 также дает ошибку округления 0,0000000000000000277555756156289135105907917022705078125. Применяется прерыватель круглой и четной связи.

Возвращаясь к преобразователю с плавающей запятой, необработанный шестнадцатеричный код для 0.30000000000000004 равен 3fd3333333333334, который заканчивается четной цифрой и, следовательно, является правильным результатом.

Патриция Шанахан
источник
2
Человеку, редактирование которого я только что откатил: я считаю кавычки кода подходящими для цитирования кода. Этот ответ, не зависящий от языка, вообще не содержит кода в кавычках. Числа могут использоваться в английских предложениях, и это не превращает их в код.
Патриция Шанахан
Это , вероятно , почему кто - то отформатированный ваши номера в виде кода - не для форматирования, но для удобства чтения.
Вай Ха Ли
... также, округление до четного относится к двоичному представлению, а не к десятичному представлению. Посмотрите это или, например, это .
Вай Ха Ли
@WaiHaLee Я не применял нечетный / четный тест к любым десятичным числам, только к шестнадцатеричным. Шестнадцатеричная цифра является четной, если и только если младший значащий бит ее двоичного расширения равен нулю.
Патриция Шанахан
14

Учитывая, что никто не упомянул об этом ...

Некоторые языки высокого уровня, такие как Python и Java, поставляются с инструментами для преодоления двоичных ограничений с плавающей запятой. Например:

  • decimalМодуль Python и BigDecimalкласс Java , которые представляют числа внутренне в десятичной записи (в отличие от двоичной записи). Оба имеют ограниченную точность, поэтому они все еще подвержены ошибкам, однако они решают наиболее распространенные проблемы с двоичной арифметикой с плавающей запятой.

    Десятичные числа очень хороши при работе с деньгами: десять центов плюс двадцать центов всегда равны тридцати центам:

    >>> 0.1 + 0.2 == 0.3
    False
    >>> Decimal('0.1') + Decimal('0.2') == Decimal('0.3')
    True
    

    decimalМодуль Python основан на стандарте IEEE 854-1987 .

  • fractionsМодуль Python и BigFractionкласс Apache Common . Оба представляют рациональные числа в виде (numerator, denominator)пар, и они могут дать более точные результаты, чем десятичная арифметика с плавающей запятой.

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

Андреа Корбеллини
источник
14

Могу ли я просто добавить; люди всегда считают, что это компьютерная проблема, но если вы рассчитываете своими руками (база 10), вы не можете получить, (1/3+1/3=2/3)=trueесли у вас нет бесконечности, чтобы добавить 0,333 ... к 0,333 ... так же, как с (1/10+2/10)!==3/10проблемой в базе 2, вы усекаете его до 0,333 + 0,333 = 0,666 и, вероятно, округляете его до 0,667, что также будет технически неточным.

Считай в троице, и трети не проблема, хотя - может быть, какая-то гонка с 15 пальцами на каждой руке спросит, почему твоя десятичная математика была нарушена ...


источник
Поскольку люди используют десятичные числа, я не вижу веской причины, по которой числа с плавающей точкой не представлены в виде десятичного числа по умолчанию, поэтому у нас есть точные результаты.
Ронен Фестингер
Люди используют множество оснований, отличных от 10 (десятичных), двоичное - это то, которое мы чаще всего используем для вычислений ... «веская причина» в том, что вы просто не можете представить каждую дробь в каждой базе…
Бинарную арифметику @RonenFestinger легко реализовать на компьютерах, поскольку она требует только восьми основных операций с цифрами: скажем, $ a $, $ b $ в $ 0,1 $, все, что вам нужно знать, это $ \ operatorname {xor} (a, b) $ и $ \ operatorname {cb} (a, b) $, где xor является исключительным или cb - это «переносной бит», который во всех случаях равен $ 0 $, кроме случаев, когда $ a = 1 = b $, и в этом случае мы имеем один (на самом деле коммутативность всех операций экономит вам $ 2 $ кейсов, и все, что вам нужно, это $ 6 $ правил). Десятичное расширение требует хранения 10 $ × 11 $ (в десятичной записи) и различных состояний $ 10 $ для каждого бита и хранения отходов на переносе.
Оскар Лимка
@RonenFestinger - десятичный НЕ является более точным. Вот что говорит этот ответ. Для любой выбранной вами базы будут рациональные числа (дроби), которые дают бесконечно повторяющиеся последовательности цифр. Напомним, что некоторые из первых компьютеров действительно использовали представления 10 для чисел, но первые разработчики компьютерного оборудования вскоре пришли к выводу, что реализация базы 2 намного проще и эффективнее.
Стивен С
9

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

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

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

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

Блэр Хоутон
источник
9

Просто для забавы, я играл с представлением поплавков, следуя определениям из Standard C99, и я написал код ниже.

Код печатает двоичное представление с плавающей точкой в ​​3 отдельных группах

SIGN EXPONENT FRACTION

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

Поэтому, когда вы пишете float x = 999..., компилятор преобразует это число в битовое представление, напечатанное функцией xx, так, чтобы сумма, напечатанная функцией, yyбыла равна данному числу.

На самом деле эта сумма только приблизительная. Для числа 999 999 999 компилятор вставит в битовое представление числа с плавающей точкой число 1 000 000 000

После кода я присоединяю консольный сеанс, в котором я вычисляю сумму терминов для обеих констант (за исключением PI и 999999999), которые действительно существуют в аппаратных средствах, вставленных туда компилятором.

#include <stdio.h>
#include <limits.h>

void
xx(float *x)
{
    unsigned char i = sizeof(*x)*CHAR_BIT-1;
    do {
        switch (i) {
        case 31:
             printf("sign:");
             break;
        case 30:
             printf("exponent:");
             break;
        case 23:
             printf("fraction:");
             break;

        }
        char b=(*(unsigned long long*)x&((unsigned long long)1<<i))!=0;
        printf("%d ", b);
    } while (i--);
    printf("\n");
}

void
yy(float a)
{
    int sign=!(*(unsigned long long*)&a&((unsigned long long)1<<31));
    int fraction = ((1<<23)-1)&(*(int*)&a);
    int exponent = (255&((*(int*)&a)>>23))-127;

    printf(sign?"positive" " ( 1+":"negative" " ( 1+");
    unsigned int i = 1<<22;
    unsigned int j = 1;
    do {
        char b=(fraction&i)!=0;
        b&&(printf("1/(%d) %c", 1<<j, (fraction&(i-1))?'+':')' ), 0);
    } while (j++, i>>=1);

    printf("*2^%d", exponent);
    printf("\n");
}

void
main()
{
    float x=-3.14;
    float y=999999999;
    printf("%lu\n", sizeof(x));
    xx(&x);
    xx(&y);
    yy(x);
    yy(y);
}

Вот сеанс консоли, в котором я вычисляю реальное значение с плавающей точкой, которое существует в аппаратных средствах. Раньше я bcпечатал сумму терминов, выводимых основной программой. Можно вставить эту сумму в Python replили что-то подобное.

-- .../terra1/stub
@ qemacs f.c
-- .../terra1/stub
@ gcc f.c
-- .../terra1/stub
@ ./a.out
sign:1 exponent:1 0 0 0 0 0 0 fraction:0 1 0 0 1 0 0 0 1 1 1 1 0 1 0 1 1 1 0 0 0 0 1 1
sign:0 exponent:1 0 0 1 1 1 0 fraction:0 1 1 0 1 1 1 0 0 1 1 0 1 0 1 1 0 0 1 0 1 0 0 0
negative ( 1+1/(2) +1/(16) +1/(256) +1/(512) +1/(1024) +1/(2048) +1/(8192) +1/(32768) +1/(65536) +1/(131072) +1/(4194304) +1/(8388608) )*2^1
positive ( 1+1/(2) +1/(4) +1/(16) +1/(32) +1/(64) +1/(512) +1/(1024) +1/(4096) +1/(16384) +1/(32768) +1/(262144) +1/(1048576) )*2^29
-- .../terra1/stub
@ bc
scale=15
( 1+1/(2) +1/(4) +1/(16) +1/(32) +1/(64) +1/(512) +1/(1024) +1/(4096) +1/(16384) +1/(32768) +1/(262144) +1/(1048576) )*2^29
999999999.999999446351872

Вот и все. Значение 999999999 на самом деле

999999999.999999446351872

Вы также можете проверить, bcчто -3.14 также возмущен. Не забудьте установить scaleфактор в bc.

Отображаемая сумма - это то, что находится внутри оборудования. Значение, которое вы получите путем вычисления, зависит от установленного вами масштаба. Я установил scaleкоэффициент на 15. Математически, с бесконечной точностью, кажется, это 1 000 000 000.

alinsoar
источник
5

Другой способ взглянуть на это: используются 64 бита для представления чисел. Как следствие, не может быть более 2 ** 64 = 18 446 744 073 709 551 616 различных чисел.

Тем не менее, Мат говорит, что между 0 и 1 уже существует бесконечно много десятичных знаков. IEE 754 определяет кодировку для эффективного использования этих 64 битов для гораздо большего числового пространства плюс NaN и +/- Infinity, поэтому между точно представленными числами, заполненными числа только приблизительные.

К сожалению, 0,3 сидит в пробел.

Торстен Беккер
источник
4

Представьте, что вы работаете в базовой десятке, скажем, с 8 цифрами точности. Вы проверяете,

1/3 + 2 / 3 == 1

и узнай, что это возвращается false. Почему? Ну, а реальные цифры у нас есть

1/3 = 0,333 .... и 2/3 = 0,666 ....

Обрезая в восемь знаков после запятой, мы получаем

0.33333333 + 0.66666666 = 0.99999999

что, конечно, отличается от 1.00000000точно 0.00000001.


Ситуация для двоичных чисел с фиксированным числом битов в точности аналогична. Как реальные цифры, мы имеем

1/10 = 0,0001100110011001100 ... (база 2)

а также

1/5 = 0,0011001100110011001 ... (база 2)

Если бы мы урезали их, скажем, до семи бит, то мы бы получили

0.0001100 + 0.0011001 = 0.0100101

в то время как с другой стороны,

3/10 = 0,01001100110011 ... (база 2)

который, усеченный до семи бит, есть 0.0100110, и они отличаются точно 0.0000001.


Точная ситуация немного сложнее, потому что эти цифры обычно хранятся в научной записи. Так, например, вместо того, чтобы хранить 1/10, поскольку 0.0001100мы можем хранить это как-то так 1.10011 * 2^-4, в зависимости от того, сколько битов мы выделили для экспоненты и мантиссы. Это влияет на то, сколько знаков точности вы получите для своих расчетов.

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

Дэниел МакЛори
источник
4

Начиная с Python 3.5 вы можете использовать math.isclose()функцию для проверки приблизительного равенства:

>>> import math
>>> math.isclose(0.1 + 0.2, 0.3)
True
>>> 0.1 + 0.2 == 0.3
False
nauer
источник
3

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

Взгляните, например, на https://posithub.org/ , который демонстрирует тип числа, называемый posit (и его предшественник unum), который обещает предложить лучшую точность с меньшим количеством битов. Если мое понимание верно, это также устраняет проблемы в этом вопросе. Довольно интересный проект, за ним стоит математик доктор Джон Густафсон . Все это с открытым исходным кодом, со многими актуальными реализациями на C / C ++, Python, Julia и C # ( https://hastlayer.com/arithmetics ).

Piedone
источник
3

Это на самом деле довольно просто. Когда у вас есть система 10-й базы (как наша), она может выражать только дроби, которые используют основной множитель базы. Первичные множители 10 равны 2 и 5. Таким образом, 1/2, 1/4, 1/5, 1/8 и 1/10 могут быть выражены чисто, потому что все знаменатели используют простые множители 10. Напротив, 1 / 3, 1/6 и 1/7 - все повторяющиеся десятичные дроби, потому что их знаменатели используют простой множитель 3 или 7. В двоичном (или базовом 2) единственном простом множителе является 2. Таким образом, вы можете выражать только те дроби, которые содержат только 2 как главный фактор. В двоичном коде 1/2, 1/4, 1/8 все будет выражено чисто в десятичном виде. В то время как 1/5 или 1/10 будут повторять десятичные дроби. Таким образом, 0,1 и 0,2 (1/10 и 1/5), в то время как чистые десятичные дроби в системе Base 10 повторяют десятичные дроби в системе Base 2, в которой работает компьютер. Когда вы выполняете математику для этих повторяющихся десятичных знаков,

С https://0.30000000000000004.com/

Влад Агурец
источник
3

Десятичные числа, такие как 0.1, 0.2и 0.3не представлены точно в двоично-кодированных типах с плавающей запятой. Сумма аппроксимаций для 0.1и 0.2отличается от аппроксимации, использованной для 0.3, следовательно, ложь, 0.1 + 0.2 == 0.3как можно увидеть здесь более четко:

#include <stdio.h>

int main() {
    printf("0.1 + 0.2 == 0.3 is %s\n", 0.1 + 0.2 == 0.3 ? "true" : "false");
    printf("0.1 is %.23f\n", 0.1);
    printf("0.2 is %.23f\n", 0.2);
    printf("0.1 + 0.2 is %.23f\n", 0.1 + 0.2);
    printf("0.3 is %.23f\n", 0.3);
    printf("0.3 - (0.1 + 0.2) is %g\n", 0.3 - (0.1 + 0.2));
    return 0;
}

Вывод:

0.1 + 0.2 == 0.3 is false
0.1 is 0.10000000000000000555112
0.2 is 0.20000000000000001110223
0.1 + 0.2 is 0.30000000000000004440892
0.3 is 0.29999999999999998889777
0.3 - (0.1 + 0.2) is -5.55112e-17

Чтобы эти вычисления были оценены более надежно, вам необходимо использовать десятичное представление для значений с плавающей запятой. Стандарт C не определяет такие типы по умолчанию, но как расширение, описанное в техническом отчете .

В _Decimal32, _Decimal64и _Decimal128типы могут быть доступны в вашей системе (например, GCC поддерживает их на выбранные цели , но Clang не поддерживает их на OS X ).

chqrlie
источник
1

Math.sum (javascript) .... вид замены оператора

.1 + .0001 + -.1 --> 0.00010000000000000286
Math.sum(.1 , .0001, -.1) --> 0.0001

Object.defineProperties(Math, {
    sign: {
        value: function (x) {
            return x ? x < 0 ? -1 : 1 : 0;
            }
        },
    precision: {
        value: function (value, precision, type) {
            var v = parseFloat(value), 
                p = Math.max(precision, 0) || 0, 
                t = type || 'round';
            return (Math[t](v * Math.pow(10, p)) / Math.pow(10, p)).toFixed(p);
        }
    },
    scientific_to_num: {  // this is from https://gist.github.com/jiggzson
        value: function (num) {
            //if the number is in scientific notation remove it
            if (/e/i.test(num)) {
                var zero = '0',
                        parts = String(num).toLowerCase().split('e'), //split into coeff and exponent
                        e = parts.pop(), //store the exponential part
                        l = Math.abs(e), //get the number of zeros
                        sign = e / l,
                        coeff_array = parts[0].split('.');
                if (sign === -1) {
                    num = zero + '.' + new Array(l).join(zero) + coeff_array.join('');
                } else {
                    var dec = coeff_array[1];
                    if (dec)
                        l = l - dec.length;
                    num = coeff_array.join('') + new Array(l + 1).join(zero);
                }
            }
            return num;
         }
     }
    get_precision: {
        value: function (number) {
            var arr = Math.scientific_to_num((number + "")).split(".");
            return arr[1] ? arr[1].length : 0;
        }
    },
    sum: {
        value: function () {
            var prec = 0, sum = 0;
            for (var i = 0; i < arguments.length; i++) {
                prec = this.max(prec, this.get_precision(arguments[i]));
                sum += +arguments[i]; // force float to convert strings to number
            }
            return Math.precision(sum, prec);
        }
    }
});

идея состоит в том, чтобы использовать Math вместо операторов, чтобы избежать ошибок с плавающей точкой

Math.sum автоматически определяет точность использования

Math.sum принимает любое количество аргументов

bortunac
источник
1
Я не уверен, что вы ответили на вопрос: « Почему происходят эти неточности? ».
Вай Ха Ли
в некотором смысле вы правы, но я пришел сюда из странного поведения javascript в отношении этой проблемы ... я просто хочу поделиться своего рода решением
bortunac
Вы все еще не отвечаете на вопрос, хотя.
Вай Ха Ли
k у вас есть проблема с этим ... скажите мне, куда его переместить или если вы настаиваете, я могу просто удалить его
bortunac
0

Я только что увидел эту интересную проблему с плавающей точкой:

Рассмотрим следующие результаты:

error = (2**53+1) - int(float(2**53+1))
>>> (2**53+1) - int(float(2**53+1))
1

Мы можем четко видеть точку останова, когда 2**53+1- все работает нормально, пока 2**53.

>>> (2**53) - int(float(2**53))
0

Введите описание изображения здесь

Это происходит из-за двоичного двоичного формата IEEE 754 с двойной точностью двоичного формата: двоичная64

Со страницы Википедии для формата с плавающей запятой двойной точности :

Бинарная с плавающей запятой двойной точности является широко используемым форматом на ПК благодаря более широкому диапазону с плавающей запятой одинарной точности, несмотря на его производительность и стоимость полосы пропускания. Как и в формате с плавающей запятой одинарной точности, ему не хватает точности для целых чисел по сравнению с целочисленным форматом того же размера. Это обычно называют просто двойной. Стандарт IEEE 754 определяет двоичный код 64 как имеющий:

  • Бит знака: 1 бит
  • Экспонент: 11 бит
  • Значительная точность: 53 бита (52 явно хранятся)

Введите описание изображения здесь

Реальное значение, принятое данным 64-битным датумом двойной точности с заданным смещенным показателем и 52-битной дробью, равно

Введите описание изображения здесь

или

Введите описание изображения здесь

Спасибо @a_guest за указание на это мне.

costargc
источник
-1

Другой вопрос был назван как дубликат этого:

В C ++, почему результат cout << xотличается от значения, для которого отображается отладчик x?

В xвопросе это floatпеременная.

Одним из примеров будет

float x = 9.9F;

Отладчик показывает 9.89999962, вывод coutоперации есть 9.9.

Получается, что ответом является coutточность по умолчанию для float6, поэтому он округляется до 6 десятичных цифр.

Смотрите здесь для справки


источник
1
ИМО - размещение этого здесь было неправильным подходом. Я знаю, что это расстраивает, но люди, которым нужен ответ на оригинальный вопрос (очевидно, теперь удаленный!), Не найдут его здесь. Если вы действительно чувствуете, что ваша работа заслуживает сохранения, я бы посоветовал: 1) найти другой вопрос, на который он действительно отвечает, 2) создать вопрос с самостоятельным ответом.
Стивен С