Форумы портала PHP.SU » PHP » Уроки php » Приложение к уроку № 1 - особенности вещественных типов данных.

Страниц (1): [1]
 

1. EuGen - 13 Июля, 2012 - 12:32:19 - перейти к сообщению
Приветствую,

Преамбула
Некоторое время материалы этого урока были "почти" готовы и теперь с некоторыми доработками я представлю их на изучение аудиторией.
Речь пойдет о некоторых особенностях вещественного типа в PHP.

О чем, собственно, идет речь
Сам предмет изучения - тип float в PHP. Он же - double. На деле float - это тип с так называемой "одинарной" точностью, а double - с "двойной" точностью. И это два разных типа данных. Однако в PHP это не так. Диапазон значений переменных в этом типе по-умолчанию следует стандарту 64-битного IEEE-формата, то есть ~1.8x10308 с точностью около 14 десятичных цифр. Однако следует учитывать, что это зависит от платформы.
В данном приложении будут рассмотрены две вещи о числах с плавающей точкой. Идем далее.

О том, что дискретное - не дискретно
По-хорошему (а точнее, алгебраически), тип данных с плавающей точкой есть "подделка" числовой прямой. То есть должен иметь непрерывные значения. Реализовать такое при современном уровне технологий не представляется возможным, так как и сама архитектура вычислительных машин - дискретна. Соответственно, вещественный тип данных, разумеется, не имеет мощности "континуум" (для любителей теории множеств - он, разумеется, не имеет и мощности "алеф ноль").
Из этого следует первая важная особенность: значения всех алгебраических функций будут представляться приближенно. Все иррациональные числа в представлении float - рациональны (ввиду ограничений точности). То есть иррациональные числа представлены приближенно.
Что же с рациональными? Увы, здесь тоже не все очевидно. Дело в том, что вещественный тип данных, как и целочисленный, представляется в двоичном виде. Это и понятно, ведь все операции происходят на архитектуре, построенном на двоичном представлении. Здесь мы подходим к одному важному вопросу - а как, собственно, этот тип данных устроен?

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



Здесь s - знак, m - мантисса, b - основание, e - порядок.
В нашем случае (то есть в случае вычислительных машин) основание почти всегда равно 2 (на деле это иногда не так, но в рамках этого приложения мы это не будем рассматривать).

Мантисса - это часть представления вещественного числа, которая составляет его старшие байты.
Порядок - это степень основания, в нашем случае - это степень двойки.

Вообще говоря, сколько бит использовать под мантиссу - не регламентируется этим представлением. Поэтому, теперь можно раскрыть тайну названия "плавающая точка" - она действительно "плавающая", так как при разном количестве бит под мантиссу, число будет представляться по-разному.
Так вот, к ответу на вопрос о рациональных числах - теперь уже слова из мануала:
Цитата:
Кроме того, рациональные числа, которые могут быть точно представлены в виде чисел с плавающей точкой с основанием 10, например, 0.1 или 0.7, не имеют точного внутреннего представления в качестве чисел с плавающей точкой с основанием 2, вне зависимости от размера мантиссы. Поэтому они и не могут быть преобразованы в их внутреннюю двоичную форму без небольшой потери точности. Это может привести к неожиданным результатам: например, floor((0.1+0.7)*10) скорее всего вернет 7 вместо ожидаемого 8, так как результат внутреннего представления будет чем-то вроде 7.9999999999999991118....

- не будут казаться непонятными. Итог - рациональные числа так же, как и иррациональные, в большинстве своем будут представляться неточно.

О практическом применении
Что это значит? То, что некоторые операции на типе данных float не будут выполняться корректно. Самый яркий пример - сравнение. Действительно, если все представляется приближенным, то речи о сравнении на равенство быть не может. Вместо этого обычно вводят константу, равную допустимой разнице между двумя значениями - допустимой для того, чтобы считать их равными. пример:
PHP:
скопировать код в буфер обмена
  1. $fLeft  = 0.5555558;
  2. $fRight = 0.5555550;
  3. $fDelta = 0.00001;
  4.  
  5. if(abs($fLeft-$fRight) < $fDelta)
  6. {
  7.     echo('Числа равны с точностью до '.floor(-log10($fDelta)).' знаков');
  8. }

-явно видно ее применение. Точно так же цикл:
PHP:
скопировать код в буфер обмена
  1. for($f=0; $f<=0.9;$f+=0.1)
  2. {
  3.    echo($f);
  4. }

- далеко не всегда выведет значения от 0 д 0.9 с шагом 0.1, иногда 0.9 может "потеряться". Если вместо этого записать :
PHP:
скопировать код в буфер обмена
  1. for($f=0; $f<1;$f+=0.1)
  2. {
  3.    echo($f);
  4. }

- то ряд значений будет полным и ожидаемым всегда.

Занимательная алгебра
С общими вопросами теперь должно быть более-менее понятно, теперь можно рассказать некоторые интересные особенности о типе float в PHP.
Дело в том, что в PHP реализованы некоторые "специальные значения" для типа float. Их три. Это

Нетрудно догадаться, что первое соответствует минус бесконечности, второе - плюс бесконечности, третье - непредставимому значению (Not A Number). Оговорюсь - непредставимому в виде вещественного числа. Это может быть, например, квадратный корень из -1.

Чтобы определить, является ли результат конечным числом, существует функция
is_finite
- а чтобы понять, является ли результат вещественным числом, существует функция
is_nan
Описание этих функций тривиально, поэтому здесь не рассматривается.

Однако самое занимательное еще только впереди! Дело в том, что в PHP эти значения созданы не просто "для галочки", они несут в себе конкретный алгебраический смысл.
Например, PHP может отличить, что:
0. Константа в сумме с бесконечностью есть бесконечность:
PHP:
скопировать код в буфер обмена
  1. var_dump(1-INF);
  2. //float(-INF)

1. Сумму бесконечностей одного знака есть бесконечность того же знака:
PHP:
скопировать код в буфер обмена
  1. var_dump(-INF-INF);
  2. //float(-INF)

2. Сумма бесконечностей разных знаков неопределена:
PHP:
скопировать код в буфер обмена
  1. var_dump(-INF+INF);
  2. //float(NAN)

Это - базовые операции. Однако еще более интересным является то, что PHP в курсе стандартных пределов функций:
3.
PHP:
скопировать код в буфер обмена
  1. //float(-INF)

4.
PHP:
скопировать код в буфер обмена
  1. //float(INF)

Но и это еще не все. Эти значения могут участвовать в выражениях. И давать вполне корректный с алгебраической точки зрения результат:
5.
PHP:
скопировать код в буфер обмена
  1. var_dump(pow(5, -INF));
  2. //float(0)

6.
PHP:
скопировать код в буфер обмена
  1. var_dump(atan(INF)/pi());
  2. //float(0.5)

Что же касается значения NAN, то здесь правило такое же, как и у любой СУБД при работе со значением NULL - выражение, любая часть которого в процессе вычисления равна NAN - будет равно NAN - что вполне логично.

Итак, вот такие любопытные операции с бесконечностями и их алгебраическим смыслом имеются в PHP. Можно сказать с некоторой долей правды, что в PHP "существует" понятие инфимума в алгебраическом смысле. В остальном же - устройство типа с плавающей точкой ничем не отличается от других языков.

 

Powered by ExBB FM 1.0 RC1