Математика с плавающей запятой сломана?

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

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

почему эти неточности произошло?

27 ответов


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

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

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

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

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

довольно всеобъемлющее рассмотрение арифметических вопросов с плавающей запятой-это Что Каждый Компьютерщик Должен Знать Об Арифметике С Плавающей Запятой. Для более легкого для переваривания объяснения см. floating-point-gui.de.


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

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

1. Обзор

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

2. Стандарты

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

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

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

основной причиной ошибки в делении с плавающей запятой являются алгоритмы деления, используемые для вычисления частного. Наиболее вычислительные системы вычисляют деление с помощью умножения на обратное, в основном в 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 ошибка округления деления: приближение Ответный

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

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

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

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

поскольку аппаратное обеспечение, которое выполняет вычисления с плавающей запятой, должно дайте результат с ошибкой менее половины одной единицы в последнем месте для одной операции, ошибка будет расти в течение повторных операций, если не смотреть. Именно по этой причине в вычислениях, требующих ограниченной ошибки, математики используют такие методы, как использование round-to-nearest четная цифра на последнем месте IEEE-754, потому что со временем ошибки с большей вероятностью будут отменять друг друга, и Интервальной Арифметики совмещенный с изменениями из IEEE 754 режимы округления предсказать ошибки округления и исправить их. Из-за своей низкой относительной ошибки по сравнению с другими режимами округления, округление до ближайшей четной цифры (в последнем месте), является режимом округления по умолчанию IEEE-754.

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

6. Резюме

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


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


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

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

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

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


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

для чисел двойн-точности (которая точность которая позволяет вам уменьшить вдвое вашу пиццу 53 раза), номера немедленно меньше и больше чем 0.1 0.09999999999999991673327315332594682276248931884765625 и 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.29999999999999999988897769753748434595763683319091796875.


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

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


ошибки округления с плавающей запятой. 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: Хорошие Части: приложение а-ужасные части (стр. 105).


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

преамбула

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

value = (-1)^s * (1.м51m50...м2m1m0)2 * 2e-1023

в 64 бита:

  • Первый БИТ знаковый бит: 1 если число отрицательное, 0 иначе1.
  • следующие 11 бит-это показатель, которым is смещение по 1023. Другими словами, после считывания разрядов экспоненты из числа с двойной точностью необходимо вычесть 1023 для получения степени два.
  • остальные 52 бита-это significand (или мантисса). В мантиссе "подразумевается"!--11--> всегда2 опущено, так как наиболее значительным битом любого двоичного значения является 1.

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

2 - это не относится к цифры Донормила, которые имеют показатель смещения нуля (и подразумеваемый 0.). Диапазон denormal double точность чисел dмин ≤ / x / ≤ dМакс, где dмин (наименьшее представимое ненулевое число) 2-1023 - 51 (≈ 4.94 * 10-324) и dМакс (крупнейший Донормила число, для которого мантисса состоит из 1S) является 2-1023 + 1 - 2-1023 - 51 (≈ 2.225 * 10-308).


поворачивать двойную точность число в двоичный

многие онлайн-конвертеры существуют для преобразования числа с плавающей запятой двойной точности в двоичный (например, at 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 версия)

Катон Джонстон (вопрос asker) спросил, почему 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 не могут быть представлены в двоичном формате точно в a конечно количество двоичных битов более 1/9, 1/3 или 1/7 может быть представлено точно в цифры.

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

0.1 = 2^-4 * [1].1001100110011001100110011001100110011001100110011010
0.2 = 2^-3 * [1].1001100110011001100110011001100110011001100110011010

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

0.1 = 2^-3 *  0.1100110011001100110011001100110011001100110011001101(0)
0.2 = 2^-3 *  1.1001100110011001100110011001100110011001100110011010
sum = 2^-3 * 10.0110011001100110011001100110011001100110011001100111

так как сумма не имеет формы 2n * 1.{bbb} мы увеличиваем экспоненты и сдвиг десятичной (бинарные) пункт сделать:

sum = 2^-2 * 1.0011001100110011001100110011001100110011001100110011(1)

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

a = 2^-2 * 1.0011001100110011001100110011001100110011001100110011
x = 2^-2 * 1.0011001100110011001100110011001100110011001100110011(1)
b = 2^-2 * 1.0011001100110011001100110011001100110011001100110100

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

sum = 2^-2 * 1.0011001100110011001100110011001100110011001100110100

TL; DR

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

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

преобразуется обратно в decimal, эти значения:

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

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

сравнение последних нескольких битов числа с плавающей запятой по своей сути опасно, так как любой, кто читает знаменитый"О Чем Должен Знать Каждый Компьютерщик Арифметика С Плавающей Запятой " (который охватывает все основные части этого ответа) будет знать.

большинство калькуляторов используют дополнительный охранник цифры чтобы обойти эту проблему, которая заключается в том, как 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 и 1 x 2⁻¹ и 0.25 и 1 x 2⁻², и добавление их приводит к 3 x 2⁻² или 0.75. Именно так.

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


ошибка округления с плавающей запятой. От Что Каждый Компьютерщик Должен Знать Об Арифметике С Плавающей Запятой:

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

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

enter image description here

хотя можно типа 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%).


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

резюме

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

даже простые цифры 0.01, 0.02, 0.03, 0.04 ... 0.24 не представимы точно в двоичной десятые доли. Если вы считаете 0.01, .02, .03 ..., не до тех пор, пока вы не дойдете до 0,25, вы получите первую дробь, представимую в base2. Если бы вы попробовали использовать FP, ваш 0.01 был бы немного выключен, поэтому единственный способ добавить 25 из них до хорошего точного 0.25 потребовал бы длинной цепочки причинности, включающей биты защиты и округление. Это трудно предсказать, поэтому мы поднимаем руки и говорим "FP неточен", но это не совсем так.

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

как это произошло?

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

           a / (2n x 5m)

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

a / 2n

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

базовый10 мы не можем представлять 1/3. Но в двоичном формате мы не можем сделать 1/10или 1/3.

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

разбираться с этим

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

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

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

вывод

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


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

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

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

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


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

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


могу я просто добавить; люди всегда предполагают, что это компьютерная проблема, но если вы считаете руками (база 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 пальцами на каждой руке спросят, почему твоя десятичная математика была нарушена...


многие из многочисленных дубликатов этого вопроса спрашивают о влиянии округления с плавающей запятой на определенные числа. На практике легче понять, как это работает, глядя на точные результаты расчетов интересов, а не просто читать об этом. Некоторые языки предоставляют способы сделать это - например, преобразование float или double to 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, который заканчивается четной цифрой и, следовательно, является правильным результатом.


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

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

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

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

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

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

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

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


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

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

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

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


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

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

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.


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

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

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

представьте, что вы собираетесь сложить два числа типа float, как 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

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

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

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


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

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

к сожалению 0.3 сидит в пропасть.


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

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


математика.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;
        }
    },
    diff:{
        value: function(A,B){
            var prec = this.max(this.get_precision(A),this.get_precision(B));
            return +this.precision(A-B,prec);
        }
    },
    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);
        }
    }
});

идея состоит в том, чтобы использовать математику вместо операторов, чтобы избежать ошибок float

Math.diff(0.2, 0.11) == 0.09 // true
0.2 - 0.11 == 0.09 // false

также обратите внимание, что Математике.разница и математика.sum auto-определение точности использования

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


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

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

на x на вопрос float переменной.

например

float x = 9.9F;

отладчик показывает 9.89999962 выход cout операция 9.9.

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

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


Sine Python 3.5 вы можете использовать math.isclose() функция в if conditions

import math

if math.isclose(0.1 + 0.2, 0.3, abs_tol=0.01):
    pass

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


вопрос резюме:

на листе 10^-8/1000 и 10^-11 оценить как равной пока в VBA они делают не.

на рабочем листе числа по умолчанию используются для научной нотации.

если вы измените ячейки на числовой формат (Ctrl+1) of Number С 15 десятичной точки, вы получаете:

=10^-11 returns 0.000000000010000
=10^(-8/1000) returns 0.981747943019984

таким образом, они, безусловно, не то же самое... один равен нулю, а другой-единице.

Excel не был разработан, чтобы иметь дело с очень небольших количествах - по крайней мере, не с шток устанавливает. Есть надстройки, которые помогают повысить точность номера.


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

преимущество плавающего представления над фиксированной точкой заключается в том, что оно может поддерживать более широкий диапазон значений. Например, представление с фиксированной точкой, содержащее 5 десятичных знаков с десятичной точкой, расположенной после третьей цифры, может представлять числа 123.34, 12.23, 2.45, etc. в то время как представление с плавающей запятой с 5-значной точностью может представлять 1.2345, 12345, 0.00012345 и т. д. Аналогично, представление с плавающей запятой позволяет расчеты в широком диапазоне значений при сохранении точности. Например,

img


Другие Ссылки: