Press "Enter" to skip to content

What You Should Know About Floats in PHP (All 'bogus' about the float in PHP)

PHP is a weakly typed language. That property inevitably requires seamless, transparent implicit type conversion. Internally PHP uses a zval to hold a value of any type. The zval structure looks like this (using 5.2 as an example):

struct _zval_struct {
    /* Variable information */
    zvalue_value value;     /* value */
    zend_uint refcount;
    zend_uchar type;    /* active type */
    zend_uchar is_ref;
};

In the structure above, what actually holds the value itself is the zvalue_value union:

typedef union _zvalue_value {
    long lval;                  /* long value */
    double dval;                /* double value */
    struct {
        char *val;
        int len;
    } str;
    HashTable *ht;              /* hash table value */
    zend_object_value obj;
} zvalue_value;

In today's topic we only care about two of its members, lval and dval. We need to be aware that long lval does not have a fixed width — it varies with the compiler and the OS word size, so it may be 32 bits or 64 bits. Whereas double dval (double precision) is defined by IEEE 754, has a fixed width, and is always 64 bits.
Please remember this point, because it is what makes some PHP code "not platform-independent". In the discussion that follows, unless explicitly stated otherwise, we assume long is 64 bits
I won't quote the IEEE 754 floating-point representation here — if you're interested you can look it up yourself. The key point is that a double's mantissa is stored in 52 bits; counting the hidden leading significant bit, that's 53 bits in total.
This raises a very interesting question. Let me illustrate with C code (assuming long is 64 bits):

    long a = x;
    assert(a == (long)(double)a);

Question: within what range of values for a will the assertion above succeed? (I'll answer at the end of the article)
Now back to the main topic. Before PHP executes a script, it first has to read the script and parse it. This process includes turning the literals in the script into zvals. For example, given the following script:

<?php
$a = 9223372036854775807; //max value of a 64-bit signed integer
$b = 9223372036854775808; //max value + 1
var_dump($a);
var_dump($b);

Output:

int(9223372036854775807)
float(9.22337203685E+18)

That is to say, during lexical analysis PHP checks whether a numeric literal exceeds the representable range of the current system's long. If it doesn't, it's stored in lval and the zval type is IS_LONG; otherwise it's represented with dval and the zval type is IS_FLOAT.
Any number greater than the maximum integer value needs care, because it may lose precision:

<?php
$a = 9223372036854775807;
$b = 9223372036854775808;
var_dump($a === ($b - 1));

The output is false.
Now, picking up the thread from the beginning: as I said, a PHP integer may be 32 bits or may be 64 bits. That determines that some code which runs perfectly fine on 64-bit systems may suffer precision loss because of implicit type conversion, and therefore fail to run correctly on 32-bit systems.
So we must be wary of this threshold value. Fortunately PHP already defines it:

<?php
    echo PHP_INT_MAX;
 ?>

Of course, to be safe, we should use strings to hold big integers and do the arithmetic with a math library such as bcmath.
In addition, there's another key configuration that tends to confuse us: php.precision. This setting determines how many significant digits PHP outputs when printing a float value.
Finally, let's come back to the question posed above: what is the largest value of a long integer such that converting it to a float and back to a long won't lose precision?
For instance, take an integer whose binary representation is 101. Now shift it right by two places to get 1.01. Dropping the implied leading significant bit 1, the binary value stored in a double for 5 is:

0/*sign bit*/ 10000000001/*exponent bits*/ 0100000000000000000000000000000000000000000000000000

The binary representation of 5 is stored in the mantissa without the slightest loss. In this case, converting from double back to long won't lose precision.
We know a double uses 52 bits for the mantissa; counting the implied leading 1, that's 53 bits of precision. From that we can conclude: if a long integer's value is less than:

2^53 - 1 == 9007199254740991; //remember, we're currently assuming a 64-bit long

then that integer will not lose precision when it goes through a long->double->long conversion.

Be First to Comment

Leave a Reply

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

This site uses Akismet to reduce spam. Learn how your comment data is processed.