[php-src] PR #23578: Fix pow(), ** and fpow() losing precision on 32-bit x86
| From: | marc-mabe | Date: | Sat, 05 Sep 2026 04:31:49 +0000 |
| Subject: | [php-src] PR #23578: Fix pow(), ** and fpow() losing precision on 32-bit x86 | ||
| Groups: | php.git-pulls | ||
| Request: | Send a blank email to git-pulls+get-38317@lists.php.net to get a copy of this message | ||
Pull Request: https://github.com/php/php-src/pull/23578
Author: marc-mabe
### Problem
On 32-bit x86, the
** operator, pow() and fpow() return
results that are off by one or more ULP compared to every other platform:
```php
var_dump(10 ** -2); // i386: float(0.010000000000000002)
// other: float(0.01)
var_dump(5 ** -4); // i386: float(0.0016000000000000007)
// other: float(0.0016)
var_dump(fpow(10.0, -2.0)); // i386: float(0.010000000000000002)
// other: float(0.01)
var_dump(10 ** -2 === 0.01); // i386: false, everywhere else: true
```
The results are not merely different, they are less accurate: measured against a high-precision
reference, the i386 values are not correctly rounded while the x86-64 and arm64 ones are. This makes
exponentiation silently architecture-dependent, so float comparisons against literals fail, cached
or serialized values differ between machines, and the error propagates into anything built on top.
### Cause
init_executor() calls zend_init_fpu(), which clamps the x87 FPU to double
precision (a 53-bit mantissa) for the whole request so that PHP's own double
arithmetic behaves like IEEE-754 binary64 rather than carrying x87's 64-bit excess precision.
libm's pow(), however, computes on that same x87 unit and relies on the extended
range internally in order to round correctly to double. While the clamp is in place it
loses that headroom, so pow() inside PHP is less accurate than the very same libm
pow() is outside PHP.
### Fix
zend_pow() in Zend/zend_float.h restores extended precision for the
duration of the libm call and truncates the result back to double on return, using the
XPFPA macros already in that header. Both call sites go through it:
- safe_pow() in Zend/zend_operators.c, which backs the **
operator and pow()
- PHP_FUNCTION(fpow) in ext/standard/math.c, which called libm
pow() directly and so did not benefit from a fix to safe_pow() alone
Wherever XPFPA_HAVE_CW is 0 — x86-64, arm64, and every other target without x87
precision control — zend_pow() compiles down to a plain pow() call, so
no other platform is affected in behaviour or code generation.
The i386 failure is exactly the precision assertions; the reference points and the IEEE-754 cases
pass in both states.
### Notes
* I came across this by my work on #19079 where some tests were failing because of these differences
* This PR is mostly LLM work - I manually verified
* This is the first PR - PRs for exp, expm1 and sinh will
follow