Перейти к основному содержимому

Числа с плавающей точкой

Предупреждение из руководства PHP​

Числа с плавающей точкой имеют ограниченную точность. Хотя это зависит от системы, PHP обычно использует формат двойной точности IEEE 754, который даёт максимальную относительную ошибку округления порядка 1.11e-16. Неэлементарные арифметические операции могут давать большие ошибки, и, разумеется, необходимо учитывать распространение погрешности при выполнении цепочки операций.

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

Поэтому никогда не доверяйте результатам вычислений с плавающей точкой до последнего знака и не сравнивайте числа с плавающей точкой напрямую на равенство. Если необходима повышенная точность, доступны функции произвольной точности (BCMath) и функции GMP.

Простой пример (с floating-point-gui.de)​

Во многих языках программирования, включая PHP:

echo 0.1 + 0.2; // 0.30000000000000004

Это происходит потому, что 0.1 и 0.2 не могут быть точно представлены в двоичной системе.

Почему так происходит? (Простое объяснение)​

Числа с плавающей точкой обычно реализованы как двоичная дробь (основание 2), а не как десятичная дробь (основание 10).

  • В десятичной системе дробь вроде 1/3 не может быть представлена точно (это 0.33333...).
  • Аналогично, в двоичной системе дроби вроде 1/10 (0.1) или 1/5 (0.2) становятся периодическими дробями.

Исключение для 0.5: Число 0.5 может быть точно представлено в двоичной системе, поскольку является степенью двойки ($2^-1$ или $1/2$).

var_dump(0.5 + 0.5 == 1.0); // true (0.5 точно представимо)

Подождите, что? Когда вы складываете 0.1 + 0.2, на самом деле складываются:

  • 0.1 (внутреннее представление: 0.1000000000000000055511151231257827021181583404541015625)
  • 0.2 (внутреннее представление: 0.200000000000000011102230246251565404236316680908203125)

Результат — 0.3000000000000000444089209850062616169452667236328125, именно поэтому 0.1 + 0.2 == 0.3 возвращает false.

Что можно сделать?​

  1. Никогда не используйте == для сравнения float. Используйте малое значение «эпсилон» (точность) для сравнения:
    $epsilon = 0.00001;
    if (abs(($a + $b) - $c) < $epsilon) {
    // Значения «достаточно равны»
    }
  2. Используйте библиотеки произвольной точности, такие как BCMath или GMP (см. ниже).
  3. Используйте целые числа для денег. Храните $1.23 как 123 центов, чтобы полностью избежать проблем с плавающей точкой.

Как этого избежать: BC Math (арифметика произвольной точности)​

Для арифметики произвольной точности PHP предоставляет BC Math Binary Calculator, который поддерживает числа любого размера и точности, представленные в виде строк.

Функции BCMath:​

  • bcadd — сложение двух чисел произвольной точности
  • bcceil — округление вверх числа произвольной точности
  • bccomp — сравнение двух чисел произвольной точности
  • bcdiv — деление двух чисел произвольной точности
  • bcdivmod — получение частного и остатка от деления числа произвольной точности
  • bcfloor — округление вниз числа произвольной точности
  • bcmod — получение остатка от деления числа произвольной точности
  • bcmul — умножение двух чисел произвольной точности
  • bcpow — возведение числа произвольной точности в степень
  • bcpowmod — возведение числа произвольной точности в степень по модулю
  • bcround — округление числа произвольной точности
  • bcscale — установка или получение масштаба по умолчанию для всех функций BCMath
  • bcsqrt — извлечение квадратного корня из числа произвольной точности
  • bcsub — вычитание одного числа произвольной точности из другого

Пример использования BCMath:​

<?php
// Установка глобального масштаба для всех функций BCMath
bcscale(3);

$a = '1.234567';
$b = '2.345678';

echo bcadd($a, $b); // 3.580 (используется масштаб по умолчанию 3)
echo bcadd($a, $b, 6); // 3.580245 (явный масштаб)
echo bccomp($a, $b, 6); // -1 (означает $a < $b)
echo bcmul($a, '2', 2); // 2.46
?>

GMP (GNU Multiple Precision)​

GMP позволяет работать с целыми числами произвольной длины.

Важное замечание о различиях между GMP и BCMath:​

  • Поддержка типов чисел: GMP работает исключительно с целыми числами произвольной точности, тогда как BCMath поддерживает десятичные (аналогичные числам с плавающей точкой) значения произвольной точности.
  • Производительность: GMP значительно быстрее, чем BCMath.
  • API: GMP использует ресурсный (или объектный в новых версиях) API, тогда как BCMath работает преимущественно со строками.

Основные функции GMP:​

  • gmp_abs — абсолютное значение
  • gmp_add — сложение чисел
  • gmp_cmp — сравнение чисел
  • gmp_div_q — деление чисел
  • gmp_div_qr — деление чисел с получением частного и остатка
  • gmp_div_r — остаток от деления чисел
  • gmp_init — создание числа GMP из строки или целого числа
  • gmp_intval — преобразование числа GMP в целое число
  • gmp_mul — умножение чисел
  • gmp_pow — возведение числа в степень
  • gmp_strval — преобразование числа GMP в строку
  • gmp_sub — вычитание чисел

Пример использования GMP:​

<?php
// Работа с большими целыми числами
$a = gmp_init("12345678901234567890");
$b = gmp_init("98765432109876543210");

$sum = gmp_add($a, $b);
echo gmp_strval($sum); // 111111111011111111100

// Объекты GMP поддерживают операторы (начиная с PHP 5.6+)
$product = $a * $b;
echo gmp_strval($product);

// Сравнение
if (gmp_cmp($a, $b) < 0) {
echo "$a меньше, чем $b";
}
?>