0

Floating point precision — why 0.1 + 0.2 !== 0.3

Intermediate5 min read·php-02-007
interviewcompare

Concept

Floating point numbers in PHP (and virtually every language that uses IEEE 754 double-precision binary) cannot represent most decimal fractions exactly. The value 0.1 is stored as the repeating binary fraction 0.000110011001100…, which rounds to approximately 0.1000000000000000055511151231257827021181583404541015625. When you add 0.1 + 0.2, the two approximations combine and the sum is 0.30000000000000004, not 0.3.

PHP uses 64-bit IEEE 754 doubles for all floating point values. This gives you 15–17 significant decimal digits of precision. The issue is not a PHP bug — it is a fundamental consequence of representing base-10 numbers in base-2. Languages like Java, C#, Python, and JavaScript all share this behaviour. The only languages immune to it use arbitrary-precision decimal arithmetic by default (e.g. certain Smalltalk implementations).

In production PHP, this causes real bugs: currency calculations that are off by a cent, discount calculations that fail equality checks, and sorting routines that produce wrong order. The correct solutions are: (1) use round() with an explicit precision before comparing, (2) use the BC Math extension (bcadd, bcsub, bccomp, bcdiv, bcmul) which operates on arbitrary-precision decimal strings, or (3) use GMP for integer arithmetic after scaling (e.g. store cents instead of dollars).

PHP's php.ini directive precision (default 14) controls how many significant digits are displayed by echo and print, not how many are stored. The directive serialize_precision (default -1 since PHP 7.1, meaning "use minimum digits to round-trip the value exactly") affects json_encode and serialize. A common gotcha: a value that looks like 0.3 when echoed may not === another value that also looks like 0.3.

When comparing floats, never use == or ===. Instead, check whether the absolute difference is smaller than an epsilon (tolerance). For financial work, always use BCMath or store amounts as integers (cents). The INF, -INF, and NAN special values produced by division by zero or overflow also require is_infinite(), is_nan(), and is_finite() — regular comparison with NAN always returns false.

ApproachAccuracySpeedUse case
Native float~15–17 sig. digitsFastestScientific, not financial
round($v, 2) then compareSufficient for displayFastUI display only
BCMath (bccomp)Arbitrary precisionModerateFinancial calculations
Store as integers (cents)ExactFastestCurrency in databases
GMPArbitrary integer precisionModerateCryptography, big integers

Code Example

php
<?php
declare(strict_types=1);

// The classic IEEE 754 trap
var_dump(0.1 + 0.2 === 0.3);   // bool(false)
var_dump(0.1 + 0.2);           // float(0.30000000000000004) with serialize_precision=-1

// What PHP actually stores
printf("%.55f\n", 0.1);
// 0.1000000000000000055511151231257827021181583404541015625

// Wrong: never compare floats with ==
$price = 0.1 + 0.2;
if ($price == 0.3) {
    echo "match";  // never prints
}

// Correct approach 1: epsilon comparison
$epsilon = 1.0e-9;
if (abs($price - 0.3) < $epsilon) {
    echo "match\n";  // prints
}

// Correct approach 2: round before comparing (display-only use cases)
if (round($price, 10) === round(0.3, 10)) {
    echo "rounded match\n";
}

// Correct approach 3: BCMath for financial work
$a = '0.1';
$b = '0.2';
$sum = bcadd($a, $b, 10);  // '0.3000000000'
var_dump(bccomp($sum, '0.3', 10) === 0);  // bool(true)

// BCMath division with explicit scale
$total  = bcdiv('1', '3', 20);  // '0.33333333333333333333'

// Storing currency as integers (recommended for DB)
$priceInCents = 1999;  // $19.99
$taxRate      = 8;     // 8%
$taxCents     = intdiv($priceInCents * $taxRate, 100);  // 159 cents
$totalCents   = $priceInCents + $taxCents;              // 2158 cents

// Special float values
var_dump(is_nan(sqrt(-1)));    // bool(true)
var_dump(is_infinite(1 / 0)); // bool(true) — PHP 8: E_DIVISION_BY_ZERO warning
var_dump(PHP_FLOAT_EPSILON);   // float(2.2204460492503E-16)
var_dump(PHP_FLOAT_MAX);       // float(1.7976931348623E+308)
var_dump(PHP_FLOAT_MIN);       // float(2.2250738585072E-308) — smallest positive normal float

Interview Q&A

Q: Why does 0.1 + 0.2 !== 0.3 in PHP, and how do you handle monetary calculations correctly in a Laravel e-commerce application?

PHP floats follow IEEE 754 double-precision binary, which cannot represent 0.1 or 0.2 exactly in binary — both are infinite repeating fractions. Their binary approximations sum to something slightly above 0.3, so the strict comparison fails. In a Laravel e-commerce context the canonical solution is to store all monetary values as integers in the database (cents, pence, etc.) and only convert to decimal for display. When BCMath arithmetic is needed — for example, applying a percentage discount — use bcmul, bcdiv, and bcround with an explicit scale parameter. The moneyphp/money library encapsulates this pattern and is widely used in production Laravel apps.


Q: What is the difference between PHP's precision and serialize_precision ini directives, and why did changing serialize_precision to -1 in PHP 7.1 matter?

precision controls how many significant digits PHP uses when casting a float to string for output via echo, print, or string concatenation (default 14). serialize_precision controls the digits used by serialize(), var_export(), and json_encode(). Before PHP 7.1, serialize_precision was 17, which caused json_encode(0.1) to produce "0.10000000000000001" — visually ugly and surprising. Setting it to -1 tells PHP to use the minimum number of digits required to uniquely identify the float, so json_encode(0.1) produces "0.1" while still guaranteeing round-trip accuracy. This is the G format in printf terms (%G). The practical impact: APIs suddenly returned cleaner JSON, but any code that relied on the old 17-digit format broke.


Q: What are PHP_FLOAT_EPSILON, PHP_FLOAT_MIN, and PHP_FLOAT_MAX, and when would you use each?

PHP_FLOAT_EPSILON (≈ 2.22e-16) is the smallest float $e such that 1.0 + $e !== 1.0 — it represents the machine epsilon for doubles. It is useful as an epsilon for comparing floats of magnitude around 1, but for values of different magnitudes you should scale the epsilon proportionally: abs($a - $b) < PHP_FLOAT_EPSILON * max(abs($a), abs($b)). PHP_FLOAT_MIN (≈ 2.22e-308) is the smallest positive normalized double — relevant in numerical algorithms where underflow to subnormal values would cause precision loss. PHP_FLOAT_MAX (≈ 1.80e+308) is the largest representable finite double, useful for initializing "find the minimum" loops. In everyday application code you mostly need PHP_FLOAT_EPSILON; BCMath or integer arithmetic is preferable to relying on any of these for financial logic.