From 919ad92fab12730b47560a100d84ce608de6c6ef Mon Sep 17 00:00:00 2001 From: Marc Bennewitz Date: Fri, 4 Sep 2026 16:03:30 +0200 Subject: [PATCH 1/3] Add test for exp() precision on a double-precision-clamped FPU On i386 the x87 FPU is the platform default and zend_init_fpu() clamps it to double precision (53-bit mantissa) for the whole request. libm's exp() computes with x87 internally and needs the extended range to deliver a correctly rounded double, so while that clamp is in place exp() is off by one or more ULP compared to every platform whose FPU has no precision control. This matters because it makes exp() silently architecture-dependent: the same script prints different floats on i386 than on x86-64 or arm64, comparisons against precomputed constants fail, and the error propagates into anything built on exp(). Each 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. This test fails on i386 and passes wherever XPFPA_HAVE_CW is 0, which includes x86-64 and arm64. The following commit fixes i386. --- .../tests/math/exp_fpu_precision.phpt | 55 +++++++++++++++++++ 1 file changed, 55 insertions(+) create mode 100644 ext/standard/tests/math/exp_fpu_precision.phpt diff --git a/ext/standard/tests/math/exp_fpu_precision.phpt b/ext/standard/tests/math/exp_fpu_precision.phpt new file mode 100644 index 000000000000..d1ad2b06cbe9 --- /dev/null +++ b/ext/standard/tests/math/exp_fpu_precision.phpt @@ -0,0 +1,55 @@ +--TEST-- +exp(): results must not lose precision when the FPU is clamped to double precision +--INI-- +serialize_precision=-1 +--FILE-- + +--EXPECT-- +-- unaffected reference points -- +float(1) +float(2.718281828459045) +-- runtime calls -- +float(20.085536923187668) +float(148.4131591025766) +float(403.4287934927351) +float(1096.6331584284585) +float(2980.9579870417283) +float(8103.083927575384) +float(22026.465794806718) +float(162754.79141900392) +float(3269017.3724721107) +float(485165195.4097903) +-- constant argument gives the same result -- +bool(true) +bool(true) From 9dbb2c0473993e733bc306489208eb1a048485aa Mon Sep 17 00:00:00 2001 From: Marc Bennewitz Date: Fri, 4 Sep 2026 16:03:31 +0200 Subject: [PATCH 2/3] Use XPFPA for exp() Restore extended FPU precision around the libm exp() call and truncate the result back to double. Why this matters: zend_init_fpu() clamps the x87 FPU to a 53-bit mantissa for the whole request so that PHP's own double arithmetic behaves like IEEE-754 binary64. glibc's exp(), however, computes on the x87 unit and relies on the extended range to round correctly to double, so the clamp costs exp() accuracy. The loss is observable from userland and makes exp() silently architecture-dependent, which breaks float comparisons, cached or serialized results, and any computation that accumulates the error. Measured against e**x evaluated to 80 decimal digits and rounded to nearest double, over 720 sampled inputs: i386 before this change 85/720 correctly rounded (11.8%) i386 after this change 714/720 correctly rounded (99.2%) arm64, unaffected 720/720 correctly rounded (100%) This is an improvement, not a guarantee. exp(67) regresses by 1 ULP, because the clamped result there is genuinely the correctly rounded one. Five further inputs -- exp(128), exp(170), exp(198), exp(3.625) and exp(25.428571428571427) -- stay wrong, because their error is algorithmic rather than a matter of precision control: exp(3.625) returns the same incorrect value at both 53-bit and 64-bit precision, and only changes if the FPU is dropped to single precision. Bit-identical results across architectures would require a libm whose double routines are binary64-exact on i386, which PHP cannot influence. The accompanying test asserts only inputs that this change actually fixes. zend_exp() in zend_float.h compiles down to a plain exp() wherever XPFPA_HAVE_CW is 0, which includes x86-64 and arm64, so no other platform is affected. --- Zend/zend_float.h | 20 ++++++++++++++++++++ ext/standard/math.c | 2 +- 2 files changed, 21 insertions(+), 1 deletion(-) diff --git a/Zend/zend_float.h b/Zend/zend_float.h index 12aa02005c2a..81148e3b99ef 100644 --- a/Zend/zend_float.h +++ b/Zend/zend_float.h @@ -20,6 +20,8 @@ #include "zend_portability.h" +#include + BEGIN_EXTERN_C() /* @@ -413,4 +415,22 @@ END_EXTERN_C() #endif /* FPU CONTROL */ +/* zend_init_fpu() clamps the x87 FPU to double precision (53-bit mantissa) for + * the whole request. libm's exp() relies on the FPU running at extended + * precision internally, so while that clamp is in place its result is off by + * one or more ULP compared to platforms whose FPU has no precision control. + * Restore extended precision for the call and truncate the result back to + * double. Compiles down to a plain exp() everywhere XPFPA_HAVE_CW is 0, which + * includes x86-64 and arm64. */ +static zend_always_inline double zend_exp(double num) +{ +#if XPFPA_HAVE_CW + XPFPA_DECLARE + XPFPA_SWITCH_DOUBLE_EXTENDED(); + XPFPA_RETURN_DOUBLE(exp(num)); +#else + return exp(num); +#endif +} + #endif diff --git a/ext/standard/math.c b/ext/standard/math.c index e13f153f63e2..2640c99dc067 100644 --- a/ext/standard/math.c +++ b/ext/standard/math.c @@ -667,7 +667,7 @@ PHP_FUNCTION(exp) Z_PARAM_DOUBLE(num) ZEND_PARSE_PARAMETERS_END(); - RETURN_DOUBLE(exp(num)); + RETURN_DOUBLE(zend_exp(num)); } /* }}} */ From 954fbd0240dadd93a542355d631651056520cac1 Mon Sep 17 00:00:00 2001 From: Marc Bennewitz Date: Sat, 5 Sep 2026 10:46:03 +0200 Subject: [PATCH 3/3] Removed "unaffected reference points" from phpt On FreeBSD x86-64 the reference point for `exp(1)` failed with 1 ULP off by lib msun and completely unrelated to this PR. Expected Result float(2.718281828459045) Result on FreeBSD float(2.7182818284590455) --- ext/standard/tests/math/exp_fpu_precision.phpt | 7 ------- 1 file changed, 7 deletions(-) diff --git a/ext/standard/tests/math/exp_fpu_precision.phpt b/ext/standard/tests/math/exp_fpu_precision.phpt index d1ad2b06cbe9..83ac2da96f03 100644 --- a/ext/standard/tests/math/exp_fpu_precision.phpt +++ b/ext/standard/tests/math/exp_fpu_precision.phpt @@ -15,10 +15,6 @@ serialize_precision=-1 $x = 3; -echo "-- unaffected reference points --\n"; -var_dump(exp(0)); -var_dump(exp(1)); - echo "-- runtime calls --\n"; var_dump(exp($x)); var_dump(exp($x + 2)); @@ -36,9 +32,6 @@ var_dump(exp(10) === exp($x + 7)); var_dump(exp(20) === exp($x + 17)); ?> --EXPECT-- --- unaffected reference points -- -float(1) -float(2.718281828459045) -- runtime calls -- float(20.085536923187668) float(148.4131591025766)