Рассмотрим следующий код:
0.1 + 0.2 == 0.3 -> false
0.1 + 0.2 -> 0.30000000000000004
Почему эти неточности случаются?
Рассмотрим следующий код:
0.1 + 0.2 == 0.3 -> false
0.1 + 0.2 -> 0.30000000000000004
Почему эти неточности случаются?
Двоичная математика с плавающей запятой выглядит следующим образом. В большинстве языков программирования он основан на стандарте IEEE 754. JavaScript использует 64-битное представление с плавающей запятой, такое же, как Java double
. Суть проблемы заключается в том, что числа представлены в этом формате как целое число, умноженное на два; рациональные числа (такие как 0.1
, то есть 1/10
), знаменатель которых не является степенью двойки, не могут быть точно представлены.
Для 0.1
в стандартном формате binary64
представление может быть записано точно как
0.1000000000000000055511151231257827021181583404541015625
в десятичном виде или0x1.999999999999ap-4
в шестигранной нотации C99.Напротив, рациональное число 0.1
, которое является 1/10
, может быть записано точно как
0.1
в десятичном виде или0x1.99999999999999...p-4
в аналоге шестигранной нотации C99, где ...
представляет бесконечную последовательность из 9-ти.Константы 0.2
и 0.3
в вашей программе также будут приблизительными к их истинным значениям. Бывает, что ближайший double
к 0.2
больше рационального числа 0.2
, но что ближайший double
к 0.3
меньше рационального числа 0.3
. Сумма 0.1
и 0.2
оказывается больше рационального числа 0.3
и, следовательно, не согласуется с константой в вашем коде.
Довольно комплексное решение арифметических задач с плавающей точкой - это Что каждый компьютерный специалист должен знать об арифметике с плавающей точкой. Более простое объяснение см. в разделе floatingpoint-gui.de.
Примечание: Все позиционные (базовые N) системы счисления точно решают эту проблему
Обычные старые десятичные числа (основание 10) имеют те же проблемы, поэтому такие числа, как 1/3, заканчиваются на 0,333333333...
Вы только что наткнулись на число (3/10), которое легко представить с помощью десятичной системы, но не соответствует двоичной системе. Он также идет в обе стороны (в некоторой степени): 1/16 - это уродливое десятичное число (0,0625), но в двоичном виде оно выглядит так же аккуратно, как 10 000-е в десятичном (0,0001) ** - если бы мы были в Привычка использовать систему счисления с базой 2 в нашей повседневной жизни - вы даже посмотрите на это число и инстинктивно поймете, что можете прийти туда, вдвое уменьшив что-то вдвое, снова и снова и снова.
** Конечно, это не совсем то, как числа с плавающей точкой хранятся в памяти (они используют форму научной записи). Тем не менее, это иллюстрирует тот факт, что двоичные ошибки точности с плавающей точкой имеют тенденцию возникать, потому что числа "реального мира", с которыми мы обычно заинтересованы работать, часто имеют степень десяти - но только потому, что мы используем десятичную систему счисления день - сегодня. По этой же причине мы будем говорить такие вещи, как 71% вместо "5 из каждых 7" (71% - это приблизительное значение, поскольку 5/7 нельзя точно представить ни одним десятичным числом).
Так что нет: двоичные числа с плавающей запятой не ломаются, они просто такие же несовершенные, как и любая другая система счисления с базовым N :)
Примечание: Работа с плавающими в программировании
На практике эта проблема точности означает, что вам нужно использовать функции округления для округления чисел с плавающей запятой до скольких интересующих вас десятичных разрядов, прежде чем вы их отобразите.
Вам также необходимо заменить тесты на равенство сравнениями, которые допускают некоторую толерантность, что означает:
Не делай if (float1 == float2) { ... }
Вместо этого сделайте if (Math.Abs(float1 - float2) < myToleranceValue) { ... }
.
myToleranceValue должен быть выбран для вашего конкретного приложения - и он будет во многом зависеть от того, сколько "места для маневра" вы готовы предоставить, и какое наибольшее число вы собираетесь сравнивать (из-за потери точности вопросы). Остерегайтесь констант стиля "double.Epsilon" на выбранном вами языке (Number.EPSILON в Javascript). Они не должны использоваться в качестве значений допуска.
Я считаю, что я должен добавить перспективу аппаратных дизайнеров, поскольку я проектирую и строю аппаратное обеспечение с плавающей точкой. Знание причины ошибки может помочь понять, что происходит в программном обеспечении, и в конечном итоге я надеюсь, что это поможет объяснить причины возникновения ошибок с плавающей запятой и, похоже, накапливаться с течением времени.
С технической точки зрения большинство операций с плавающей запятой будут иметь некоторый элемент ошибки, поскольку аппаратное обеспечение, выполняющее вычисления с плавающей запятой, требуется только для того, чтобы иметь ошибку менее половины одной единицы на последнем месте. Поэтому многие аппаратные средства будут останавливаться с точностью, которая необходима только для того, чтобы в последнем месте была допущена ошибка менее половины одной единицы за одну операцию, что особенно проблематично в делении с плавающей запятой. То, что составляет одну операцию, зависит от количества операндов, которые принимает единица. Для большинства это два, но некоторые единицы принимают 3 или более операндов. Из-за этого нет гарантии, что повторные операции приведут к желательной ошибке, поскольку ошибки со временем складываются.
Большинство процессоров следуют стандарту IEEE-754, но некоторые используют денормализованные или разные стандарты , Например, в IEEE-754 существует денормализованный режим, который позволяет отображать очень маленькие числа с плавающей запятой за счет точности. Однако следующее будет охватывать нормализованный режим IEEE-754, который является типичным режимом работы.
В стандарте IEEE-754 разработчикам аппаратного обеспечения допускается любое значение ошибки/эпсилон, если оно занимает менее половины одной единицы на последнем месте, и результат должен быть меньше половины одной единицы в последнем месте за одну операцию. Это объясняет, почему при повторных операциях ошибки складываются. Для двойной точности IEEE-754 это 54-й бит, поскольку 53 бита используются для представления числовой части (нормализованной), также называемой мантиссой, числа с плавающей запятой (например, 5.3 в 5.3e5). В следующих разделах более подробно рассматриваются причины аппаратной ошибки для различных операций с плавающей запятой.
Основная причина ошибки в делении с плавающей запятой - алгоритмы деления, используемые для вычисления частного. Большинство компьютерных систем вычисляют деление с использованием умножения на обратное, главным образом в Z=X/Y
, Z = X * (1/Y)
. Разделение вычисляется итеративно, т.е. Каждый цикл вычисляет некоторые биты частного, пока не будет достигнута желаемая точность, что для IEEE-754 - это что-либо с ошибкой менее одной единицы на последнем месте. Таблица обратных значений Y (1/Y) известна как таблица выбора коэффициентов (QST) в медленном делении, а размер в битах таблицы выбора факторов обычно равен ширине основани или количеству битов фактор, вычисленный на каждой итерации, плюс несколько защитных бит. Для стандарта IEEE-754 с двойной точностью (64-разрядной) это будет размер радиуса делителя плюс несколько защитных бит k, где k>=2
. Так, например, типичная таблица выбора коэффициентов для делителя, который вычисляет 2 бита фактора за раз (radix 4), будет битами 2+2= 4
(плюс несколько дополнительных бит).
3.1 Ошибка округления округления: приближение взаимного
Какие обратные в таблице выбора факторов зависят от метода деления : медленное деление, такое как разделение СТО, или быстрое деление, такое как дивизия Гольдшмидта; каждая запись модифицируется в соответствии с алгоритмом деления в попытке получить наименьшую возможную ошибку. Однако в любом случае все обратные являются приближенными к фактическим взаимным и вводят некоторый элемент ошибки. Оба метода медленного деления и быстрого деления вычисляют коэффициент итеративно, т.е. Вычисляется каждый бит бита каждого шага, затем результат вычитается из дивиденда, а делитель повторяет этапы, пока ошибка не станет меньше половины одной единица на последнем месте. Методы медленного деления вычисляют фиксированное количество цифр частного на каждом шаге и обычно дешевле строить, а быстрые методы деления вычисляют переменное количество цифр на каждый шаг и обычно более дороги для сборки. Наиболее важная часть методов деления состоит в том, что большинство из них полагаются на повторное умножение на аппроксимацию обратного, поэтому они подвержены ошибкам.
Другая причина ошибок округления во всех операциях - это разные способы усечения окончательного ответа, который допускает IEEE-754. Там усекаются, округляются до нуля, округлые до ближайшего (по умолчанию), округлые и округлые. Все методы вводят элемент ошибки менее одной единицы в последнем месте для одной операции. Со временем и повторяющимися операциями усечение также добавляет кумулятивно к полученной ошибке. Эта ошибка усечения особенно проблематична в возведении в степень, что связано с некоторой формой повторного умножения.
Так как аппаратное обеспечение, которое выполняет вычисления с плавающей запятой, должно давать результат с ошибкой менее половины одной единицы в последнем месте за одну операцию, ошибка будет увеличиваться по сравнению с повторяющимися операциями, если не смотреть. Это связано с тем, что в вычислениях, требующих ограниченной ошибки, математики используют такие методы, как использование четной цифры на последнем месте IEEE-754, поскольку со временем ошибки с большей вероятностью будут отменять друг друга, а Interval Arithmetic в сочетании с вариациями IEEE 754, чтобы предсказать ошибки округления и исправить их. Из-за его низкой относительной погрешности по сравнению с другими режимами округления округленная до ближайшей четной цифры (в последнем месте) является стандартным режимом округления IEEE-754.
Обратите внимание, что режим округления по умолчанию, округленная до ближайшей четная цифра на последнем месте, гарантирует ошибку менее половины одна единица в последнем месте за одну операцию. Использование обрезания, округления и округления в одиночку может привести к ошибке, которая превышает половину одной единицы в последнем месте, но меньше единицы в последнем месте, поэтому эти режимы не рекомендуются, если они не являются используется в интервальной арифметике.
Короче говоря, основной причиной ошибок в операциях с плавающей запятой является комбинация усечения в аппаратном обеспечении и усечение обратной в случае деления. Поскольку для стандарта IEEE-754 требуется только одна половина одной единицы в одном месте за одну операцию, ошибки с плавающей запятой по повторяющимся операциям будут добавлены, если не будут исправлены.
Когда вы конвертируете .1 или 1/10 в base 2 (двоичный), вы получаете повторяющийся шаблон после десятичной точки, точно так же, как пытаетесь представить 1/3 в базе 10. Значение не является точным, и поэтому вы можете 'делать точную математику с ним, используя обычные методы с плавающей запятой.
Большинство ответов здесь затрагивают этот вопрос в очень сухих технических терминах. Я хотел бы остановиться на этом в терминах, которые могут понять обычные люди.
Представьте, что вы пытаетесь нарезать пиццу. У вас есть роботизированный нож для пиццы, который может разрезать кусочки пиццы ровно пополам. Он может вдвое сократить целую пиццу, или он может сократить вдвое существующий срез, но в любом случае, сокращение пополам всегда точное.
У пиццы есть очень тонкие движения, и если вы начнете с целой пиццы, то уменьшите ее вдвое и продолжите вдвое меньший кусочек каждый раз, вы можете сделать половину 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. Некоторые языки программирования также предоставляют резцы для пиццы, которые могут разделять фрагменты на точные десятки. Хотя такие резаки для пиццы необычны, если у вас есть доступ к одному, вы должны использовать его, когда важно иметь ровно одну десятую или одну пятую части среза.
Ошибки округления с плавающей запятой. 0,1 не могут быть представлены точно в базе-2, как в базе-10, из-за недостающего простого коэффициента 5. Так же, как 1/3 принимает бесконечное число цифр для представления в десятичной форме, но составляет "0,1" в базе-3, 0.1 принимает бесконечное число цифр в базе-2, где оно не находится в базе-10. И компьютеры не имеют бесконечного объема памяти.
В дополнение к другим правильным ответам вы можете рассмотреть возможность масштабирования ваших значений, чтобы избежать проблем с арифметикой с плавающей запятой.
Например:
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) ).
Мой ответ довольно длинный, поэтому я разделил его на три части. Поскольку вопрос касается математики с плавающей точкой, я сделал упор на том, что на самом деле делает машина. Я также определил двойную (64-битную) точность, но аргумент одинаково применим к любой арифметике с плавающей запятой.
преамбула
Число двоичного формата с плавающей запятой IEEE 754 (двоичное число 64) представляет собой число в форме
value = (-1) ^ s * (1.m 51 m 50... m 2 m 1 m 0) 2 * 2 e -1 023
в 64 битах:
1
если число отрицательное, 0
противном случае 1.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 -1 023 - 51 (≈ 4.94 * 10 -324) и d max (наибольшее денормальное число, для которого мантисса целиком состоит из 1
с) 2 -1 023 + 1–2 -1 023–51 (≈ 2,225 * 10 -308).
Превращение числа с двойной точностью в двоичное
Существует много онлайн-конвертеров для преобразования числа с плавающей запятой двойной точности в двоичное (например, на сайте binaryconvert.com), но здесь приведен пример кода С# для получения представления 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 -5 4.
Двоичное представление 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 -5 4 что составляет ~ 5.5511151231258 × 10 -1 7 - незначительно (для многих приложений) по сравнению с исходными значениями.
Сравнение последних нескольких битов числа с плавающей запятой по своей природе опасно, и любой, кто прочитает знаменитую статью " Что должен знать каждый компьютерный специалист об арифметике с плавающей запятой " (которая охватывает все основные части этого ответа).
Большинство калькуляторов используют дополнительные защитные цифры, чтобы обойти эту проблему, поэтому 0.1 + 0.2
дало бы 0.3
: последние несколько бит округляются.
Числа с плавающей запятой, хранящиеся в компьютере, состоят из двух частей: целого и экспоненты, в которых база берется и умножается на целую часть.
Если компьютер работал в базе 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
is 1 x 2⁻¹
и 0.25
is 1 x 2⁻²
, и добавить их приводит к 3 x 2⁻²
, или 0.75
. Точно.
Проблема связана с числами, которые могут быть представлены точно в базе 10, но не в базе 2. Эти цифры должны быть округлены до их ближайшего эквивалента. Предполагая, что формат 64-битного формата с плавающей точкой IEEE очень распространен, самым близким к 0.1
является 3602879701896397 x 2⁻⁵⁵
, а ближайшим числом до 0.2
является 7205759403792794 x 2⁻⁵⁵
; добавив их вместе в результат 10808639105689191 x 2⁻⁵⁵
или точное десятичное значение 0.3000000000000000444089209850062616169452667236328125
. Номера с плавающей запятой, как правило, округлены для отображения.
Решение, чтобы убрать неприглядное переполнение,
function strip(number) {
return (parseFloat(number.toPrecision(12)));
}
Использование toPrecision(12)
оставляет конечные нули, которые parseFloat()
удаляет. Предположим, что он имеет точность до плюс/минус на наименьшей значащей цифре.
Ошибка округления точки с плавающей запятой. Из Что каждый компьютерный ученый должен знать о арифметике с плавающей точкой:
Сжатие бесконечного числа действительных чисел в конечное число бит требует приблизительного представления. Несмотря на то, что в большинстве программ имеется бесконечно много целых чисел, результат целочисленных вычислений может быть сохранен в 32 битах. В отличие от этого, учитывая любое фиксированное количество бит, большинство вычислений с реальными числами будут давать количества, которые не могут быть точно представлены с использованием этого количества бит. Поэтому результат вычисления с плавающей запятой часто должен быть округлен, чтобы соответствовать его конечному представлению. Эта ошибка округления является характерной особенностью вычисления с плавающей запятой.
Мое обходное решение:
function add(a, b, precision) {
var x = Math.pow(10, precision || 2);
return (Math.round(a * x) + Math.round(b * x)) / x;
}
точность относится к числу цифр, которые вы хотите сохранить после десятичной точки во время добавления.
Было опубликовано много хороших ответов, но я хотел бы добавить еще один.
Не все числа могут быть представлены с помощью поплавков/ удваивается Например, число "0,2" будет представлено как "0.200000003" в единой точности в стандарте по плавающей точке IEEE754.
Модель для хранения действительных чисел под капотом представляет собой число с плавающей запятой как
Если вы легко можете набрать 0.2
, FLT_RADIX
и DBL_RADIX
равно 2; не 10 для компьютера с FPU, который использует "стандарт IEEE для двоичной арифметики с плавающей точкой (ISO/IEEE Std 754-1985)".
Поэтому немного сложно представить такие числа. Даже если вы укажете эту переменную явно без какого-либо промежуточного вычисления.
Некоторые статистические данные, связанные с этим известным вопросом с двойной точностью.
При добавлении всех значений (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, когда точность имеет большое значение. С 2 десятичными цифрами (шаг 0,01) ситуация несколько ухудшается (18% и 36%).
Резюме
Арифметика с плавающей запятой точна, к сожалению, она не очень хорошо сочетается с нашим обычным представлением числа base-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.
Как это случилось?
Когда мы пишем в десятичной форме, каждая дробь (в частности, каждое конечное десятичное число) является рациональным числом формы
a/(2 n x 5 м)
В двоичном случае мы получаем только 2 n- член, то есть:
a/2 n
Таким образом, в десятичной системе, мы не можем представить 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.
Таким образом, хотя каждая двоичная дробь может быть записана в десятичной форме, обратное неверно. И на самом деле большинство десятичных дробей повторяются в двоичном формате.
Работа с ним
Обычно разработчикам рекомендуется выполнять сравнения, лучше советовать округлять до целых значений (в библиотеке C: round() и roundf(), т.е. Оставаться в формате FP), а затем сравнивать. Округление до определенной длины десятичной доли решает большинство проблем с выходом.
Кроме того, при реальных проблемах с хрустом (проблемы, которые FP был изобретен на ранних, ужасно дорогих компьютерах), физические константы Вселенной и все другие измерения известны только относительно небольшому числу значимых цифр, поэтому все проблемное пространство был "неточным" в любом случае. FP "точность" не является проблемой в этом виде применения.
Вся проблема действительно возникает, когда люди пытаются использовать FP для подсчета бобов. Это работает для этого, но только если вы придерживаетесь интегральных значений, какой вид поражает смысл его использования. Вот почему у нас есть все эти библиотеки программного обеспечения с десятичной дроби.
Мне нравится ответ "Пицца" Криса, потому что он описывает реальную проблему, а не просто обычную ручную работу о "неточности". Если бы FP были просто "неточными", мы могли бы исправить это и сделали бы это несколько десятилетий назад. Причина, по которой у нас нет, заключается в том, что формат FP компактен и быстр, и это лучший способ хрустить множество чисел. Кроме того, это наследие космической эры и гонки вооружений и ранние попытки решить большие проблемы с очень медленными компьютерами с использованием небольших систем памяти. (Иногда отдельные магнитные сердечники для 1-битного хранения, но это другая история.)
Заключение
Если вы просто считаете beans в банке, программные решения, которые используют представления десятичной строки, в первую очередь работают отлично. Но вы не можете делать квантовую хромодинамику или аэродинамику таким образом.
Вы пытались решить проблему с клейкой лентой?
Попробуйте определить, когда возникают ошибки, и исправить их с помощью коротких операторов if, это не очень хорошо, но для некоторых проблем это единственное решение, и это один из них.
if( (n * 0.1) < 100.0 ) { return n * 0.1 - 0.000000000000001 ;}
else { return n * 0.1 + 0.000000000000001 ;}
У меня была такая же проблема в проекте научного моделирования в С#, и я могу сказать вам, что если вы проигнорируете эффект бабочки, он превратится в большого толстого дракона и укусит вас в **
Эти странные цифры появляются из-за того, что компьютеры используют двоичную (базовую 2) систему подсчета, в то время как мы используем десятичную (базовую 10).
Есть большинство дробных чисел, которые не могут быть представлены точно либо в двоичном, либо в десятичном, либо в обоих. Результат - округленное (но точное) число результатов.
Чтобы предложить лучшее решение, я могу сказать, что обнаружил следующий метод:
parseFloat((0.1 + 0.2).toFixed(10)) => Will return 0.3
Позвольте мне объяснить, почему это лучшее решение. Как и другие, упомянутые в ответах выше, было бы неплохо использовать готовую функцию Javascript toFixed() для решения проблемы. Но, скорее всего, вы столкнетесь с некоторыми проблемами.
Представьте, что вы собираетесь сложить два числа с плавающей точкой, такие как 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? Это время, чтобы преобразовать его в плавающее, чтобы сделать это, как вы хотите:
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>
Многие из этого вопроса многочисленные дубликаты спрашивают о влиянии округления с плавающей запятой на определенные числа. На практике легче понять, как это работает, глядя на точные результаты вычислений, а не просто на чтение. Некоторые языки предоставляют способы сделать это - например, преобразование a float
или double
в BigDecimal
в Java.
Так как это языковой агностический вопрос, ему нужны языковые агностические инструменты, такие как Decimal to Floating-Point Converter.
Применяя его к номерам в вопросе, рассматриваемым как двойные:
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, который заканчивается четной цифрой и, следовательно, является правильным результатом.
Учитывая, что никто не упомянул об этом...
Некоторые языки высокого уровня, такие как Python и Java, поставляются с инструментами для преодоления двоичных ограничений с плавающей запятой. Например:
Python decimal
module и Java BigDecimal
class, которые представляют числа внутри с десятичной нотацией (в отличие от двоичной нотации). Оба имеют ограниченную точность, поэтому они все еще подвержены ошибкам, однако они решают наиболее распространенные проблемы с бинарной арифметикой с плавающей запятой.
Десятичные числа очень хороши при работе с деньгами: десять центов плюс двадцать центов всегда равны тридцати центам:
>>> 0.1 + 0.2 == 0.3
False
>>> Decimal('0.1') + Decimal('0.2') == Decimal('0.3')
True
Модуль Python decimal
основан на стандарте IEEE 854-1987.
Python fractions
module и Apache Common BigFraction
класс. Оба представляют рациональные числа как пары (numerator, denominator)
, и они могут давать более точные результаты, чем десятичная арифметика с плавающей запятой.
Ни одно из этих решений не является совершенным (особенно, если мы смотрим на производительность, или если нам требуется очень высокая точность), но все же они решают большое количество проблем с двоичной арифметикой с плавающей запятой.
Могу я просто добавить; люди всегда предполагают, что это проблема с компьютером, но если вы считаете своими руками (база 10), вы не можете получить (1/3+1/3=2/3)=true
, если у вас нет бесконечности, чтобы добавить 0,3333... 0,333... так, как с (1/10+2/10)!==3/10
проблема в базе 2, вы усекаете ее до 0.333 + 0.333 = 0.666 и, вероятно, округлите ее до 0.667, что также будет технически неточным.
Граф в тройном, а третья - не проблема, хотя - может быть, какая-то раса с 15 пальцами на каждой руке спросит, почему ваша десятичная математика была сломана...
Я нашел решение, которое вы можете использовать эту функцию для правильного анализа поплавков также вы можете установить свою собственную точность
function getFloat(int) {
var num = new Number(int);
return parseFloat(num.toPrecision(2));
}
Все номера в JavaScript представлены в двоичном формате как IEEE-754 Doubles, который обеспечивает точность примерно до 14 или 15 значащих цифр. Поскольку они числа с плавающей запятой, они не всегда точно представляют действительные числа, включая дроби.
Вид математики с плавающей запятой, который может быть реализован на цифровом компьютере, обязательно использует приближение действительных чисел и операций над ними. (Стандартная версия работает до более чем пятидесяти страниц документации и имеет комитет, чтобы справиться со своими ошибками и дальнейшей доработкой.)
Это приближение представляет собой смесь приближений разного вида, каждый из которых можно либо игнорировать, либо тщательно учитывать из-за его специфического способа отклонения от точности. Это также включает в себя ряд явных исключительных случаев как на уровне аппаратного обеспечения, так и на уровне программного обеспечения, которое большинство людей проходит в прошлом, делая вид, что не замечает.
Если вам нужна бесконечная точность (например, число π, а не одно из его более коротких резервных копий), вы должны написать или использовать символическую математическую программу.
Но если вы понимаете, что иногда математика с плавающей запятой нечеткая по значению и логике, и ошибки могут быстро накапливаться, и вы можете написать свои требования и тесты, чтобы это сделать, то ваш код может часто получаться с чем в вашем FPU.
Просто для удовольствия я играл с представлением поплавков, следуя определениям из Стандарта C99, и я написал код ниже.
Код печатает двоичное представление поплавков в трех выделенных группах
SIGN EXPONENT FRACTION
и после этого он печатает сумму, которая при суммировании с достаточной точностью покажет значение, которое действительно существует в аппаратном обеспечении.
Поэтому, когда вы пишете float x = 999...
, компилятор преобразует это число в битовое представление, напечатанное функцией xx
, так что сумма, напечатанная функцией yy
, будет равна заданному числу.
В действительности эта сумма является лишь приближением. Для номера 999,999,999 компилятор будет вставлять в битовое представление float номер 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);
}
Вот сеанс консоли, в котором я вычисляю действительное значение float, которое существует в аппаратном обеспечении. Я использовал 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.
Короче говоря коротко...
Для тех, кто использует Java и имеет такие проблемы: Используйте класс BigDecimal
.
Еще один способ взглянуть на это: Используются 64 бита для представления чисел. Как следствие, не может быть представлено более 2 ** 64 = 18 446 744 073 709 551 616 различных чисел.
Тем не менее, Math говорит, что существует уже бесконечное число десятичных знаков между 0 и 1. IEE 754 определяет кодировку для эффективного использования этих 64 бит для гораздо большего количества пробелов плюс NaN и +/- Infinity, поэтому между четко представленными числа, заполненные числами, только приближены.
К сожалению, 0,3 сидит в промежутке.
Поскольку этот поток немного разветкился в общем обсуждении текущих реализаций с плавающей запятой, я бы добавил, что есть проекты по устранению их проблем.
Посмотрите https://posithub.org/, в котором демонстрируется тип номера, называемый posit (и его предшественника unum), чтобы promises предлагать лучшую точность с меньшим количеством бит. Если мое понимание правильное, оно также фиксирует проблемы в вопросе. Весьма интересный проект, человек, стоящий за ним, является математиком. Dr. Джон Густафсон. Все это с открытым исходным кодом, с множеством реализаций в C/С++, Python, Julia и С# (https://hastlayer. ком/арифметика).
Для тех, кто читает этот поток, стремясь получить точность до определенного количества десятичных знаков, а не цифр, вместо num.toPrecision(2) вы можете использовать num.toFixed(2 ).
Я взял это из документации PHP:
http://php.net/manual/en/language.types.float.php#language.types.float.comparison
Чтобы проверить значения с плавающей запятой для равенства, верхняя граница используется относительная ошибка из-за округления. Это значение известно как машинный эпсилон или округлое округление, и является наименьшим приемлемым разница в расчетах.
$a и $b равны 5 цифрам точности.
<?php
$a = 1.23456789;
$b = 1.23456780;
$epsilon = 0.00001;
if(abs($a-$b) < $epsilon) {
echo "true";
}
Почти все объяснили и правильно ответили с разных точек зрения о том, почему это происходит. Если вам когда-нибудь понадобится работать с десятичными знаками с помощью Javascript и не попадать в эти проблемы, вы можете использовать модуль Big.js: https://github.com/MikeMcl/big.js. Он позволяет выполнять операции с произвольная точность десятичной арифметики.
var Big = require('big.js');
x = Big(.1);
console.log(x.plus(.2).toString());
Результат возвращает 0.3