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

Feb 26 2009

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

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

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

Ответы

2379 DanielScott Feb 26 2009 at 04:40

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

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

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

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

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

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

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

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

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

625 KernelPanik Apr 18 2013 at 18:52

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

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

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

481 JoelCoehoorn Feb 26 2009 at 04:43

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

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

322 ChrisJester-Young Nov 20 2014 at 09:39

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

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

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

215 DevinJeanpierre Feb 26 2009 at 04:41

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

126 DanielVassallo Apr 09 2010 at 19:25

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

Например:

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

120 WaiHaLee Feb 24 2015 at 00:15

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

59 MarkRansom Mar 16 2016 at 12:27

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

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

49 BrettDaniel Feb 26 2009 at 04:42

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

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

33 Justineo Dec 26 2011 at 13:51

Мое обходное решение:

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

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

30 bruziuz Oct 06 2014 at 01:39

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

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

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

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

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

29 KostasChalkias Jan 03 2015 at 19:12

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

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

28 DigitalRoss Feb 03 2016 at 06:49

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

Резюме

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

Вывод

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

26 MuhammadMusavi Aug 07 2018 at 16:34

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

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

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

18 workoverflow Aug 01 2012 at 14:02

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

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

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

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

16 PiyushS528 Oct 14 2013 at 23:45

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

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

16 AndreaCorbellini Aug 21 2015 at 21:53

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

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

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

16 PatriciaShanahan Dec 21 2015 at 18:15

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

15 Noname Mar 18 2016 at 07:38

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

9 BlairHoughton Oct 05 2015 at 22:55

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

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

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

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

9 alinsoar Dec 29 2016 at 17:29

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

5 TorstenBecker Dec 20 2017 at 05:37

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

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

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

5 nauer Aug 08 2018 at 15:47

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

>>> import math
>>> math.isclose(0.1 + 0.2, 0.3)
True
>>> 0.1 + 0.2 == 0.3
False
4 DanielMcLaury Dec 21 2018 at 01:27

Представьте, что вы работаете по основанию десять с точностью, скажем, 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 chqrlie Apr 22 2019 at 08:02

Десятичные числа, такие как 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 Piedone Dec 22 2017 at 23:39

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

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

3 VladAgurets May 08 2019 at 02:45

На самом деле это довольно просто. Когда у вас есть система с основанием 10 (например, наша), она может выражать только дроби, использующие простой множитель основания. Простые множители 10 - это 2 и 5. Таким образом, 1/2, 1/4, 1/5, 1/8 и 1/10 могут быть выражены чисто, потому что все знаменатели используют простые множители 10. Напротив, 1 / 3, 1/6 и 1/7 - все повторяющиеся десятичные дроби, потому что в их знаменателях используется простой множитель 3 или 7. В двоичной системе (или по основанию 2) единственный простой множитель равен 2. Таким образом, вы можете четко выражать только дроби, которые содержат только 2 в качестве основного множителя. В двоичном формате 1/2, 1/4, 1/8 все будут четко выражены десятичными знаками. В то время как 1/5 или 1/10 будут повторять десятичные дроби. Таким образом, 0,1 и 0,2 (1/10 и 1/5) при чистых десятичных дробях в системе с основанием 10 повторяют десятичные дроби в системе с основанием 2, в которой работает компьютер. Когда вы выполняете вычисления с этими повторяющимися десятичными знаками, вы получаете остатки которые переносятся, когда вы конвертируете компьютерное число с основанием 2 (двоичное) в более удобочитаемое число с основанием 10.

Из https://0.30000000000000004.com/

2 RollerSimmer Aug 20 2020 at 22:38

Нормальная арифметика - это основание 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) ...
1 TheMachinist Aug 03 2020 at 22:03

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

costargc Oct 06 2019 at 04:46

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

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

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 за то, что указал мне на это.