Friday, February 5, 2016

[PHP] Floating point precision is wrong

Let me show you that PHP is bad at Math:
1
2
3
4
<?php
echo "0.1 + 0.2 = ". ( 0.1 + 0.2 ) ."\n";
$true = 0.1 + 0.2 == 0.3 ? "Equal" : "Not equal";
echo "0.1 + 0.2 = 0.3 => $true\n";
Output:
1
2
0.1 + 0.2 = 0.3
0.1 + 0.2 = 0.3 => Not equal
Now, to analyze what is happening here let’s first look at JavaScript:
1
2
3
4
>>> 0.1 + 0.2
0.30000000000000004
>>> 0.1 + 0.2 == 0.3
false
Okay, so JavaScript is also bad at Math, but at least it prints numbers (better) as it internally represents them, making debugging easier. We see that the answer is slightly off and that is why the test is returning false. Before explaining why there are errors, let me give you some more examples of the problem:
1
2
3
0.1 * .1 results in 0.010000000000000002
0.7 + .1 results in 0.7999999999999999
1.1 + .1 results in 1.2000000000000002
As you can see the problem also exists when multiplying. This is because the problem does not lie in the operation, but in the way computers internally store numbers that contain a decimal point. The internal representation is called a “floating point “. This floating point representation has accuracy problems as we have shown above, but it doesn’t only apply to PHP floating points. The reason we have these accuracy problems is described in the floating point guide:
Because internally, computers use a format (binary floating-point) that cannot accurately represent a number like 0.1, 0.2 or 0.3 at all.
When the code is compiled or interpreted, your “0.1” is already rounded to the nearest number in that format, which results in a small rounding error even before the calculation happens.  — floating point guide
The floating point guide also explains clearly that:
…binary fractions are different from decimal fractions in what numbers they can accurately represent with a given number of digits, and thus also in what numbers result in rounding errors:
Specifically, binary can only represent those numbers as a finite fraction where the denominator is a power of 2. Unfortunately, this does not include most of the numbers that can be represented as finite fraction in base 10, like 0.1.
FractionBasePositional NotationRounded to 4 digitsRounded value as fractionRounding error
1/10100.10.11/100
1/3100.30.33333333/100001/30000
1/220.10.11/20
1/1020.000110.00011/163/80
And this is how you already get a rounding error when you just write down a number like 0.1 and run it through your interpreter or compiler. It’s not as big as 3/80 and may be invisible because computers cut off after 23 or 52 binary digits rather than 4. But the error is there and will cause problems eventually if you just ignore it. — floating point guide
Now let’s go back to the PHP floating point calculation and evaluate this code:
1
2
3
4
5
<?php
ini_set('precision', 17);
echo "0.1 + 0.2 = ". ( 0.1 + 0.2 ) ."\n";
$true = 0.1 + 0.2 == 0.3 ? "Equal" : "Not equal";
echo "0.1 + 0.2 = 0.3 => $true\n";
Output:
1
2
0.1 + 0.2 = 0.30000000000000004
0.1 * 0.2 = 0.3 => Not equal
That is more like it. It does not solve the problem, but makes it easier to understand. Note that we have set the “precision” of the representation of floating point numbers to 17 with the “ini_set” PHP command. Gustavo Lopes explains on the php-internals mailing listwhy other values (like 100) do not make sense:
Given that the implicit precision of a (normal) IEEE 754 double precision number is slightly less than 16 digits [2], this is a serious overkill. Put another way, while the mantissa is composed of 52 bits plus 1 implicit bit, 100 decimal digits can carry up to 100*log2(10) =~ 332 bits of information, around 6 times more.
Given this, I propose changing the default precision to 17 (while the precision is slightly less than 16, a 17th digit is necessary because the first decimal digit carries little information when it is low). — source
So for now, let’s change the precision to from 14 to 17 in “/etc/php5/apache2/php.ini” on our servers and save ourselves some headaches when we are using PHP floating points.
1
2
3
; The number of significant digits displayed in floating point numbers.
; http://php.net/precision
precision = 17