Математика с плавающей точкой нарушена? - PullRequest
2646 голосов
/ 26 февраля 2009

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

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

Почему возникают эти неточности?

Ответы [ 30 ]

27 голосов
/ 05 октября 2014

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

Не все числа могут быть представлены через с плавающей запятой / с двойным числом Например, число «0.2» будет представлено как «0.200000003» с одинарной точностью в стандарте IEEE754 с плавающей запятой.

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

enter image description here

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

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

26 голосов
/ 03 января 2015

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

При добавлении всех значений ( 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%).

24 голосов
/ 03 февраля 2016

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

Краткое описание

Арифметика с плавающей точкой является точной, к сожалению, она не очень хорошо согласуется с нашим обычным представлением чисел с основанием 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 m )

В двоичном коде мы получаем только 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-битного хранилища, но это другая история. )

Заключение

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

18 голосов
/ 01 августа 2012

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

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

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

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

15 голосов
/ 14 октября 2013

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

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

13 голосов
/ 21 декабря 2015

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

13 голосов
/ 18 марта 2016

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

13 голосов
/ 07 августа 2018

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

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

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 голосов
/ 21 августа 2015

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

Некоторые языки высокого уровня, такие как 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 BigFraction класс . Оба представляют рациональные числа в виде пар (numerator, denominator), и они могут давать более точные результаты, чем десятичная арифметика с плавающей запятой.

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

9 голосов
/ 05 октября 2015

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

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

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

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

...