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

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

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

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

Ответов (25)

Решение

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

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

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

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

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

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

Достаточно всестороннее рассмотрение вопросов арифметики с плавающей запятой - вот что должен знать каждый компьютерный ученый об арифметике с плавающей запятой . Более простое объяснение см. На сайте float-point-gui.de .

Боковое примечание: все позиционные системы счисления (с основанием N) разделяют эту проблему с точностью.

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

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

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

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

Боковое примечание: работа с поплавками в программировании

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

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

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

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

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

Мое решение:

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

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

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

Например:

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) .

Вы пробовали использовать клейкую ленту?

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

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

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

Взгляд дизайнера аппаратного обеспечения

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

1. Обзор

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

2. Стандарты

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

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

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

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

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

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

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

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

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

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

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

6. Резюме

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

Резюме

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

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

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

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

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

           а / (2 н х 5 м )

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

           а / 2 н

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

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

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

Как справиться с этим

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

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

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

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

Заключение

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

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

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

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

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

Код выводит двоичное представление чисел с плавающей запятой в 3 отдельные группы.

SIGN EXPONENT FRACTION

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

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

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

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

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

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

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

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

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

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

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

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

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

Вот и все. Фактически, значение 999999999

999999999.999999446351872

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

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

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

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

У этого ножа для пиццы очень тонкие движения, и если вы начнете с целой пиццы, затем разделите ее вдвое и каждый раз продолжите уменьшать вдвое наименьший кусок, вы можете сделать половину 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,300000000000000044408920985006261616169452667236328125, тогда как на самом деле это число составляет 0,299934


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

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

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

Преамбула

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

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

в 64 битах:

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

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

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


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

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

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

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

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

Ближе к делу: исходный вопрос

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

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

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

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

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

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

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

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

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

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

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

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

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

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

x = 2^-2  * 1.0011001100110011001100110011001100110011001100110011(1)

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

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

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

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

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

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

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

TL; DR

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

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

После обратного преобразования в десятичную форму эти значения:

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

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

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

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

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

1/3 + 2 / 3 == 1

и узнайте, что это возвращается false . Почему? Что ж, в качестве реальных чисел у нас есть

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

Усекая до восьми знаков после запятой, получаем

0.33333333 + 0.66666666 = 0.99999999

который, конечно, отличается от 1.00000000 by 0.00000001 .


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

1/10 = 0,0001100110011001100 ... (основание 2)

а также

1/5 = 0,0011001100110011001 ... (основание 2)

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

0.0001100 + 0.0011001 = 0.0100101

а с другой стороны,

3/10 = 0,01001100110011 ... (основание 2)

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


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

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

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

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

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

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

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

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

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

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

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

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

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

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

Это происходит из-за двоичного формата двойной точности: IEEE 754 двоичный формат с плавающей запятой двойной точности: binary64

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

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

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

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

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

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

или

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

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

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

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

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

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

>>> import math
>>> math.isclose(0.1 + 0.2, 0.3)
True
>>> 0.1 + 0.2 == 0.3
False

Короче , потому что:

Числа с плавающей запятой не могут точно представлять все десятичные дроби в двоичном формате.

Так же, как 10/3, которого точно не существует в базе 10 (это будет 3,33 ... повторяющееся), точно так же 1/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? Пришло время преобразовать его в float, чтобы сделать его таким, каким вы хотите:

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

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

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

Попробуем сами:

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

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

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

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

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

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

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

Он сломан точно так же, как и десятичная (с основанием 10), только для основания 2.

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

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

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