[php-src] PR #23579: Fix exp() losing precision on 32-bit x86
| From: | marc-mabe | Date: | Sat, 05 Sep 2026 05:29:21 +0000 |
| Subject: | [php-src] PR #23579: Fix exp() losing precision on 32-bit x86 | ||
| Groups: | php.git-pulls | ||
| Request: | Send a blank email to git-pulls+get-38318@lists.php.net to get a copy of this message | ||
Pull Request: https://github.com/php/php-src/pull/23579
Author: marc-mabe
### Problem
On 32-bit x86,
exp() returns results that are off by one or more ULP compared to other
platforms, and — more importantly — that are not correctly rounded:
```php
var_dump(exp(3)); // i386: float(20.08553692318767)
// other: float(20.085536923187668)
var_dump(exp(10)); // i386: float(22026.465794806725)
// other: float(22026.465794806718)
var_dump(exp(20)); // i386: float(485165195.40979075)
// other: float(485165195.4097903)
```
Measured against e**x evaluated to 80 decimal digits and rounded to nearest double, over 720 sampled
inputs:
| build | correctly rounded |
| --- | --- |
| i386, current | 85/720 (11.8%) |
| x86-64 / arm64 | 720/720 (100%) |
So this is not a case of two platforms disagreeing about an arbitrary last bit: i386 is wrong and
the others are right. It makes exp() silently architecture-dependent, so comparisons
against precomputed constants fail, cached or serialized values differ between machines, and the
error propagates into anything built on exp().
### 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 exp(), 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 exp() inside PHP is markedly less accurate than the very same
libm exp() is outside PHP.
### Fix
zend_exp() 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. PHP_FUNCTION(exp) calls it.
Wherever XPFPA_HAVE_CW is 0 — x86-64, arm64, and every other target without x87
precision control — zend_exp() compiles down to a plain exp() call, so
no other platform is affected in behaviour or code generation.
Result on i386, same 720 inputs and same reference:
| build | correctly rounded |
| --- | --- |
| i386, before | 85/720 (11.8%) |
| i386, after | 714/720 (99.2%) |
### Known limitations
This is a large improvement, not a guarantee, and the remaining cases are worth stating explicitly:
- exp(67) **regresses** by 1 ULP. The clamped result there is genuinely the correctly
rounded one, so this input is made worse. It is the only regression in the sample.
- Five inputs stay wrong either way: exp(128), exp(170),
exp(198), exp(3.625) and exp(25.428571428571427). Their error
is algorithmic rather than a matter of precision control — exp(3.625) returns the
identical incorrect value at both 53-bit and 64-bit precision, and only changes if the FPU is
dropped to single precision.
Reaching bit-identical results across architectures would require a libm whose double routines are
binary64-exact on i386, which is a toolchain/distribution matter outside PHP's control. 99.2%
appears to be the practical ceiling from PHP's side.
The test asserts only inputs that this change fixes; the six cases above are deliberately excluded.
### Tests
ext/standard/tests/math/exp_fpu_precision.phpt.
Every expected value is the correctly rounded double, written as the shortest decimal that
round-trips back to it. exp(0) and exp(1) are included as reference points
that the clamp does not affect, so a failure points at the affected inputs rather than at the whole
function.
Verified locally with a full make clean + ./configure + make
per configuration, on i386 and arm64:
| | without fix | with fix |
| --- | --- | --- |
| arm64 | pass | pass |
| i386 | **fail** | pass |
The i386 failure is exactly the ten precision assertions; the two reference points and both identity
checks pass in either state.
### Notes
* This is the same class of bug as #23578 fix and adds a helper to the same part of
Zend/zend_float.h
* This PR is mostly LLM work - I manually verified
* This is the second PR - PRs for expm1 and sinh will follow