Чему равен (√2)²
Напишем небольшую программу. Каждый из вас может использовать свой любимый язык.
a = Sqrt(2);
WriteLine(a*a);
Здесь Sqrt -- функция извлечения квадратного корня, а WriteLine выводит в консоль значение параметра. Запустите и посмотрите, что вы получите. У меня на C# вышло 2,0000000000000004. Почему так?
Не все числа можно представить
Корень из двух -- бесконечная дробь, а для записи значения числа в памяти компьютера отводится конечное число байт. У нас нет возможности, представить √2 точно. Поэтому a*a в операторе вывода не равно (√2)².
С корнем из двух проблема ожидаема, поскольку это иррациональное число. Но возьмем небольшое рациональное: 5/100. Переведем его в двоичную систему:
0.05₁₀ ≈ 0.00001100110011001100110011001100110011001100110011₂
Получается периодическая бесконечная дробь. Выходит мы не можем представить точно и это число. Это приводит к пониманию, что числовая прямая компьютера не совпадает с привычной нам в математике, она не является непрерывной. Результатом выражения может быть не представимое число и будет использовано его приближение.
Не всё одинаковое равно
Поэкспериментируем с числом 0.05 (программа на C#).
float x = 0.05f;
double y = 0.05;
decimal z = 0.05M;
Console.WriteLine(x == y);
Console.WriteLine(x == z);
Console.WriteLine(y == z);
Что получим в результате? Ничего -- второй и третий операторы вывода не скомпилируются с сообщением, что "==" к данным операндам не применим. Если их убрать, то оставшийся выведет false. Всё становится ясно, если вспомнить про бесконечную дробь. X и y содержат разные приближения числа 0.05. Вот и не равны.
Скачущее округление
В Java Script есть функция toFixed, преобразующая число в строку в десятичном представлении с округлением до указанного числа десятичных знаков. Используем ее в простой программе:
console.log(0.325.toFixed(2));
console.log(10.325.toFixed(2));
console.log(100.325.toFixed(2));
console.log(500.325.toFixed(2));
Программа простая, а вот результат озадачивает:
0.33
10.32
100.33
500.32
Однако, если мы увеличим число десятичных знаков до 20, то приоткроем завесу тайны:
0.32500000000000001110
10.32499999999999928946
100.32500000000000284217
500.32499999999998863132
Здесь опять невозможность точного представления приводит к не ожидаемому результату. Стоит однако отметить, что другие языки как-то с этим справляются. Округление в C# для указанного набора дает стабильные 33 после десятичной точки.
Длинные суммы
Возьмем дельту равную 0.001 и просуммируем ее миллион раз. А после сравним сумму с произведением дельты на миллион. На C# с double получились следующие результаты:
Произведение: 1000;
Сумма: 999,9999999832651
Если еще посчитать число случаев, когда сумма была равна произведению дельты на номер итерации, все станет еще печальнее:
Сумма и произведение равны: 29
Сумма и произведение не равны: 999971
Всего итераций: 1000000
Последнее равенство: 292296
Первое расхождение: 10
А вот для дельты равной 0.001953125 все прекрасно:
Произведение: 1953.125;
Сумма: 1953.125
Сумма и произведение равны: 1000000
Сумма и произведение не равны: 0
Всего итераций: 1000000
Последнее равенство: 999999
Первое расхождение: нет расхождений
Секрет такого счастья: 0.001953125 = 1/512 = 1/2⁹ и имеет точное представление.