Числа с плавающей запятой и их особенности
Xpoint.ru» База знаний» Статьи» Числа с плавающей запятой и их особенности По мотивам обсуждений: Объект Number и его особые свойства (бесконечности и нули), Ошибка в вычислениях чисел с плавающей запятой Многие программисты годами пишут свои программы, не понимая, что такое числа с плавающей запятой, и чем они отличаются от "обычных", целых чисел. Это не мешает им создавать хорошие программы. Но в конце концов каждый сталкивается с "необъяснимым" явлением: $a = 1.1 - 1;$b = 0.1;if ($a == $b){ print "$a равно $b";}else{ print "$a не равно $b";}Здесь и далее все примеры простоты ради приводятся на Perl. Они должны быть понятны и тем, кто не знаком с этим языком программирования, а обсуждаемые проблемы одинаковы для всех языков. Эта программа печатает "0.1 не равно 0.1". В чём дело? Напрашивается вывод, что в языке программирования что-то не в порядке. В сети можно найти немало переписок с разработчиками языков о подобных "ошибках". На самом же деле, этот пример демонстрирует некоторые важные свойства чисел с плавающей запятой — их эта статья и попытается объяснить. Оглавление
Как узнать, что используются числа с плавающей запятой? В языках программирования со строгой типизацией существуют, как правило, специальные типы данных для чисел с плавающей запятой (float/double/long double в Си, single/double/extended в Паскале). Если в вычислении участвует хотя бы одна переменная или константа с плавающей запятой, все другие числа тоже преобразовываются к этому типу. В языках без строгой типизации, как Perl, PHP или JavaScript, заметить использование чисел с плавающей запятой сложнее. Для программиста все числа выглядят одинаково, переключение с целочисленных типов на типы с плавающей запятой происходит автоматически. Можно исходить из того, что используются операции для чисел с плавающей запятой, если какая-нибудь из участвующих переменных содержит дробную часть или её значение выходит за пределы диапазона целых чисел. Но бывают и случаи, когда числа с плавающей запятой используются для целочисленных значений: $a = 0.5;$a *= 2;Здесь переменная $a равна единице (это покажет и сравнение, в отличие от примера в начале статьи), но её значение всё равно хранится как число с плавающей запятой потому, что её значение раньше содержало дробную часть. Некоторые языки, к примеру dBase, содержат дополнительный тип данных для чисел с фиксированной запятой. Они предназначены для более точных расчётов, к примеру в бухгалтерии. По сути, это "обычные" целые числа, у которых несколько последних знаков определены, как знаки после запятой. Соответственно, они и ведут себя так же, как целые числа. Всё, что написано в этой статье — не о них. Откуда берётся неточность? Основная причина неточности при использовании чисел с плавающей запятой в том, что компьютер не может работать с бесконечными дробями, которые мы знаем из математики — для них понадобилось бы бесконечное количество памяти. Это и понятно, мы тоже округляем числа до какого-то знака, когда имеем дело с десятичными дробями. Но это не объясняет приведённого в начале статьи примера — ведь там всего один знак после запятой? Существует ещё один фактор — компьютер считает не в десятичной системе, а в двоичной. А если представить 0.1 как двоичную дробь, то она окажется периодической: 0.0(0011). Соответственно, в памяти компьютера число 0.1 представлено как 1.1001100110011001100110011001100110011001100110011010b * 2^(-4). Обратите внимание на округление в конце числа. Если перевести его обратно в десятичную систему, то получится 0.10000000000000000555111512. Почему тогда показывается не это число, а 0.1? Дело в том, что числа с плавающей запятой на выводе всегда округляются. Perl, к примеру, по умолчанию выводит только пятнадцать значащих знаков. Но, если использовать функцию printf(), можно вывести до семнадцати значащих знаков, и тогда мы вдруг увидим 0.10000000000000001. В некоторых браузерах метод Number.toPrecision() JavaScript'а позволяет выводить числа даже с пятьюдесятью значащими знаками. Так почему всё-таки 1.1 − 1 не равно 0.1? Если посмотреть значение числа 1.1 − 1, то мы увидим 0.10000000000000008881784197. Оно, очевидно, не равно компьютерному представлению числа 0.1, хотя при стандартном округлении и выглядит точно так же. Разница объясняется тем, что мы считали с округлённой версией числа 1.1. Как бороться с погрешностями? Если использовать числа с плавающей запятой, то погрешность результатов оценить сложно. До сих пор не существует удовлетворительной математической теории, которая позволяла бы это делать. С другой стороны, погрешность при операциях с целыми числами оценивается легко. Поэтому одна возможность избавиться от неточностей — это использование чисел с фиксированной запятой, упомянутых выше. Эта возможность нашла наиболее широкое применении в финансовых программах, где заранее известно нужное количество знаков после запятой. Для других же приложений ограничения чисел с фиксированной запятой часто оказываются проблематичными — с их помощью нельзя представить ни очень большие числа, ни очень маленькие. Вместо этого можно дальше использовать числа с плавающей запятой, но при этом всегда учитывать, что возможны неточности. Так, при выводе результаты надо непременно округлять, причём часто автоматического округления недостаточно, и округление приходится задавать явно. Также надо быть осторожным с операцией сравнения. Можно округлять числа перед сравнением, либо, что более эффективно, смотреть на разницу чисел: if (abs($a - $b) < 0.000001 * min($a, $b)){ print "$a равно $b";}else{ print "$a не равно $b";}Проблема здесь, опять же, состоит в том, что сложно оценить размер возможных погрешностей — неизвестно, с какой максимальной точностью можно считать, чтобы погрешности не попали в результат. Выражать это число через размер переменных, с которыми мы работаем, как это сделано в примере — первый шаг, но он не решает всех проблем.
|