While browsing bugs.php.net, I found an interesting ticket. Someone reported a surprising behavior, and I wanted to understand what is happening under the hood—because I think this is a really important concept to understand in PHP internals.
Consider the following script and its output:
<?php
$NaN = sqrt(-1);
$array = [$NaN];
var_dump($array === $array); // always true
var_dump([$NaN] === [$NaN]); // always false
At first glance, this looks confusing. Let’s investigate it line by line.
Generating NaN
The first line contains a mathematical expression that is not defined in the real numbers: $NaN = sqrt(-1);
PHP delegates this operation to its internal math implementation in ext/standard/math.c, which ultimately relies on the system’s math library (for example, glibc on Linux, Apple libc on macOS, or musl on Alpine). These libraries follow the IEEE 754 floating-point standard. According to this standard, operations that are mathematically undefined (such as the square root of a negative number) produce NaN (“Not a Number”). Importantly, NaN is never equal to anything, including itself. This is a fundamental rule of IEEE 754 floating-point arithmetic.
Putting NaN into an array
Next, we store this NaN value in an array:
$array = [$NaN];
Comparing the same array
Now we compare the array to itself:
var_dump($array === $array);
In PHP, the === operator performs a strict comparison, meaning:
same type same value Here, both operands refer to the same variable, so the comparison returns true. This is effectively an identity shortcut. Internally, both $array operands point to the same zval.
A short note about zvals
A zval (Zend value) is the fundamental data structure used by the Zend Engine to represent all PHP variables. Every variable in PHP is stored internally as a zval. PHP 7 redesigned the zval structure to be significantly more memory-efficient and faster compared to PHP 5.
A zval is essentially a container that holds:
- the value
- the type information
- flags related to references and garbage collection
PHP uses a copy-on-write (CoW) mechanism for efficiency. Multiple variables can point to the same underlying value without duplicating memory. Only when a modification occurs does PHP create a copy, ensuring changes don’t affect other variables.
Comparing two distinct arrays
Finally, let’s look at this line:
var_dump([$NaN] === [$NaN]);
Here, we are comparing two distinct arrays, even though their contents look identical. To understand why this returns false, let’s briefly follow the relevant internal comparison logic in PHP:
zend_is_identical(op1, op2), (both operands are arrays):
- zend_compare_arrays()
- zend_compare_symbol_tables()
- zend_hash_compare()
- hash_zval_identical_function()
- zend_is_identical(v1, v2)
During this process, PHP compares each element of the arrays using strict identity comparison. Eventually, the comparison reaches the array elements themselves: NaN === NaN
According to the IEEE 754 standard, NaN is never equal to itself, so this comparison returns false. Because the first (and only) elements of the arrays are not identical, the entire array comparison fails.
Conclusion
This behavior is not a bug in PHP, but a direct consequence of how floating-point numbers and strict comparisons are defined and implemented.
This example highlights why understanding PHP internals—such as zvals, copy-on-write, and strict comparison semantics—is crucial when working with edge cases involving floating-point numbers. What may initially look like inconsistent behavior is actually a predictable and well-defined result of PHP’s internal design.
Top comments (0)