PHP is a weakly typed language, and this characteristic inevitably requires seamless and transparent implicit type conversion. Internally, PHP uses zval to store values of any type. The structure of zval is as follows (5.2 as an example):1
2
3
4
5
6
7struct \_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 stores the value itself is the zvalue_value union:1
2
3
4
5
6
7
8
9
10typedef 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 realize that long lval is not a fixed length — it varies with the compiler and the OS word size, and it may be 32 bits or 64 bits — whereas double dval (double precision) is specified by IEEE 754 and has a fixed length, always exactly 64 bits. Please keep this in mind, because it is what gives rise to the “platform dependence” of some PHP code. In the discussion that follows, unless specially noted, we assume that long is 64 bits I won’t quote IEEE 754’s floating-point notation here; those interested can look it up themselves. The key point is that a double stores its significand in 52 bits, and counting the hidden leading 1 bit, that is 53 bits in total. Here a rather interesting question comes up. Let’s use C code as an example (assuming long is 64 bits):1
2 long a = x;
assert(a == (long)(double)a);
The question is: within what range of values for a will the code above assert successfully? (The answer is left for the end of the article.) Now let’s get back on topic. Before PHP executes a script, it first has to read the script in and parse it, and this process also involves zval-izing the literals in the script. For example, for the following script:1
2
3
4
5<?php
$a = 9223372036854775807; //maximum value of a 64-bit signed number
$b = 9223372036854775808; //maximum value + 1
var_dump($a);
var_dump($b);
Output:1
2int(9223372036854775807)
float(9.22337203685E+18)
In other words, during the lexical analysis stage, for a numeric literal PHP will determine whether it exceeds the range of values that long can represent on the current system. If it does not, it is stored in lval and the zval is IS_LONG; otherwise it is represented with dval and the zval is IS_FLOAT. We must be careful with any value greater than the maximum integer value, because it may suffer a loss of precision:1
2
3
4
5<?php
$a = 9223372036854775807;
$b = 9223372036854775808;
var_dump($a === ($b - 1));
The output is false. Now, connecting back to the discussion at the beginning: as said earlier, PHP integers may be 32-bit or 64-bit, and that determines that some code which runs fine on 64 bits may suffer a loss of precision due to invisible type conversion, and thus fail to run properly on a 32-bit system. So we must be on our guard against this critical value — fortunately PHP has already defined it:1
2
3<?php
echo PHP\_INT\_MAX;
?>
Of course, to be on the safe side, we should store large integers as strings and use a math library such as bcmath to perform calculations. In addition, there is one more key configuration that can confuse us, and that is php.precision. This configuration determines how many significant digits PHP outputs when it prints a float value. Finally, let’s go back and look at the question raised above: namely, what is the largest value of a long integer such that converting it to a float and then back to a long will not lose precision? For example, for an integer we know its binary representation is 101. Now let’s shift it right by two places to get 1.01, discard the hidden leading significant 1, and we get that the binary value stored for 5 in a double is:
- 0/sign bit/ 10000000001/exponent bits/ 0100000000000000000000000000000000000000000000000000
The binary representation of 5 is stored in the significand part entirely intact; in this case, converting from double back to long will not lose precision. We know a double uses 52 bits for the significand, and counting the implicit leading 1, that is 53 bits of precision in total. From this we can conclude that if the value of a long integer is less than:
- 2^53 - 1 == 9007199254740991; //keep in mind, we are now assuming a 64-bit long
then this integer will not lose precision during a long->double->long numeric conversion. The original article comes from: http://www.laruence.com/2011/12/19/2399.html

