From 186309e0a08b7a246b1d3c92d1729c972ff06a4e Mon Sep 17 00:00:00 2001 From: Marc Bennewitz Date: Mon, 7 Sep 2026 08:47:43 +0200 Subject: [PATCH] Build with SSE2 on x86 where supported SSE2 is part of the x86-64 baseline, but optional on 32-bit x86, where GCC targets i686 by default. Such builds miss the __SSE2__ code paths and compute doubles on the x87 unit. x87 registers have a 15-bit exponent that zend_init_fpu() cannot clamp, so subnormal results are rounded twice and can be one ULP off: $a = 5.678650331314532e-158; $b = 2.3715405351663569e-153; var_dump($a * $b); // x87: 1.34671494457485E-310 // SSE2 and 64-bit: 1.3467149445748E-310 (correctly rounded) The new PHP_CHECK_SSE2 macro in build/php.m4, called from the platform-specific compile settings in configure.ac, prepends to CFLAGS and CXXFLAGS on x86 targets: - -msse2 if supported and not enabled by default; - -mfpmath=sse if SSE2 is enabled and doubles do not use it by default. Clang does this by itself; GCC needs the flag. Configure warns but builds without SSE2 if the build machine cannot run SSE2 code. When cross compiling, -msse2 is added without that check. The resulting build requires SSE2. Pass CFLAGS=-mno-sse2 to build without SSE2. phpize builds do not call the macro, but an extension can call PHP_CHECK_SSE2 from its config.m4. --- UPGRADING | 6 ++ UPGRADING.INTERNALS | 4 + Zend/tests/float_prec_002.phpt | 25 +++++++ build/php.m4 | 129 +++++++++++++++++++++++++++++++++ configure.ac | 4 + 5 files changed, 168 insertions(+) create mode 100644 Zend/tests/float_prec_002.phpt diff --git a/UPGRADING b/UPGRADING index a4df23352639..93800477a567 100644 --- a/UPGRADING +++ b/UPGRADING @@ -180,6 +180,12 @@ PHP 8.7 UPGRADE NOTES 13. Other Changes ======================================== +- Core: + . 32-bit x86 builds now use SSE2 for floating point arithmetic by default, + so results match 64-bit builds; previously, subnormal results could be + one ULP off. Such builds require an SSE2 capable CPU; build with + CFLAGS=-mno-sse2 to keep the previous behaviour. + ======================================== 14. Performance Improvements ======================================== diff --git a/UPGRADING.INTERNALS b/UPGRADING.INTERNALS index 10f54d2395c1..bd4541d30daa 100644 --- a/UPGRADING.INTERNALS +++ b/UPGRADING.INTERNALS @@ -66,6 +66,10 @@ PHP 8.7 INTERNALS UPGRADE NOTES - Unix build system changes: . Autoconf minimum required version upgraded to 2.71. + . On x86, configure now builds with SSE2 and -mfpmath=sse. 32-bit x86 + builds therefore require an SSE2 capable CPU. Pass CFLAGS=-mno-sse2 to + build without SSE2. The new Autoconf macro PHP_CHECK_SSE2 implements + this, and extensions can use it in phpize/config.m4. ======================== 3. Module changes diff --git a/Zend/tests/float_prec_002.phpt b/Zend/tests/float_prec_002.phpt new file mode 100644 index 000000000000..4e3be55e6d1c --- /dev/null +++ b/Zend/tests/float_prec_002.phpt @@ -0,0 +1,25 @@ +--TEST-- +Float multiplication and division with subnormal results are correctly rounded +--DESCRIPTION-- +The x87 FPU rounds such results twice (into its wide exponent range, then when +storing the double), which can leave them one ULP away from the IEEE-754 +binary64 result. +--FILE-- + +--EXPECT-- +float(1.3467149445748E-310) +string(16) "000018ca74512179" +float(1.3140427690170504E-308) +string(16) "000972f0870d58e1" +float(3.42155699569866E-309) +string(16) "000275da670e8e41" diff --git a/build/php.m4 b/build/php.m4 index d8bf0221e83e..d1d4358db16e 100644 --- a/build/php.m4 +++ b/build/php.m4 @@ -2520,6 +2520,135 @@ AS_VAR_IF([php_cv_have_avx512vbmi], [yes], [Define to 1 if the compiler supports AVX-512 VBMI.])]) ]) +dnl +dnl PHP_CHECK_SSE2 +dnl +dnl Build for SSE2 on x86 targets that support it but do not enable it already. +dnl SSE2 is part of the x86-64 baseline, but optional on 32-bit x86, where the +dnl i686 baseline GCC targets by default predates it. +dnl +dnl -msse2 lets the compiler emit SSE2 instructions and defines __SSE2__. It is +dnl not added when the compiler does not honour it, including when CFLAGS turn +dnl SSE2 off again with -mno-sse2, or when a probe running SSE2 code fails. +dnl +dnl With SSE2 enabled, -mfpmath=sse is added as well, so that double arithmetic +dnl also uses SSE2 rather than the x87 unit. Clang does that by itself as soon +dnl as SSE2 is enabled; GCC needs the flag, and ignores it with only a warning +dnl when SSE2 is not enabled. x87 registers hold a 64-bit mantissa and a 15-bit +dnl exponent. zend_init_fpu() clamps the mantissa to 53 bits, which makes +dnl normal-range arithmetic behave like IEEE-754 binary64, but the exponent +dnl range cannot be clamped: a result that lands in the subnormal range is +dnl rounded once into the wide register and a second time when it is stored, and +dnl that double rounding can leave it one ULP away from the correctly rounded +dnl value. +dnl +dnl Neither flag is added when it is already in effect. +dnl +dnl On x86 the resulting build requires an SSE2 capable CPU. Pass +dnl CFLAGS=-mno-sse2 to build without SSE2. +dnl +AC_DEFUN([PHP_CHECK_SSE2], [dnl +php_sse2_flags= + +AC_CACHE_CHECK([whether the target is x86], [php_cv_target_x86], + [AC_COMPILE_IFELSE([AC_LANG_PROGRAM([], [[ +#if !defined(__i386__) && !defined(__x86_64__) +#error not x86 +#endif + ]])], + [php_cv_target_x86=yes], + [php_cv_target_x86=no])]) + +AS_VAR_IF([php_cv_target_x86], [yes], [dnl + php_sse2_save_CFLAGS=$CFLAGS + + AC_CACHE_CHECK([whether SSE2 is enabled by default], [php_cv_sse2_default], + [AC_COMPILE_IFELSE([AC_LANG_PROGRAM([], [[ +#ifndef __SSE2__ +#error SSE2 not enabled +#endif + ]])], + [php_cv_sse2_default=yes], + [php_cv_sse2_default=no])]) + + AS_VAR_IF([php_cv_sse2_default], [no], [dnl + CFLAGS="-msse2 $php_sse2_save_CFLAGS" + + AC_CACHE_CHECK([whether -msse2 takes effect], [php_cv_cc_msse2], + [AC_COMPILE_IFELSE([AC_LANG_PROGRAM([[#include ]], [[ +#ifndef __SSE2__ +#error SSE2 not enabled +#endif +__m128d x = _mm_set_sd(1.0); +(void) x; + ]])], + [php_cv_cc_msse2=yes], + [php_cv_cc_msse2=no])]) + + AS_VAR_IF([php_cv_cc_msse2], [yes], [dnl + AC_CACHE_CHECK([whether this machine can run SSE2 code], + [php_cv_sse2_runs], + [AC_RUN_IFELSE([AC_LANG_SOURCE([[ +#include +int main(void) +{ + volatile double a = 3.0, b = 7.0; + double c = _mm_cvtsd_f64(_mm_div_sd(_mm_set_sd(a), _mm_set_sd(b))); + return (c > 0.4 && c < 0.5) ? 0 : 1; +} + ]])], + [php_cv_sse2_runs=yes], + [php_cv_sse2_runs=no], + [php_cv_sse2_runs=cross])]) + + AS_CASE([$php_cv_sse2_runs], + [yes], [php_sse2_flags=-msse2], + [no], [AC_MSG_WARN( + [this machine cannot run SSE2 code; building without SSE2])], + [cross], [php_sse2_flags=-msse2]) + ]) + ]) + + CFLAGS="$php_sse2_flags $php_sse2_save_CFLAGS" + + AC_CACHE_CHECK([whether double arithmetic uses SSE2 by default], + [php_cv_sse2_math_default], + [AC_COMPILE_IFELSE([AC_LANG_PROGRAM([], [[ +#ifndef __SSE2_MATH__ +#error x87 math +#endif + ]])], + [php_cv_sse2_math_default=yes], + [php_cv_sse2_math_default=no])]) + + AS_IF([test "$php_cv_sse2_math_default" = no && + { test "$php_cv_sse2_default" = yes || test -n "$php_sse2_flags"; }], [dnl + CFLAGS="$php_sse2_flags -mfpmath=sse $php_sse2_save_CFLAGS" + + AC_CACHE_CHECK([whether -mfpmath=sse takes effect], [php_cv_cc_mfpmath_sse], + [AC_COMPILE_IFELSE([AC_LANG_PROGRAM([], [[ +#ifndef __SSE2_MATH__ +#error x87 math +#endif + ]])], + [php_cv_cc_mfpmath_sse=yes], + [php_cv_cc_mfpmath_sse=no])]) + + AS_VAR_IF([php_cv_cc_mfpmath_sse], [yes], + [php_sse2_flags="${php_sse2_flags:+$php_sse2_flags }-mfpmath=sse"]) + ]) + + CFLAGS=$php_sse2_save_CFLAGS +]) + +AC_MSG_CHECKING([for SSE2 compiler flags to add]) +AS_IF([test -n "$php_sse2_flags"], [dnl + CFLAGS="$php_sse2_flags $CFLAGS" + CXXFLAGS="$php_sse2_flags $CXXFLAGS" + AC_MSG_RESULT([$php_sse2_flags]) +], [AC_MSG_RESULT([none])]) +]) + dnl dnl PHP_CHECK_VARIABLE_ATTRIBUTE(attribute) dnl diff --git a/configure.ac b/configure.ac index abfddd1f28e4..393780c2d8e1 100644 --- a/configure.ac +++ b/configure.ac @@ -208,6 +208,10 @@ case $host_cpu in ;; esac +dnl Build for SSE2 on x86 where supported. The flags are prepended to CFLAGS, +dnl so that user flags such as -mno-sse2 still take precedence. +PHP_CHECK_SSE2 + dnl See https://github.com/php/php-src/issues/14140 AX_CHECK_COMPILE_FLAG([-ffp-contract=off], [CFLAGS="$CFLAGS -ffp-contract=off"])