[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