[libc-commits] [libc] [libc][math] Prevent spurious floating-point exceptions in fdim (PR #228749)

via libc-commits libc-commits at lists.llvm.org
Sat Oct 3 10:43:15 PDT 2026


llvmorg-github-actions[bot] wrote:


<!--LLVM PR SUMMARY COMMENT-->

@llvm/pr-subscribers-libc

Author: Chen Miao (ChenMiaoi)

<details>
<summary>Changes</summary>

Without `FENV_ACCESS` enabled, Clang 22 at `-O2` optimizes the conditional expression in `fdim`:

```cpp
return x > y ? x - y : T(0);
```

into the following LLVM IR:

```llvm
%cmp = fcmp ogt double %x, %y
%diff = fsub double %x, %y
%result = select i1 %cmp, double %diff, double 0.000000e+00
```

The `fsub` is not guarded by `x > y`. Even when `x <= y`, the subtraction executes before `select` chooses zero as the result. The `select` only chooses a value; it cannot undo floating-point exceptions already raised by the subtraction. Consequently, `fdim(-inf, -inf)` raises `FE_INVALID`, `fdim(-max, -min_subnormal)` raises `FE_INEXACT`, and `fdim(-max, max)` can raise `FE_OVERFLOW` and `FE_INEXACT`, despite all these calls returning zero.

Without floating-point environment access enabled, the compiler need not preserve the observable behavior of these exceptions and can speculate the subtraction. Simply rewriting the conditional expression as an `if` statement does not reliably prevent this optimization.

Enable `FENV_ACCESS` in the shared implementation so the subtraction uses `llvm.experimental.constrained.fsub` with dynamic rounding and strict exception semantics. The optimized IR keeps the subtraction in the `x > y` branch, avoiding spurious exceptions on the path that returns zero. Enable `FENV_ACCESS` in the public entry points as well to allow the strict floating-point implementation to be inlined.

Fixes #<!-- -->88771

---
Full diff: https://github.com/llvm/llvm-project/pull/228749.diff


8 Files Affected:

- (modified) libc/src/__support/FPUtil/BasicOperations.h (+4) 
- (modified) libc/src/math/generic/fdim.cpp (+1) 
- (modified) libc/src/math/generic/fdimbf16.cpp (+1) 
- (modified) libc/src/math/generic/fdimf.cpp (+1) 
- (modified) libc/src/math/generic/fdimf128.cpp (+1) 
- (modified) libc/src/math/generic/fdimf16.cpp (+1) 
- (modified) libc/src/math/generic/fdiml.cpp (+1) 
- (modified) libc/test/src/math/smoke/FDimTest.h (+30) 


``````````diff
diff --git a/libc/src/__support/FPUtil/BasicOperations.h b/libc/src/__support/FPUtil/BasicOperations.h
index 5b81b921296a04..e63f464ee59d28 100644
--- a/libc/src/__support/FPUtil/BasicOperations.h
+++ b/libc/src/__support/FPUtil/BasicOperations.h
@@ -260,6 +260,10 @@ LIBC_INLINE constexpr T fminimum_mag_num(T x, T y) {
 
 template <typename T, cpp::enable_if_t<cpp::is_floating_point_v<T>, int> = 0>
 LIBC_INLINE T constexpr fdim(T x, T y) {
+  // Do not speculate x - y when x <= y: it can raise spurious FP exceptions.
+  // See https://github.com/llvm/llvm-project/issues/88771.
+  // The entry points also enable FENV_ACCESS to allow inlining this function.
+  LIBC_FENV_ACCESS_ON
   FPBits<T> bitx(x), bity(y);
 
   if (bitx.is_nan()) {
diff --git a/libc/src/math/generic/fdim.cpp b/libc/src/math/generic/fdim.cpp
index 65aca3374e9c45..9977e181ccf1bd 100644
--- a/libc/src/math/generic/fdim.cpp
+++ b/libc/src/math/generic/fdim.cpp
@@ -12,6 +12,7 @@
 namespace LIBC_NAMESPACE_DECL {
 
 LLVM_LIBC_FUNCTION(double, fdim, (double x, double y)) {
+  LIBC_FENV_ACCESS_ON
   return math::fdim(x, y);
 }
 
diff --git a/libc/src/math/generic/fdimbf16.cpp b/libc/src/math/generic/fdimbf16.cpp
index e5a6504e98ed76..c3be2d7b8fa227 100644
--- a/libc/src/math/generic/fdimbf16.cpp
+++ b/libc/src/math/generic/fdimbf16.cpp
@@ -12,6 +12,7 @@
 namespace LIBC_NAMESPACE_DECL {
 
 LLVM_LIBC_FUNCTION(bfloat16, fdimbf16, (bfloat16 x, bfloat16 y)) {
+  LIBC_FENV_ACCESS_ON
   return math::fdimbf16(x, y);
 }
 
diff --git a/libc/src/math/generic/fdimf.cpp b/libc/src/math/generic/fdimf.cpp
index fda284b35e1632..cdab9e95506c81 100644
--- a/libc/src/math/generic/fdimf.cpp
+++ b/libc/src/math/generic/fdimf.cpp
@@ -12,6 +12,7 @@
 namespace LIBC_NAMESPACE_DECL {
 
 LLVM_LIBC_FUNCTION(float, fdimf, (float x, float y)) {
+  LIBC_FENV_ACCESS_ON
   return math::fdimf(x, y);
 }
 
diff --git a/libc/src/math/generic/fdimf128.cpp b/libc/src/math/generic/fdimf128.cpp
index 385d683eba01b7..b6fbe6a48516a3 100644
--- a/libc/src/math/generic/fdimf128.cpp
+++ b/libc/src/math/generic/fdimf128.cpp
@@ -15,6 +15,7 @@ namespace LIBC_NAMESPACE_DECL {
 using LIBC_NAMESPACE::fputil::Float128;
 
 LLVM_LIBC_FUNCTION(float128, fdimf128, (float128 x, float128 y)) {
+  LIBC_FENV_ACCESS_ON
   return cpp::bit_cast<float128>(
       math::fdimf128(cpp::bit_cast<Float128>(x), cpp::bit_cast<Float128>(y)));
 }
diff --git a/libc/src/math/generic/fdimf16.cpp b/libc/src/math/generic/fdimf16.cpp
index ce675d66a55cd8..a3663524925b52 100644
--- a/libc/src/math/generic/fdimf16.cpp
+++ b/libc/src/math/generic/fdimf16.cpp
@@ -12,6 +12,7 @@
 namespace LIBC_NAMESPACE_DECL {
 
 LLVM_LIBC_FUNCTION(float16, fdimf16, (float16 x, float16 y)) {
+  LIBC_FENV_ACCESS_ON
   return math::fdimf16(x, y);
 }
 
diff --git a/libc/src/math/generic/fdiml.cpp b/libc/src/math/generic/fdiml.cpp
index 9ad2fa78bc5930..4631e4c4b336de 100644
--- a/libc/src/math/generic/fdiml.cpp
+++ b/libc/src/math/generic/fdiml.cpp
@@ -12,6 +12,7 @@
 namespace LIBC_NAMESPACE_DECL {
 
 LLVM_LIBC_FUNCTION(long double, fdiml, (long double x, long double y)) {
+  LIBC_FENV_ACCESS_ON
   return math::fdiml(x, y);
 }
 
diff --git a/libc/test/src/math/smoke/FDimTest.h b/libc/test/src/math/smoke/FDimTest.h
index 28e73201189f23..4fe7f8552180b3 100644
--- a/libc/test/src/math/smoke/FDimTest.h
+++ b/libc/test/src/math/smoke/FDimTest.h
@@ -60,6 +60,33 @@ class FDimTestTemplate : public LIBC_NAMESPACE::testing::FEnvSafeTest {
     EXPECT_FP_EQ(zero, func(neg_zero, neg_zero));
   }
 
+  void test_no_spurious_exceptions(FuncPtr func) {
+    const T max = FPBits::max_normal().get_val();
+    const T neg_max = FPBits::max_normal(Sign::NEG).get_val();
+    const T neg_denorm = FPBits::min_subnormal(Sign::NEG).get_val();
+    auto call = [func](T x, T y) {
+      T result = func(x, y);
+      // Comparing subnormal operands can raise the non-standard denormal
+      // exception. Only check the standard floating-point exceptions here.
+#ifdef FE_DENORM
+      LIBC_NAMESPACE::fputil::clear_except(FE_DENORM);
+#elif defined(FE_DENORMAL)
+      LIBC_NAMESPACE::fputil::clear_except(FE_DENORMAL);
+#elif defined(__FE_DENORM)
+      LIBC_NAMESPACE::fputil::clear_except(__FE_DENORM);
+#endif
+      return result;
+    };
+
+    // When x <= y, the subtraction must not raise exceptions before returning
+    // zero, even when it would produce an invalid, inexact, or overflow result.
+    EXPECT_FP_EQ_WITH_EXCEPTION_ALL_ROUNDING(zero, call(inf, inf), 0);
+    EXPECT_FP_EQ_WITH_EXCEPTION_ALL_ROUNDING(zero, call(neg_inf, neg_inf), 0);
+    EXPECT_FP_EQ_WITH_EXCEPTION_ALL_ROUNDING(zero, call(neg_max, neg_denorm),
+                                             0);
+    EXPECT_FP_EQ_WITH_EXCEPTION_ALL_ROUNDING(zero, call(neg_max, max), 0);
+  }
+
   void test_in_range(FuncPtr func) {
     constexpr StorageType STORAGE_MAX =
         LIBC_NAMESPACE::cpp::numeric_limits<StorageType>::max();
@@ -92,5 +119,8 @@ class FDimTestTemplate : public LIBC_NAMESPACE::testing::FEnvSafeTest {
   TEST_F(LlvmLibc##Name##Test, InfArg) { test_inf_arg(&func); }                \
   TEST_F(LlvmLibc##Name##Test, NegInfArg) { test_neg_inf_arg(&func); }         \
   TEST_F(LlvmLibc##Name##Test, BothZero) { test_both_zero(&func); }            \
+  TEST_F(LlvmLibc##Name##Test, NoSpuriousExceptions) {                         \
+    test_no_spurious_exceptions(&func);                                        \
+  }                                                                            \
   TEST_F(LlvmLibc##Name##Test, InFloatRange) { test_in_range(&func); }         \
   static_assert(true, "Require semicolon.")

``````````

</details>


https://github.com/llvm/llvm-project/pull/228749


More information about the libc-commits mailing list