Вводная лекция в курс «Численные методы»
ОП.10 Численные методы · Специальность 09.02.07 «Информационные системы и программирование»
Среднее профессиональное образование (СПО)
Добро пожаловать в мир, где компьютеры ошибаются в простейшей арифметике. Эта лекция — не про скучные формулы, а про реальные баги, которые ломают финансовые системы, игры и даже ракеты. Мы разберем легендарный сайт и поймем, как с этим жить программисту.
В мире программирования есть сайт, который стал мемом, учебным пособием и предупреждением одновременно. Его адрес говорит сам за себя: 0.30000000000000004.com.
Если вы откроете консоль любого современного языка программирования (Python, JavaScript, Java, C++) и введете простейшее выражение, вы получите неожиданный результат:
>>> 0.1 + 0.2
0.30000000000000004
Для обычного пользователя это выглядит как критическая поломка компьютера. Как так? Неужели машина, которая управляет самолетами и банками, не может сложить две простые дроби?
Ответ: с компьютером всё в порядке. Проблема в том, как он представляет числа внутри своей памяти. И именно об этом весь курс «Численные методы».
Мы привыкли к десятичной системе счисления (основание 10). В ней мы легко записываем дроби вроде 0.1 или 0.2. Но компьютеры не умеют считать в десятичной системе. Они работают исключительно в двоичной системе счисления (основание 2), используя только нули и единицы.
Проблема возникает при переводе десятичной дроби в двоичную. Оказывается, многие привычные нам десятичные дроби в двоичной системе являются бесконечными периодическими.
Попробуйте записать дробь 1/3 в десятичной системе. Вы получите 0.333333... (тройки повторяются бесконечно). Если вы обрежете эту запись на 5-м знаке (0.33333), а затем умножите на 3, вы получите 0.99999, а не 1. Вы потеряли точность из-за обрезания.
Точно так же компьютер «обрезает» бесконечную двоичную дробь, потому что у него конечный объем памяти (обычно 64 бита для типа double).
Давайте посмотрим, как число 0.1 выглядит в двоичном виде на самом деле:
0.00011001100110011001100110011001100110011001100110011010...
Эта последовательность 1100 повторяется бесконечно. Поскольку стандарт хранения чисел (о нем ниже) выделяет на дробную часть (мантиссу) строго ограниченное количество бит (52 бита для 64-битного числа), компьютер вынужден округлить это число на 52-м знаке.
Когда вы складываете два таких уже округленных числа (0.1 и 0.2), их микроскопические ошибки округления суммируются. В результате вместо идеального 0.3 (которое тоже бесконечная дробь в двоичной системе) получается число, чуть-чуть большее: 0.30000000000000004.
Это не баг языка программирования. Это фундаментальное ограничение аппаратного обеспечения, закрепленное в международном стандарте.
Чтобы все компьютеры в мире считали одинаково, был принят международный стандарт IEEE 754 (разработанный Институтом инженеров по электротехнике и электронике — Institute of Electrical and Electronics Engineers).
Этот стандарт определяет, как хранить числа с плавающей запятой (floating-point) в памяти электронно-вычислительной машины (ЭВМ). Число разбивается на три части:
Именно ограничение в 52 бита на мантиссу не позволяет записать бесконечную двоичную дробь 0.1 точно. Возникает погрешность представления.
Можно подумать: «Ну и что, что там появилась четверка на 17-м знаке после запятой?». В некоторых областях это фатально.
Если банк проводит миллионы транзакций в день, и в каждой накапливается ошибка в 10⁻¹⁶ рубля, к концу года «потерянные» или «лишние» деньги могут составить значительную сумму. Именно поэтому в банковском ПО запрещено использовать типы float или double для денег.
Знаменитая катастрофа ракеты «Патриот» в 1991 году произошла из-за накопления ошибки округления при умножении времени на 0.1. За 100 часов работы ошибка составила 0.34 секунды, что при скорости ракеты 1,5 км/с привело к промаху на 600 метров и человеческим жертвам.
Вывод: программист, который не понимает природу погрешностей, создает опасное программное обеспечение.
Первое и главное правило работы с числами с плавающей точкой: никогда не сравнивайте их на точное равенство с помощью оператора ==.
# ПЛОХО: Это условие никогда не выполнится
if (0.1 + 0.2) == 0.3:
print("Всё верно")
else:
print("Ошибка! Получилось:", 0.1 + 0.2)
# Результат: Ошибка! Получилось: 0.30000000000000004
Вместо этого нужно проверять, находится ли разница между числами в пределах допустимой погрешности (которую принято обозначать греческой буквой ε — «эпсилон»).
# ХОРОШО: Сравнение с допуском
epsilon = 1e-9 # Допустимая погрешность (0.000000001)
if abs((0.1 + 0.2) - 0.3) < epsilon:
print("Числа равны в пределах заданной точности")
В Python для этого есть удобная встроенная функция math.isclose().
Если вам нужна абсолютная точность (например, при работе с валютами, ценами или научными константами), используйте специализированные типы данных, которые не полагаются на двоичное представление.
В Python для этого существует модуль decimal. Он хранит числа так, как мы привыкли видеть их на бумаге (в десятичном виде), жертвуя скоростью вычислений ради точности.
from decimal import Decimal
# Важно: передавайте числа как строки, чтобы избежать
# ошибки округления еще до создания объекта Decimal!
a = Decimal('0.1')
b = Decimal('0.2')
print(a + b) # Вывод: 0.3
print(a + b == Decimal('0.3')) # Вывод: True
Именно тип Decimal (или его аналоги в других языках, например, BigDecimal в Java) является стандартом для финансовых вычислений во всем мире.
Феномен 0.30000000000000004 — это лишь верхушка айсберга. В реальных задачах программирования мы сталкиваемся с гораздо более сложными проблемами:
Численные методы — это набор алгоритмов, которые позволяют находить приближенные решения математических задач с гарантированной точностью, понимая и контролируя все погрешности.
Теперь вы знаете главный секрет:
в компьютере нет точных дробей, есть только управляемые погрешности.
На следующих лекциях мы научимся не бояться этих погрешностей, а использовать их для решения реальных инженерных и IT-задач.
Переходим к Лекции №1: «Элементы теории погрешностей»!
💡 Подсказка: используйте клавиши ← → на клавиатуре для переключения слайдов