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

3447

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

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

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

18
2607

Двоичная математика с плавающей запятой выглядит так. В большинстве языков программирования он основан на стандарте 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-тысячное в десятичном (0,0001) ** - если бы мы были в Привычка использовать систему счисления с основанием 2 в нашей повседневной жизни, вы даже посмотрите на это число и инстинктивно поймете, что можете прийти к нему, уменьшив что-то вдвое, снова и снова и снова, и снова.

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

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

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

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

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

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

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

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

22
  • 195
    Я думаю, что «некоторая константа ошибок» более правильная, чем «Эпсилон», потому что нет «Эпсилона», который можно было бы использовать во всех случаях. В разных ситуациях нужно использовать разные эпсилоны. И машина epsilon почти никогда не бывает хорошей константой для использования.
    Rotsor
    4 сен '10 в 23:33
  • 34
    Не совсем верно, что вся математика с плавающей запятой основана на стандарте IEEE [754]. До сих пор используются некоторые системы, в которых, например, используется старый шестнадцатеричный FP IBM, и все еще существуют видеокарты, не поддерживающие арифметику IEEE-754. Однако это верно в разумном приближении. 03 янв.
  • 19
    Cray отказался от соответствия стандарту IEEE-754 ради скорости. Java также ослабила свою приверженность в качестве оптимизации. 12 фев '13 в 3:12
  • 31 год
    Я думаю, вам следует добавить что-то к этому ответу о том, что вычисления с деньгами всегда должны выполняться с арифметикой с фиксированной точкой для целых чисел , потому что деньги квантуются. (Возможно, имеет смысл проводить внутренние бухгалтерские вычисления в крошечных долях цента или в любой другой вашей наименьшей денежной единице - это часто помогает, например, уменьшить ошибку округления при преобразовании «29,99 доллара в месяц» в дневную ставку - но это должно по-прежнему будет арифметикой с фиксированной точкой.)
    zwol
    12 мая '14 в 22:23
  • 33
    Интересный факт: эта самая 0.1, не точно представленная в двоичной системе с плавающей запятой, вызвала печально известную ошибку в программном обеспечении ракеты Patriot, в результате которой во время первой войны в Ираке погибло 28 человек.
    hdl
    24 сен '15 в 12:57
661

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

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

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 Ошибка округления деления: аппроксимация взаимного

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

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

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

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

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

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

6. Резюме

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

15
  • 8
    (3) неверно. Ошибка округления при делении составляет не менее одной единицы на последнем месте, но не более чем на половину единицы на последнем месте. 23 апр '14 в 22:31
  • 6
    @ gnasher729 Хороший улов. Большинство базовых операций также имеют ошибку менее 1/2 единицы на последнем месте с использованием стандартного режима округления IEEE. Отредактировал объяснение, а также отметил, что ошибка может быть больше 1/2 от одного ulp, но меньше 1 ulp, если пользователь отменяет режим округления по умолчанию (это особенно верно для встроенных систем). 24 апр '14 в 11: 172014-04-24 11:17
  • 47
    (1) с плавающей запятой числа не имеют ошибок. Каждое значение с плавающей запятой в точности то, что есть. Большинство (но не все) с плавающей точкой операции дают неточные результаты. Например, не существует двоичного значения с плавающей запятой, точно равного 1.0 / 10.0. Некоторые операции (например, 1,0 + 1,0) действительно дают точные результаты, с другой стороны. 10 июн.
  • 24
    «Основная причина ошибки при делении с плавающей запятой - это алгоритмы деления, используемые для вычисления частного» - это очень вводящая в заблуждение вещь. Для деления, соответствующего стандарту IEEE-754, единственной причиной ошибки при делении с плавающей запятой является невозможность точного представления результата в формате результата; один и тот же результат вычисляется независимо от используемого алгоритма. 23 фев '15 в 20:23
  • 7
    @Matt Извините за поздний ответ. В основном это связано с проблемами ресурсов / времени и компромиссами. Есть способ сделать длинное деление / более «нормальное» деление, это называется SRT-деление с основанием два. Однако это многократно сдвигает и вычитает делитель из делимого и занимает много тактовых циклов, поскольку вычисляет только один бит частного за такт. Мы используем таблицы обратных величин, чтобы мы могли вычислять большее количество бит частного за цикл и находить эффективный компромисс между производительностью и скоростью. 1 фев '16 в 15:33
547

Он сломан точно так же, как и десятичная система счисления (с основанием 10), которую вы выучили в начальной школе, только для системы с основанием 2.

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

9
  • 158
    Отличный и короткий ответ. Повторяющийся узор выглядит так: 0,00011001100110011001100110011001100110011001100110011 ... 16 июн.
  • 17
    Существуют методы, которые дают точные десятичные значения. BCD (двоично-десятичное число) или различные другие формы десятичного числа. Однако они оба медленнее (НАМНОГО медленнее) и занимают больше места, чем использование двоичных чисел с плавающей запятой. (Например, упакованный BCD хранит 2 десятичных цифры в байте. Это 100 возможных значений в байте, которые могут фактически хранить 256 возможных значений, или 100/256, что тратит впустую около 60% возможных значений байта.) 21 июня '16 в 16: 432016-06-21 16:43
  • 1
    @IInspectable, для операций с плавающей запятой математика на основе BCD в сотни раз медленнее, чем нативная двоичная с плавающей запятой. 8 сен '16 в 0:20
  • 1
    @DuncanC Ну, есть методы, которые дают точные десятичные значения - для сложения и вычитания. Для деления, умножения и т. Д. Они имеют те же проблемы, что и двоичные методы. Вот почему BCD используется в бухгалтерском учете, поскольку он в основном имеет дело с плюсами и минусами, и вы не можете учитывать что-либо меньшее, чем пенни. Однако что-то простое, например, 1/3*3 == 1терпит неудачу (оценивается как ложь) в математике BCD, точно так же, как если бы вы использовали десятичное деление на бумаге.
    Joooeey
    22 июня '18 в 21: 192018-06-22 21:19
  • 6
    @DuncanC: "BCD намного медленнее двоичного числа с плавающей запятой, точка". - Ага. Если это не так. Совершенно уверен, что есть архитектуры , в которых математика BCD по крайней мере так же (или быстрее), чем математика с плавающей запятой IEEE-754. Но это помимо сути: если вам нужна десятичная точность, вы не можете использовать представление с плавающей запятой IEEE-754. Так вы добьетесь только одного: более быстрого расчета неверных результатов. 23 июня '18 в 10: 302018-06-23 10:30
347

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

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

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

17
  • 4
    Обратите внимание, что есть некоторые языки, которые включают точную математику. Одним из примеров является Scheme, например, через GNU Guile. См. Draketo.de/english/exact-math-to-the-rescue - они сохраняют математику в виде дробей и разрезают только на части. 20 ноя '14 в 6:40
  • 5
    @FloatingRock На самом деле очень немногие основные языки программирования имеют встроенные рациональные числа. Арне, как и я, является интриганом, так что это то, чем мы балуемся. 25 ноя '14 в 16: 562014-11-25 16:56
  • 6
    @ArneBabenhauserheide Я думаю, стоит добавить, что это будет работать только с рациональными числами. Итак, если вы занимаетесь математикой с иррациональными числами, такими как пи, вам придется хранить их как кратные пи. Конечно, любое вычисление, связанное с числом Пи, не может быть представлено в виде точного десятичного числа. 11 марта '15 в 13: 062015-03-11 13:06
  • 14
    @connexo Хорошо. Как бы вы запрограммировали вращатель пиццы на 36 градусов? Что такое 36 градусов? (Подсказка: если вы можете определить это точно, у вас также есть резак для пиццы с точностью до десятых.) Другими словами, у вас не может быть 1/360 (градус) или 1 / 10 (36 градусов) только с двоичной плавающей запятой. 13 авг.
  • 13
    @connexo Кроме того, «каждый идиот» не может повернуть пиццу ровно на 36 градусов. Люди слишком склонны к ошибкам, чтобы делать что-то настолько точное. 13 авг.
224

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

5
  • 25
    @Pacerier Конечно, они могут использовать два целых числа неограниченной точности для представления дроби или они могут использовать нотацию кавычек. Это конкретное понятие «двоичный» или «десятичный» делает это невозможным - идея, что у вас есть последовательность двоичных / десятичных цифр и, где-то там, точка счисления. Чтобы получить точные рациональные результаты, нам понадобится лучший формат. 15 окт.
  • 15
    @Pacerier: ни двоичная, ни десятичная числа с плавающей запятой не могут точно хранить 1/3 или 1/13. Десятичные типы с плавающей запятой могут точно представлять значения формы M / 10 ^ E, но они менее точны, чем двоичные числа с плавающей запятой аналогичного размера, когда дело доходит до представления большинства других дробей . Во многих приложениях более полезно иметь более высокую точность с произвольными дробями, чем иметь идеальную точность с несколькими «специальными» дробями. 24 апр '14 в 16:43
  • @supercat При сравнении точности binary64 и decimal64 : точность вполне сопоставима - конечно, в пределах 10 раз друг от друга. Допустим, что decimal64 колеблется больше, чем binary64. 26 авг.
  • 3
    @chux: разница в точности между двоичными и десятичными типами невелика, но разница 10: 1 в точности в лучшем и худшем случае для десятичных типов намного больше, чем разница 2: 1 для двоичных типов. Мне любопытно, создал ли кто-нибудь аппаратное или написанное программное обеспечение для эффективной работы с любым из десятичных типов, поскольку ни один из них, похоже, не поддается эффективной реализации в аппаратном или программном обеспечении. 26 авг.
  • 1
    @DevinJeanpierre Я думаю, дело в том, что «компьютеры» не имеют «определенного понятия« двоичный »или« десятичный »». Похоже, что Пэйсьер считает, что именно разработчики языков слишком рано решили перейти к «плавающей запятой», запомнив такие числа, как «0,1», «0,2» и «0,3», которые могут быть не только более точными, но и также более компактно хранится в виде текста (BCD).
    Jeff Y
    1 фев '16 в 14:58
134
+100

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

Преамбула

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

value = (-1)^s * (1.m51m50...m2m1m0)2 * 2e-1023

в 64 битах:

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

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

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


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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

Теперь в мантиссе 53 бита (53-й находится в квадратных скобках в строке выше). Режим округления по умолчанию для IEEE 754 - « 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: последние несколько бит округляются.

1
  • 22
    Мой ответ был отклонен вскоре после публикации. С тех пор я внес много изменений (включая явное указание повторяющихся битов при записи 0,1 и 0,2 в двоичном формате, которые я пропустил в оригинале). На случай, если это увидит тот, кто проголосовал против, не могли бы вы дать мне несколько отзывов, чтобы я мог улучшить свой ответ? Я чувствую, что мой ответ добавляет что-то новое, поскольку обработка суммы в IEEE 754 не рассматривается таким же образом в других ответах. Хотя статья «Что должен знать каждый компьютерный ученый ...» охватывает примерно тот же материал, мой ответ конкретно касается случая 0,1 + 0,2. 24 фев '15 в 7:29
132

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

Например:

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

... вместо того:

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

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

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


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

3
  • 5
    Проблема в том, что само преобразование неточное. 16,08 * 100 = 1607,9999999999998. Должны ли мы прибегать к разделению числа и преобразованию отдельно (как в случае 16 * 100 + 08 = 1608)?
    Jason
    7 окт.
  • 41 год
    Решение здесь состоит в том, чтобы делать все ваши вычисления в целых числах, а затем делить их на вашу долю (в данном случае 100) и округлять только при представлении данных. Это гарантирует, что ваши расчеты всегда будут точными. 08 дек.
  • 18
    Просто чтобы немного придираться: целочисленная арифметика точна только с плавающей запятой до точки (каламбур). Если число больше 0x1p53 (для использования шестнадцатеричной нотации с плавающей запятой Java 7 = 9007199254740992), тогда ulp равно 2, и поэтому 0x1p53 + 1 округляется до 0x1p53 (а 0x1p53 + 3 округляется до 0x1p53 +). 4, из-за округления до четности). :-D Но, конечно, если ваше число меньше 9 квадриллионов, все будет в порядке. :-П 3 дек '14 в 13:28
63

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

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

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

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

Squeezing infinitely many real numbers into a finite number of bits requires an approximate representation. Although there are infinitely many integers, in most programs the result of integer computations can be stored in 32 bits. In contrast, given any fixed number of bits, most calculations with real numbers will produce quantities that cannot be exactly represented using that many bits. Therefore the result of a floating-point calculation must often be rounded in order to fit back into its finite representation. This rounding error is the characteristic feature of floating-point computation.

37

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

Floating point numbers cannot represent all decimals precisely in binary

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

3
  • это вызвало у меня настоящую головную боль. Я суммирую 12 чисел с плавающей запятой, затем показываю сумму и среднее значение этих чисел. использование toFixed () может исправить суммирование двух чисел, но при суммировании нескольких чисел скачок значительный. 17 марта '20 в 12:27
  • @Nuryagdy Mustapayev Я не понял вашего намерения, поскольку я тестировал, прежде чем вы сможете суммировать 12 чисел с плавающей запятой, затем использовать функцию floatify () для результата, а затем делать с ней все, что хотите, я не заметил никаких проблем с ее использованием. 18 марта '20 в 22: 212020-03-18 19:21
  • Я просто говорю, что в моей ситуации, когда у меня есть около 20 параметров и 20 формул, где результат каждой формулы зависит от других, это решение не помогло. 20 мар.
33

Мое решение:

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

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

0
31

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

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

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

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

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

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

31

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

Summary

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

How did this happen?

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

           а / (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 .

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

Dealing with it

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

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

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

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

Conclusion

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

5
  • Округление до ближайшего целого числа не во всех случаях является безопасным способом решения проблемы сравнения. 0,4999998 и 0,500001 округляются до разных целых чисел, поэтому вокруг каждой точки отсечки округления есть «опасная зона». (Я знаю, что эти десятичные строки, вероятно, нельзя точно представить как двоичные числа с плавающей запятой IEEE.) 9 дек.
  • 1
    Кроме того, хотя формат с плавающей запятой является «устаревшим» форматом, он очень хорошо спроектирован. Я не знаю ничего, что бы кто-нибудь изменил, если бы переделал его сейчас. Чем больше я узнаю об этом, тем больше думаю, что он действительно хорошо спроектирован. например, смещенная экспонента означает, что последовательные двоичные числа с плавающей запятой имеют последовательные целочисленные представления, поэтому вы можете реализовать nextafter()с целочисленным приращением или уменьшением двоичного представления числа с плавающей запятой IEEE. Кроме того, вы можете сравнить числа с плавающей запятой как целые числа и получить правильный ответ, за исключением случаев, когда они оба отрицательны (из-за знака величины и дополнения до 2). 9 дек.
  • Я не согласен, числа с плавающей запятой следует хранить как десятичные, а не двоичные, и все проблемы решены. 19 фев '17 в 19:32
  • Разве « x / (2 ^ n + 5 ^ n) » не должно быть « x / (2 ^ n * 5 ^ n) »? 5 фев '18 в 7:34
  • @stephen c вы сможете определить нужную точность в настройках компилятора. Но он просто округлит результат, как в калькуляторе. 16 авг.
29

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

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

18

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

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

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

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

18

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

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

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

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

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

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

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

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

1
  • Мы также можем использовать фиксированную точку. Например, если центы - это ваша самая точная детализация, тогда вычисления могут выполняться с целыми числами в центах, а не в долларах.
    qwr
    18 июля '20 в 21: 582020-07-18 21:58
17

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

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

1
  • 1
    @Nae Я бы перевел второй абзац следующим образом: «Большинство дробей не могут быть представлены точно ни в десятичном, ни в двоичном виде. Таким образом, большинство результатов будет округлено, хотя они все равно будут точными до количества бит / цифр, присущих представлению. быть использованным." 9 марта '18 в 14: 192018-03-09 14:19
16

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

4
  • 2
    Для человека, от правки которого я только что откатился: я считаю кавычки кода подходящими для цитирования кода. Этот ответ, не зависящий от языка, вообще не содержит цитируемого кода. Числа можно использовать в предложениях на английском языке, и это не превращает их в код. 22 ноя '17 в 16:22
  • Это , вероятно , почему кто - то отформатированный ваши номера в виде кода - не для форматирования, но для удобства чтения. 12 янв.
  • ... также, округление до даже относится к двоичному представлению, а не к десятичному представлению. См. Это или, например, это . 12 янв.
  • @WaiHaLee Я не применял проверку нечетности / четности ни к каким десятичным числам, только к шестнадцатеричным. Шестнадцатеричная цифра даже тогда и только тогда, когда младший бит ее двоичного расширения равен нулю. 25 сен '19 в 16:39
16

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

4
  • Поскольку люди используют десятичные числа, я не вижу веских причин, по которым числа с плавающей запятой по умолчанию не представлены в виде десятичных чисел, поэтому у нас есть точные результаты. 19 фев '17 в 19:27
  • Люди используют много оснований, кроме десятичной (десятичной), двоичная - это та, которую мы используем больше всего для вычислений ... "веская причина" в том, что вы просто не можете представить каждую дробь в каждой базе ..
    user1641172
    20 фев '17 в 8:59
  • 1
    Бинарную арифметику @RonenFestinger легко реализовать на компьютерах, потому что для нее требуется всего восемь основных операций с цифрами: скажем, $ a $, $ b $ в $ 0,1 $, все, что вам нужно знать, это $ \ operatorname {xor} (a, b) $ и $ \ operatorname {cb} (a, b) $, где xor является исключающим или, а cb - "бит переноса", который равен $ 0 $ во всех случаях, кроме случая, когда $ a = 1 = b $, и в этом случае мы имеем один (фактически коммутативность всех операций экономит вам 2 доллара, а все, что вам нужно, это 6 правил). Десятичное расширение требует хранения $ 10 \ times 11 $ (в десятичной системе счисления) случаев и $ 10 $ различных состояний для каждого бита и тратит память на перенос. 25 мар.
  • 1
    @RonenFestinger - Десятичное число НЕ более точное. Вот о чем говорит этот ответ. Для любой базы, которую вы выберете, будут рациональные числа (дроби), которые дают бесконечно повторяющиеся последовательности цифр. Для справки, некоторые из первых компьютеров действительно использовали представление чисел по основанию 10, но первые разработчики компьютерного оборудования вскоре пришли к выводу, что реализация с основанием 2 была намного проще и эффективнее. 07 июл.
9

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

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

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

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

0
9

Ради удовольствия я поигрался с представлением чисел с плавающей запятой, следуя определениям из 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.

0
6

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

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

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

6

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

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

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

0.125

имеет значение 1/10 + 2/100 + 5/1000 и, таким же образом, двоичную дробь:

0.001

имеет значение 0/2 + 0/4 + 1/8. Эти две дроби имеют одинаковое значение, с той лишь разницей, что первая - десятичная дробь, вторая - двоичная.

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

К задаче проще подойти в базе 10. Возьмем, например, дробь 1/3. Вы можете округлить его до десятичной дроби:

0.3

или лучше,

0.33

или лучше,

0.333

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

Аналогичным образом, независимо от того, сколько десятичных знаков с основанием 2 вы используете, десятичное значение 0,1 не может быть представлено точно как двоичная дробь. В базе 2 1/10 - это периодическое число:

0.0001100110011001100110011001100110011001100110011 ...

Остановитесь на любом конечном количестве битов, и вы получите приближение.

Для Python на типичной машине для точности числа с плавающей запятой используется 53 бита, поэтому значение, сохраненное при вводе десятичной дроби 0,1, является двоичной дробью.

0.00011001100110011001100110011001100110011001100110011010

что близко, но не совсем равно 1/10.

Легко забыть, что сохраненное значение является приближением исходной десятичной дроби из-за способа отображения чисел с плавающей запятой в интерпретаторе. Python отображает только десятичное приближение значения, хранящегося в двоичном формате. Если бы Python выводил истинное десятичное значение двоичного приближения, сохраненного для 0,1, он бы выводил:

>>> 0.1
0.1000000000000000055511151231257827021181583404541015625

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

>>> 0.1
0.1

Важно понимать, что на самом деле это иллюзия: сохраненное значение не точно 1/10, просто на дисплее сохраненное значение округляется. Это становится очевидным, как только вы выполняете арифметические операции с этими значениями:

>>> 0.1 + 0.2
0.30000000000000004

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

В этом есть еще один сюрприз. Например, если вы попытаетесь округлить значение 2,675 до двух десятичных знаков, вы получите

>>> round (2.675, 2)
2.67

В документации примитива round () указано, что он округляется до ближайшего значения, отличного от нуля. Поскольку десятичная дробь находится точно посередине между 2,67 и 2,68, вы должны ожидать получить (двоичное приближение) 2,68. Однако это не так, потому что, когда десятичная дробь 2,675 преобразуется в число с плавающей запятой, она сохраняется с помощью приближения, точное значение которого:

2.67499999999999982236431605997495353221893310546875

Поскольку аппроксимация немного ближе к 2,67, чем к 2,68, округление меньше.

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

>>> from decimal import Decimal
>>> Decimal (2.675)
>>> Decimal ('2.67499999999999982236431605997495353221893310546875')

Еще одно следствие того факта, что 0,1 не точно сохраняется в 1/10, состоит в том, что сумма десяти значений 0,1 также не дает 1,0:

>>> sum = 0.0
>>> for i in range (10):
... sum + = 0.1
...>>> sum
0.9999999999999999

Арифметика двоичных чисел с плавающей запятой таит много таких сюрпризов. Проблема с «0.1» подробно объясняется ниже, в разделе «Ошибки представления». См. «Опасности с плавающей точкой» для более полного списка таких сюрпризов.

Это правда, что нет простого ответа, однако не стоит слишком подозревать плавающие виртуальные числа! Ошибки в Python при операциях с числами с плавающей запятой связаны с базовым оборудованием, и на большинстве машин их не более 1 из 2 ** 53 на операцию. Это более чем необходимо для большинства задач, но вы должны помнить, что это не десятичные операции, и каждая операция с числами с плавающей запятой может столкнуться с новой ошибкой.

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

В этой части ответа подробно объясняется пример «0.1» и показано, как вы можете самостоятельно провести точный анализ этого типа кейса. Мы предполагаем, что вы знакомы с двоичным представлением чисел с плавающей запятой. Термин Ошибка представления означает, что большинство десятичных дробей не могут быть представлены точно в двоичном формате. Это основная причина, по которой Python (или Perl, C, C ++, Java, Fortran и многие другие) обычно не отображает точный результат в десятичном формате:

>>> 0.1 + 0.2
0.30000000000000004

Почему ? 1/10 и 2/10 не могут быть представлены точно в двоичных дробях. Однако все машины сегодня (июль 2010 г.) следуют стандарту IEEE-754 для арифметики чисел с плавающей запятой. и большинство платформ используют "двойную точность IEEE-754" для представления Python с плавающей запятой. Двойная точность IEEE-754 использует 53 бита точности, поэтому при чтении компьютер пытается преобразовать 0,1 в ближайшую дробь формы J / 2 ** N, где J - целое число, равное ровно 53 битам. Перепишите:

1/10 ~ = J / (2 ** N)

в :

J ~ = 2 ** N / 10

помня, что J составляет ровно 53 бита (поэтому> = 2 ** 52, но <2 ** 53), наилучшее возможное значение для N равно 56:

>>> 2 ** 52
4503599627370496
>>> 2 ** 53
9007199254740992
>>> 2 ** 56/10
7205759403792793

Таким образом, 56 - единственное возможное значение для N, которое оставляет для J ровно 53 бита. Следовательно, наилучшее возможное значение для J - это частное, округленное:

>>> q, r = divmod (2 ** 56, 10)
>>> r
6

Поскольку перенос больше половины 10, наилучшее приближение получается округлением в большую сторону:

>>> q + 1
7205759403792794

Следовательно, наилучшее возможное приближение для 1/10 в «двойной точности IEEE-754» - это выше 2 ** 56, то есть:

7205759403792794/72057594037927936

Обратите внимание, что, поскольку округление было выполнено в большую сторону, результат на самом деле немного больше 1/10; если бы мы не округлили в большую сторону, частное было бы чуть меньше 1/10. Но ни в коем случае не 1/10!

Таким образом, компьютер никогда не "видит" 1/10: он видит точную дробь, указанную выше, наилучшее приближение с использованием чисел с плавающей запятой двойной точности из "" IEEE-754 ":

>>>. 1 * 2 ** 56
7205759403792794.0

Если мы умножим эту дробь на 10 ** 30, мы сможем увидеть значения ее 30 десятичных знаков сильного веса.

>>> 7205759403792794 * 10 ** 30 // 2 ** 56
100000000000000005551115123125L

Это означает, что точное значение, хранящееся в компьютере, примерно равно десятичному значению 0,100000000000000005551115123125. В версиях до Python 2.7 и Python 3.1 Python округлял эти значения до 17 значащих десятичных знаков, отображая «0,10000000000000001». В текущих версиях Python отображаемое значение - это значение, дробная часть которого является как можно короче, но дает точно такое же представление при преобразовании обратно в двоичную форму, просто отображая «0,1».

4

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

1/3 + 2 / 3 == 1

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

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

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

0.33333333 + 0.66666666 = 0.99999999

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


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

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

а также

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

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

0.0001100 + 0.0011001 = 0.0100101

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

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

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


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

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

4

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

#include <stdio.h>

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

Выход:

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

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

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

3

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

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

3

It's actually pretty simple. When you have a base 10 system (like ours), it can only express fractions that use a prime factor of the base. The prime factors of 10 are 2 and 5. So 1/2, 1/4, 1/5, 1/8, and 1/10 can all be expressed cleanly because the denominators all use prime factors of 10. In contrast, 1/3, 1/6, and 1/7 are all repeating decimals because their denominators use a prime factor of 3 or 7. In binary (or base 2), the only prime factor is 2. So you can only express fractions cleanly which only contain 2 as a prime factor. In binary, 1/2, 1/4, 1/8 would all be expressed cleanly as decimals. While, 1/5 or 1/10 would be repeating decimals. So 0.1 and 0.2 (1/10 and 1/5) while clean decimals in a base 10 system, are repeating decimals in the base 2 system the computer is operating in. When you do math on these repeating decimals, you end up with leftovers which carry over when you convert the computer's base 2 (binary) number into a more human readable base 10 number.

С https://0.30000000000000004.com/

3

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

В аппаратном обеспечении числа с плавающей запятой хранятся как целые мантиссы и экспоненты. Мантисса представляет собой значащие цифры. Экспонента похожа на научную запись, но в ней используется основание 2 вместо 10. Например, 64,0 будет представлено мантиссой, равной 1, и показателем степени 6. 0,125 будет представлено мантиссой, равной 1, и показателем степени -3.

Десятичные дроби с плавающей запятой должны складывать отрицательные степени двойки.

0.1b = 0.5d
0.01b = 0.25d
0.001b = 0.125d
0.0001b = 0.0625d
0.00001b = 0.03125d

и так далее.

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

if(a==b) ...

вы бы использовали

delta = 0.0001; // or some arbitrarily small amount
if(a - b > -delta && a - b < delta) ...
3

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

Единственный простой множитель 2 - 2, в то время как 10 имеет простые множители 2 и 5. Результатом этого является то, что каждое число, которое может быть записано точно как двоичная дробь, также может быть записано точно как десятичная дробь, но только подмножество числа, которые могут быть записаны как десятичные дроби, могут быть записаны как двоичные дроби.

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

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

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