Мой ответ довольно длинный, поэтому я разделил его на три части. Поскольку вопрос касается математики с плавающей точкой, я сделал упор на том, что на самом деле делает машина. Я также определил двойную (64-битную) точность, но аргумент в равной степени применим к любой арифметике с плавающей запятой.
Преамбула
двоичный формат с плавающей точкой двойной точности IEEE 754 (binary64) число представляет число вида
значение = (-1) ^ с * (1 м 51 м 50 ... м 2 м 1 м 0 ) 2 * 2 e-1023
в 64 битах:
- Первый бит - это знаковый бит :
1
, если число отрицательное, 0
в противном случае 1 .
- Следующими 11 битами являются экспонента , что составляет смещение на 1023. Другими словами, после считывания экспонентных битов из числа с двойной точностью, 1023 должно быть вычтено из получить силу двух.
- Остальные 52 бита имеют значение и (или мантиссу). В мантиссе «подразумеваемый»
1.
всегда 2 опускается, так как самый старший бит любого двоичного значения - 1
.
1 - IEEE 754 допускает концепцию нулевого знака - +0
и -0
трактуются по-разному: 1 / (+0)
- положительная бесконечность; 1 / (-0)
отрицательная бесконечность. Для нулевых значений биты мантиссы и экспоненты равны нулю. Примечание: нулевые значения (+0 и -0) явно не классифицируются как денормальные 2 .
2 - Это не относится к ненормальным числам , у которых показатель смещения равен нулю (и подразумевается 0.
). Диапазон числовых чисел двойной точности равен d min ≤ | x | ≤ d max , где d min (наименьшее представимое ненулевое число) составляет 2 -1023 - 51 (≈ 4,94 * 10 -324 ) и d max (наибольшее денормальное число, для которого мантисса полностью состоит из 1
с) составляет 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 равен ' Округление до ближайшего ' - т.е. если число 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
: последние несколько бит округляются.