Вы можете поверить, что 0,1 + 0,2 не равно 0,3 ?!

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

  • Число одинарной точности (binary32). Стандартное представление IEEE с плавающей запятой одинарной точности требует 32 бита, которые могут быть представлены как пронумерованные от 0 до 31 слева направо. Первый бит - это знаковый бит, S, следующие 8 битов - это биты экспоненты, «E», а последние 23 бита - это дробная часть «F». Это снижает степень точности.
  • Число двойной точности (binary64): стандартное представление IEEE с плавающей запятой двойной точности требует 64 бита, которые могут быть представлены как пронумерованные от 0 до 63 слева направо. Первый бит - это знаковый бит, S, следующие 11 бит - это биты экспоненты, «E», а последние 53 бита (52 бита, явно сохраненные) представляют собой дробь «F». Двойная точность с плавающей запятой используется там, где требуется высокая арифметическая точность и должны использоваться числа вроде -2/19.

Недавно я увидел 0,1 + 0,2! = 0,3 в викторине по программированию, и мне просто стало любопытно, что происходит! Давайте посмотрим, что происходит. Существует два типа представления чисел с плавающей запятой: основание 10 и основание 2. Например, 0,125 равно 1/10 + 2/100 + 5/1000 находится в базе 10 (десятичная дробь), а 0,001 равно 0/2. + 0/4 + 1/8 находится в основании 2 (двоичная дробь). Проблема возникает, поскольку большинство дробных чисел не могут быть представлены двоичными дробями, поэтому их оценка будет сохранена.

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

В случае 1/10 двоичная дробь равна 3602879701896397 / (2 ** 55), что близко, но не в точности равно истинному значению 1/10. Когда вы вводите 0,1 в интерпретаторе Python, на выходе получается 0,1. Но если вы войдете

print('{:.30f}'.format(0.1))
OR
print(format(0.1, '.30f'))

вывод выглядит так:

0.100000000000000005551115123126

На самом деле это может немного отличаться на разных компьютерах, но именно так python видит 0.1! он обрезает бесполезные числа, когда вы просто печатаете 0,1. То же самое для 0.2:

0.200000000000000011102230246252

Когда вы вычисляете 0,1 + 0,2, ошибка представления обоих чисел накапливается и становится равной:

0.30000000000000004

что больше 0,3, значит, не равно 0,3! Когда вы распечатываете дробь 1/10, она показывает 0,1. Просто помните, даже если напечатанный результат выглядит как точное значение 1/10, фактическое сохраненное значение является ближайшей представимой двоичной дробью.

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

Но вы спросите, а как вы рассчитали 3602879701896397 / (2 ** 55) на 1/10 ?! Как говорится в учебнике по питону:

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

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

Мы можем переписать это как

J ~= 2**N / 10

Мы знаем, что J имеет ровно 53 бита (поэтому J больше или равно 2**52, но меньше 2**53), поэтому согласно

2**52 <= 2**N // 10 < 2**53

наилучшее значение для N - 56. Почему? Если вы умножите все три утверждения на 10 (я предположил, что 10 равно ~ ›2 ** 3, где обозначение ~› означает немного больше), вы обнаружите, что это выглядит как

10*(2**52) <= 2**56 < 10(2**53)
THEN
(~> 2**3)*(2**52) <= 2**56 < (~> 2**3)*(2**53)
THEN
(~> 2**55) <= 2**56 < (~> 2**56)

и единственное целочисленное значение между ~ ›2 ** 55 и ~› 2 ** 56 - 2 ** 56, поэтому N равно 56, и это единственное значение для N, которое оставляет J ровно с 53 битами. Итак, как мы знаем

2**56 // 10 = 7205759403792793 (quotient)

а также

2**56 % 10 = 6 (remainder)

Поскольку остаток больше 10/2 (половина от 10), частное должно округляться в большую сторону, и J становится 7205759403792794. Таким образом, 1/10 равно

7205759403792794 / (2**56) = 3602879701896397 / (2**55)

Поскольку мы округлили в большую сторону, это на самом деле немного больше 1/10; если бы мы не округлили в большую сторону, частное было бы немного меньше 1/10. Но ни в коем случае не может быть точно 1/10! Округление в большую сторону дает J = (2 ** 56) / 10 следующее утверждение:

J < (2**56) / 10 < J + epsilon
THEN
J / (2**56) < 1/10

Чтобы увидеть фактическое значение 1/10, вы должны умножить J на ​​10 ** 55, чтобы вы могли увидеть значение до 55 десятичных цифр:

(3602879701896397 * (10**55)) // (2**55)

и результат выглядит как

1000000000000000055511151231257827021181583404541015625

Это число хранится в компьютере вместо 0,1 :)

Спасибо за прочтение. Надеюсь, ты повеселишься. Всегда будьте УЧЕНЫМ;) Присоединяйтесь к нам в нашем Telegram канале.