The PHP floating point precision is wrong by default

Let me show you that PHP is bad at Math:

<?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:

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:

>>> 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:

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.

Fraction Base Positional Notation Rounded to 4 digits Rounded value as fraction Rounding error
1/10 10 0.1 0.1 1/10 0
1/3 10 0.3 0.3333 3333/10000 1/30000
1/2 2 0.1 0.1 1/2 0
1/10 2 0.00011 0.0001 1/16 3/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:

<?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:

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 list why 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.

; The number of significant digits displayed in floating point numbers.
; http://php.net/precision
precision = 17

If you want more background on this topic read the excellent article “What Every Computer Scientist Should Know About Floating-Point Arithmetic“.

Share

10 thoughts on “The PHP floating point precision is wrong by default”

  1. I agree that the default precision of 14 is not quite enough.

    When you say that ini_set(‘precision’) is the precision of the *representation* of floating point numbers – that is the key point I feel. ini_set(‘precision’) sets the *displayed* precision of floating point numbers when made stringy by whatever means (echo $float, (string)$float, json_encode(array(‘something’ => $float))).

    This is especially annoying when we consider PHP’s native microtime(true); which returns the current UNIX timestamp down to microsecond (ie. 6 digit) accuracy. With the default precision of 14, we don’t get the full 6 digits! We’ll get something like:

    1377467978.1234

    Instead of 1377467978.123456

    Obviously it is still 6 digits accurate internally, but a lot of things are moving towards text representations now (NoSQL databases etc) and so it becomes a bit important.

  2. @Daniel: I work a lot with Redis and unique identifiers do matter there as well. I still don’t know whether counters or timestamps are more suitable for identifiers. Collisions may occur when using timestamps, when there is enough concurrency. On the other hand counters scale worse, because you need a single unique identifier generator (service). Still your comment is very relevant, since I did not discuss the effects on the microtime command, which – as it seems – might benefit from a higher precision. Thank you for this smart and valuable comment!

  3. Hi,

    thanks for the explanation.

    Just asking, is using “bcmath” and functions like “bcadd” and “bccomp” will solve this problem ?

  4. @ismaail: You are welcome, thanks for commenting. And the answer is: Yes, using “bcmath” will solve the problem. The reason is written on http://www.php.net/manual/en/intro.bc.php. “For arbitrary precision mathematics PHP offers the Binary Calculator which supports numbers of any size and precision, represented as strings.”

  5. The first rule of floats, regardless of language, is to *never* compare them for equality; instead, check whether their absolute difference is less than the desired accuracy. For example “if (abs($x – $y) < 0.001) { … }".

    If your desired accuracy is e-14 then you should probably use a different number format, since rounding errors will destroy your data after only a few operations.

  6. After changing the precision from 14 to 17 in php.ini the output of round seems strange.

    Before:
    round(6.677321269973708, 2) -> 6.68

    After:
    round(6.677321269973708, 2) -> 6.6799999999999997

    Does this only reflect the change of display or do it need to worry when changing this running an existing codebase?

  7. @Mischa: Thank you for your comment. IMHO you should be concerned when using floating points to represent decimal numbers, regardless of the representation setting. Changing the setting will make debugging easier. Your code rounds to the closest floating point representation. When printing (converting to string) this may lead to a longer string when the representation setting is different. To illustrate:


    maurits@nuc:~$ php -a
    Interactive mode enabled

    php > var_dump("v=".round(6.677321269973708, 2)=='v=6.68');
    bool(true)
    php > ini_set('precision', 17);
    php > var_dump("v=".round(6.677321269973708, 2)=='v=6.68');
    bool(false)
    php >

    If you convert floating points to strings before comparing this change may impact the behavior of your system, so beware!

Leave a Reply

Your email address will not be published. Required fields are marked *