Интересно Вкусно Весело.
В скором времени буду добавлять информацию в данном разделе.
В скором времени буду добавлять информацию в данном разделе.
Арифметика JavaScript и PHP
Сегодня во время учебы Js, я столкнулся с интересной проблемой. Я написал простенький код:
Но как ни странно ответ получился - 0.19999999999999996 После долгих раздумий я решил выяснить из-за чего возникла данная проблема, и почему программа выводит совершенно не адекватный ответ для простой человеческой логике. На ум пришло только то-что JavaScript написан не верно, но это просто НЕ возможно. Тогда я решил проверить тоже самое в языке PHP. Ответ получился верным:
Где мне начали обьяснять что да как и почему так а не иначе. Ответ является таковым. Многие программисты годами пишут свои программы, не понимая, что такое числа с плавающей запятой, и чем они отличаются от "обычных", целых чисел. Это не мешает им создавать хорошие программы. Но в конце концов каждый сталкивается с "необъяснимым" явлением: $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"; } Проблема здесь, опять же, состоит в том, что сложно оценить размер возможных погрешностей — неизвестно, с какой максимальной точностью можно считать, чтобы погрешности не попали в результат. Выражать это число через размер переменных, с которыми мы работаем, как это сделано в примере — первый шаг, но он не решает всех проблем. Бесконечность и прочие вкусности Для чисел с плавающей запятой определены несколько специальных значений, которые весьма непривычны для программистов, привыкших к целочисленным операциям. Так, если взять самое большое целое число и прибавить к нему единицу, произойдёт переполнение, и число станет отрицательным. Если же прибавить единицу к самому большому числу с плавающей запятой, то не произойдёт ровным счётом ничего; в результате мы получим то же самое число. Это явление объясняется ниже. Переполнения можно добиться, к примеру, умножив это число на два. Но результат будет несколько необычным — "число" Inf (от англ. infinity = бесконечность). Аналогичным образом можно получить отрицательную бесконечность — -Inf. Бесконечность получается и при делении на ноль, причём и здесь она может быть как положительной, так и отрицательной (никакого исключения, как при работе с целыми числами, не возникает). И с ней действительно можно решать! Так, если разделить любое число на бесконечность, получится ноль. Произведение двух бесконечностей опять даёт бесконечность, как и сумма бесконечностей с одинаковым знаком. А вот сумма бесконечностей с разными знаками не определена, результатом получается NaN, другое специальное значение (от англ. Not a Number = не число). То же самое выйдет, если попытаться умножить бесконечность на ноль или поделить ноль на ноль. В некоторых языках программирования NaN является ещё и результатом неудачного преобразования строки в число. С NaN тоже можно решать, но результат любой операции будет опять же NaN. Ну и под конец ещё одно необычное явление: если в JavaScript написать 1/0, то результатом будет Inf, а вот 1/-0 вернёт -Inf. Для чисел с плавающей запятой действительно определены два нуля: положительный и отрицательный! К счастью, в программе это обычно не нужно учитывать. Оба нуля при сравнении равны и на выводе они, в большинстве языков программирования, тоже выглядят одинаково. Знак нуля важен только для операций деления и умножения. Поэтому во многих языках программирования нельзя даже определить константу со значением −0, она автоматически преобразуется в положительный ноль (именно по этой причине пришлось использовать JavaScript в примере). Это все я прочитал, но для меня осталось загадкой тот фактор , что , 1,1- 1 , в любом конечном итоге должен получиться 0,1 не зависимо сколько после него нулей. Можно точно так же сделать вывод , что 10-1 не равно 9 , а равно 9,000... и не понятному числу в конце. Меня интересует сам факт того, каким образом компьютер вычисляет 2 цифры и получает совершенно другое число. И в данном случае плавающая точка не фактор по которому можно делать выводы. А вот и ответ на него. а) и 1.1, и 1 компьютер сначала переводит в двоичную систему, причём первое число ОКРУГЛЯЕТ б) затем отнимает от одного двоичного числа другое двоичное в) в результате получает третье число - двоичную периодическую дробь, тоже ОКРУГЛЕННУЮ г) затем преобразует его двоичное представление в десятичное д) и вот тут-то из-за этой самой ОКРУГЛЕННОСТИ первого преобразования и возникает погрешность Но так же встает интересная неурядица , каким образом JavaScript и [COLOR="rgb(255, 140, 0)"]PHP[/COLOR] считают тот же самый алгоритм совершенно разным образом. А вот и ответ: считает компьютер точно также только вот последнее преобразование (пункт Г выше) происходит иначе где-то читал, что в PHP братья-израильтяне "вшили" небольшой "еврео-интеллектуальный" алгоритм округления результата в зависимости от представления исходных данных что-то вроде того, что если исходные числа-операнды написаны с точностью до 2-х знаков после запятой, то результат показывает не более 4-х знаков после запятой причем зависимость не прямая, а квадратичная: 0 знаков в исходных - 1 знак в результате 2 в исходных - 4 в результате ... 5 в исходных - 25 в результате И под конец хотелось бы добавить еще немножечко информации. Для закрепления знаний. Числа с плавающей точкой Числа с плавающей точкой (также известные как "float", "double", или "real") могут быть определены следующими синтаксисами: <?php $a = 1.234; $b = 1.2e3; $c = 7E-10; ?> Формально: LNUM [0-9]+ DNUM ([0-9]*[\.]{LNUM}) | ({LNUM}[\.][0-9]*) EXPONENT_DNUM [+-]?(({LNUM} | {DNUM}) [eE][+-]? {LNUM}) Размер числа с плавающей точкой зависит от платформы, хотя максимум, как правило составляет ~1.8e308 с точностью около 14 десятичных цифр (64-битный IEEE формат). Внимание Точность чисел с плавающей точкой Числа с плавающей точкой имеют ограниченную точность. Хотя это зависит от операционной системы, в PHP обычно используется формат двойной точности IEEE 754, дающий максимальную относительную ошибку округления порядка 1.11e-16. Неэлементарные арифметические операции могут давать большие ошибки, и, разумеется, необходимо принимать во внимание распространение ошибок при совместном использовании нескольких операций. Кроме того, рациональные числа, которые могут быть точно представлены в виде чисел с плавающей точкой с основанием 10, например, 0.1 или 0.7, не имеют точного внутреннего представления в качестве чисел с плавающей точкой с основанием 2, вне зависимости от размера мантиссы. Поэтому они и не могут быть преобразованы в их внутреннюю двоичную форму без небольшой потери точности. Это может привести к неожиданным результатам: например, floor((0.1+0.7)*10) скорее всего вернет 7 вместо ожидаемого 8, так как результат внутреннего представления будет чем-то вроде 7.9999999999999991118.... Так что никогда не доверяйте точности чисел с плавающей точкой до последней цифры, и не проверяйте напрямую их равенство. Если вам действительно необходима высокая точность, используйте математические функции произвольной точности и gmp-функции. Преобразование в число с плавающей точкой Информацию о преобразовании строк в числа с плавающей точкой смотрите в разделе Преобразование строк в числа. Для значений других типов преобразование будет сначала осуществлено в в integer и затем в число с плавающей точкой. Дополнительную информацию смотрите в разделе Преобразование к целому. Начиная с версии PHP 5, при преобразовании объекта к числу с плавающей точкой выводится замечание об ошибке. Сравнение чисел с плавающей точкой Как указано выше, проверять числа с плавающей точкой на равенство проблематично из-за их внутреннего представления. Тем не менее, существуют способы для их сравнения, которые работают несмотря на все эти ограничения. Для сравнения чисел с плавающей точкой используется верхняя граница относительной ошибки при округлении. Эта величина называется машинной эпсилон или единица округления(unit roundoff) и представляет собой самую маленькую допустимую разницу при расчетах. $a и $b равны до 5-ти знаков после запятой. <?php $a = 1.23456789; $b = 1.23456780; $epsilon = 0.00001; if(abs($a-$b) < $epsilon) { echo "true"; } ?> NaN Некоторые числовые операции могут возвращать значение, представляемое константой NAN. Данный результат означает неопределенное или непредставимое значение в операциях с плавающей точкой. Любое строгое или нестрогое сравнение данного значения с другим значением, включая его самого, возвратит FALSE. Так как NAN представляет собой неограниченное количество различных значений, то NAN не следует сравнивать с другими значениями, включая ее саму. Вместо этого, для определения ее наличия необходимо использовать функцию is_nan(). |
Всего комментариев 11
Комментарии
-
Запись от Зверушь размещена 13.12.2012 в 13:41 -
Запись от Kantaria размещена 13.12.2012 в 14:52 -
Запись от Зверушь размещена 14.12.2012 в 11:15 -
Запись от Kantaria размещена 14.12.2012 в 14:38 -
Запись от EPMAK размещена 14.12.2012 в 21:46
Обновил(-а) EPMAK 14.12.2012 в 21:47 -
Запись от Зверушь размещена 15.12.2012 в 22:24 -
Спасибо! Познавательно, изучая C# с таким не сталкивался, а вот с JS и PHP мог бы столкнуться.
Запись от Lovrentiy размещена 01.06.2013 в 11:03 -
Запись от Kantaria размещена 01.06.2013 в 11:25 -
Запись от BANO размещена 28.02.2016 в 12:31 -
Запись от Avazart размещена 29.02.2016 в 23:52
Обновил(-а) Avazart 29.02.2016 в 23:53 -
Запись от volodin661 размещена 03.10.2016 в 00:03