Skip to content

[APFloat] Add exp function for APFloat::IEEESsingle using expf implementation from LLVM libc. #143959

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 7 commits into
base: main
Choose a base branch
from

Conversation

lntue
Copy link
Contributor

@lntue lntue commented Jun 12, 2025

Discourse RFC: https://discourse.llvm.org/t/rfc-make-clang-builtin-math-functions-constexpr-with-llvm-libc-to-support-c-23-constexpr-math-functions/86450

  • The implementation in LLVM libc is header-only.
  • expf implementation in LLVM libc is correctly rounded for all rounding modes.
  • LLVM libc implementation will round to the floating point environment's rounding mode.
  • No cmake build dependency between LLVM and LLVM libc, only requires LLVM libc source presents in llvm-project/libc folder.
  • This is guarded with the cmake flag LLVM_INTEGRATE_LIBC.

@llvmbot
Copy link
Member

llvmbot commented Jun 12, 2025

@llvm/pr-subscribers-libc
@llvm/pr-subscribers-backend-amdgpu
@llvm/pr-subscribers-llvm-support

@llvm/pr-subscribers-llvm-adt

Author: None (lntue)

Changes

Discourse RFC: https://discourse.llvm.org/t/rfc-make-clang-builtin-math-functions-constexpr-with-llvm-libc-to-support-c-23-constexpr-math-functions/86450

  • The implementation in LLVM libc is header-only.
  • expf implementation in LLVM libc is correctly rounded for all rounding modes.
  • LLVM libc implementation will round to the floating point environment's rounding mode.
  • No cmake build dependency between LLVM and LLVM libc, only requires LLVM libc source presents in llvm-project/libc folder.
  • This is guarded with the cmake flag LLVM_INTEGRATE_LIBC.

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

7 Files Affected:

  • (modified) llvm/CMakeLists.txt (+11)
  • (modified) llvm/include/llvm/ADT/APFloat.h (+5)
  • (modified) llvm/include/llvm/Config/llvm-config.h.cmake (+3)
  • (modified) llvm/lib/Support/APFloat.cpp (+34)
  • (modified) llvm/lib/Support/CMakeLists.txt (+6)
  • (modified) llvm/lib/Target/AMDGPU/AMDGPULibCalls.cpp (+1-1)
  • (modified) llvm/unittests/ADT/APFloatTest.cpp (+19)
diff --git a/llvm/CMakeLists.txt b/llvm/CMakeLists.txt
index cfb67472aa71e..ff2f4ff4ea298 100644
--- a/llvm/CMakeLists.txt
+++ b/llvm/CMakeLists.txt
@@ -644,6 +644,17 @@ endif()
 
 set(LLVM_ENABLE_Z3_SOLVER_DEFAULT "${Z3_FOUND}")
 
+set(LLVM_INTEGRATE_LIBC "OFF" CACHE STRING "Use LLVM libc codes directly if available.")
+
+if(LLVM_INTEGRATE_LIBC)
+  message(STATUS "LLVM_INTEGRATE_LIBC is ${LLVM_INTEGRATE_LIBC}")
+  include(FindLibcCommonUtils)
+  if(NOT TARGET llvm-libc-common-utilities)
+    message(STATUS "LLVM_INTEGRATE_LIBC is set but cannot find LLVM libc at ${libc_path}.")
+    set(LLVM_INTEGRATE_LIBC OFF)
+  endif()
+endif()
+
 
 if( LLVM_TARGETS_TO_BUILD STREQUAL "all" )
   set( LLVM_TARGETS_TO_BUILD ${LLVM_ALL_TARGETS} )
diff --git a/llvm/include/llvm/ADT/APFloat.h b/llvm/include/llvm/ADT/APFloat.h
index 13df838da3dad..6f6dd3c014584 100644
--- a/llvm/include/llvm/ADT/APFloat.h
+++ b/llvm/include/llvm/ADT/APFloat.h
@@ -1522,6 +1522,7 @@ class APFloat : public APFloatBase {
   friend int ilogb(const APFloat &Arg) { return ilogb(Arg.getIEEE()); }
   friend APFloat scalbn(APFloat X, int Exp, roundingMode RM);
   friend APFloat frexp(const APFloat &X, int &Exp, roundingMode RM);
+  friend APFloat exp(const APFloat &X, roundingMode RM);
   friend IEEEFloat;
   friend DoubleAPFloat;
 };
@@ -1657,6 +1658,10 @@ inline APFloat maximumnum(const APFloat &A, const APFloat &B) {
   return A < B ? B : A;
 }
 
+/// Implement IEEE 754-2019 exp functions.
+LLVM_READONLY
+APFloat exp(const APFloat &X, RoundingMode RM);
+
 inline raw_ostream &operator<<(raw_ostream &OS, const APFloat &V) {
   V.print(OS);
   return OS;
diff --git a/llvm/include/llvm/Config/llvm-config.h.cmake b/llvm/include/llvm/Config/llvm-config.h.cmake
index dbc882937b4f4..8ec9a30dc85d1 100644
--- a/llvm/include/llvm/Config/llvm-config.h.cmake
+++ b/llvm/include/llvm/Config/llvm-config.h.cmake
@@ -133,4 +133,7 @@
    and to 0 otherwise. */
 #cmakedefine01 LLVM_ENABLE_DEBUGLOC_COVERAGE_TRACKING
 
+/* Define if LLVM and clang uses LLVM libc for math computations. */
+#cmakedefine LLVM_INTEGRATE_LIBC
+
 #endif
diff --git a/llvm/lib/Support/APFloat.cpp b/llvm/lib/Support/APFloat.cpp
index 5e0b29ffb2590..150853743540d 100644
--- a/llvm/lib/Support/APFloat.cpp
+++ b/llvm/lib/Support/APFloat.cpp
@@ -28,6 +28,11 @@
 #include <cstring>
 #include <limits.h>
 
+#ifdef LLVM_INTEGRATE_LIBC
+// Shared headers from LLVM libc
+#include "shared/math.h"
+#endif // LLVM_INTEGRATE_LIBC
+
 #define APFLOAT_DISPATCH_ON_SEMANTICS(METHOD_CALL)                             \
   do {                                                                         \
     if (usesLayout<IEEEFloat>(getSemantics()))                                 \
@@ -5601,6 +5606,35 @@ float APFloat::convertToFloat() const {
   return Temp.getIEEE().convertToFloat();
 }
 
+#ifdef LLVM_INTEGRATE_LIBC
+APFloat exp(const APFloat &X, RoundingMode rounding_mode) {
+  assert((&X.getSemantics() == (const llvm::fltSemantics *)&semIEEEsingle) &&
+         "Float semantics is not IEEEsingle");
+  if (&X.getSemantics() == (const llvm::fltSemantics *)&semIEEEsingle) {
+    int current_rounding_mode = fegetround();
+    switch (rounding_mode) {
+    case APFloat::rmNearestTiesToEven:
+      fesetround(FE_TONEAREST);
+      break;
+    case APFloat::rmTowardPositive:
+      fesetround(FE_UPWARD);
+      break;
+    case APFloat::rmTowardNegative:
+      fesetround(FE_DOWNWARD);
+      break;
+    case APFloat::rmTowardZero:
+      fesetround(FE_TOWARDZERO);
+      break;
+    default:
+    }
+    float result = LIBC_NAMESPACE::shared::expf(X.convertToFloat());
+    fesetround(current_rounding_mode);
+    return APFloat(result);
+  }
+  llvm_unreachable("Unexpected semantics");
+}
+#endif // LLVM_INTEGRATE_LIBC
+
 } // namespace llvm
 
 #undef APFLOAT_DISPATCH_ON_SEMANTICS
diff --git a/llvm/lib/Support/CMakeLists.txt b/llvm/lib/Support/CMakeLists.txt
index 45d961e994a1a..aeeba93819227 100644
--- a/llvm/lib/Support/CMakeLists.txt
+++ b/llvm/lib/Support/CMakeLists.txt
@@ -379,3 +379,9 @@ if(LLVM_WITH_Z3)
     ${Z3_INCLUDE_DIR}
     )
 endif()
+
+if(LLVM_INTEGRATE_LIBC)
+  set_property(TARGET LLVMSupport PROPERTY CXX_STANDARD 17)
+  target_include_directories(LLVMSupport PRIVATE "${LLVM_INCLUDE_DIR}/../../libc")
+  target_compile_options(LLVMSupport PRIVATE "-Wno-c99-extensions") # _Complex warnings.
+endif()
diff --git a/llvm/lib/Target/AMDGPU/AMDGPULibCalls.cpp b/llvm/lib/Target/AMDGPU/AMDGPULibCalls.cpp
index 8767208d20ec9..1e2aa2839fc8f 100644
--- a/llvm/lib/Target/AMDGPU/AMDGPULibCalls.cpp
+++ b/llvm/lib/Target/AMDGPU/AMDGPULibCalls.cpp
@@ -1540,7 +1540,7 @@ bool AMDGPULibCalls::evaluateScalarMathFunc(const FuncInfo &FInfo, double &Res0,
     return true;
 
   case AMDGPULibFunc::EI_EXP:
-    Res0 = exp(opr0);
+    Res0 = std::exp(opr0);
     return true;
 
   case AMDGPULibFunc::EI_EXP2:
diff --git a/llvm/unittests/ADT/APFloatTest.cpp b/llvm/unittests/ADT/APFloatTest.cpp
index 7a5fd83cd9581..c56cd8898d17a 100644
--- a/llvm/unittests/ADT/APFloatTest.cpp
+++ b/llvm/unittests/ADT/APFloatTest.cpp
@@ -8356,4 +8356,23 @@ TEST(APFloatTest, hasSignBitInMSB) {
   EXPECT_FALSE(APFloat::hasSignBitInMSB(APFloat::Float8E8M0FNU()));
 }
 
+#ifdef LLVM_INTEGRATE_LIBC
+TEST(APFloatTest, expf) {
+  EXPECT_EQ(
+      1.0f,
+      llvm::exp(APFloat(0.0f), APFloat::rmNearestTiesToEven).convertToFloat());
+  EXPECT_EQ(
+      0x1.5bf0a8p1f,
+      llvm::exp(APFloat(1.0f), APFloat::rmNearestTiesToEven).convertToFloat());
+  EXPECT_EQ(
+      0x1.5bf0aap1f,
+      llvm::exp(APFloat(1.0f), APFloat::rmTowardPositive).convertToFloat());
+  EXPECT_EQ(
+      0x1.5bf0a8p1f,
+      llvm::exp(APFloat(1.0f), APFloat::rmTowardNegative).convertToFloat());
+  EXPECT_EQ(0x1.5bf0a8p1f,
+            llvm::exp(APFloat(1.0f), APFloat::rmTowardZero).convertToFloat());
+}
+#endif // LLVM_INTEGRATE_LIBC
+
 } // namespace

@lntue lntue added the libc label Jun 12, 2025
Copy link
Contributor

@jhuber6 jhuber6 left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Given sparse checkouts, I'm wondering if we should check if lib/ exists in the tree and use this by default if so.

@arsenm arsenm added the floating-point Floating-point math label Jun 13, 2025
int current_rounding_mode = fegetround();
switch (rounding_mode) {
case APFloat::rmNearestTiesToEven:
fesetround(FE_TONEAREST);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We're not compiling LLVM with strictfp (which also barely works, on one target). Can we instead just call a fixed rounding mode implementation in libc?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The main problem the preventing linking LLVM libc directly to LLVM / APFloat is that ideally, the libc project belongs to the LLVM_ENABLE_RUNTIMES list which will be built after LLVM and clang in the LLVM_ENABLE_PROJECTS list. To break that build dependency cycle, and simplify build systems for both projects, I think letting LLVM directly include and build libc's math functions is the simplest way.

On the other hand, I do see 2 problems that might arise:

  • we might need to build these libc math routines wrapped inside #pragma STDC FENV_ACCESS ON to make sure that the floating point environment accesses are not shuffled around. At the moment it looks like they are built correctly, but it would be safer to put them under the pragma.
  • as more and more math routines are included directly, it might be slow to build APFloat.cpp, so we might need to refactor them into separate TUs to speed up the build.

What do you think about refactor this into something like APFloatMath.cpp or APFloatExpLog.cpp, put them under #pragma STDC FENV_ACCESS ON, and then add them to LLVMSupport?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think the suggestion is to do LIBC_NAMESPACE::shared::expf(X.convertToFloat(), rounding_mode) and not mess with global state. It would be much cleaner if that were possible

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Unfortunately, at the moment, LLVM libc math functions are rounded according to the rounding mode of the floating point environment. So in order to have a specific rounding mode, they need to be wrapped with fesetround() calls, and LLVM libc won't have access to APFloat::rm*. So to hide the fegetround/fesetround calls, we could add either LIBC_NAMESPACE::shared::expf(float, int) to take the fenv.h rounding mode on the libc side, but the switch / translation from APFloat::rm* to FE_* still needs to be in APFloat.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

So to hide the fegetround/fesetround calls, we could add either LIBC_NAMESPACE::shared::expf(float, int) to take the fenv.h rounding mode on the libc side, but the switch / translation from APFloat::rm* to FE_* still needs to be in APFloat.

This would be fine - we are mostly concerned about the global state. having to map an enum to another is perfectly reasonable

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I've moved the rounding mode adjustment into libc.

Comment on lines 8362 to 8363
1.0f,
llvm::exp(APFloat(0.0f), APFloat::rmNearestTiesToEven).convertToFloat());
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Compare bit-equal? Can you also test the edge case values, nan, inf, denormals, -0

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I added more tests for those values. And for floating point values that are not +-0 or NaN, the equal (==) comparison is bit-equal.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not true when denormals are flushed, and x87 unnormals

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, or those odd cases. But in here we have float and normal values, and I hope people don't alias float with x87 80-bit fp type

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This doesn't address that the denormal values will compare equal to 0 depending on the host FP environment

Copy link
Contributor

@jcranmer-intel jcranmer-intel left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm also concerned about having a function declaration but not definition if LLVM_INTEGRATE_LIBC is off.

@@ -1522,6 +1522,7 @@ class APFloat : public APFloatBase {
friend int ilogb(const APFloat &Arg) { return ilogb(Arg.getIEEE()); }
friend APFloat scalbn(APFloat X, int Exp, roundingMode RM);
friend APFloat frexp(const APFloat &X, int &Exp, roundingMode RM);
friend APFloat exp(const APFloat &X, roundingMode RM);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If I'm following correctly, we have two separate declarations of this function...

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed. And added test for using the default rounding mode.

}

APFloat exp(const APFloat &X,
RoundingMode rounding_mode = APFloat::rmNearestTiesToEven) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

... and the default parameter is only in the definition body that no user code sees.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed. And added test for using the default rounding modes.

@lntue
Copy link
Contributor Author

lntue commented Jun 13, 2025

I'm also concerned about having a function declaration but not definition if LLVM_INTEGRATE_LIBC is off.

I wrapped the declarations inside #ifdef LLVM_INTEGRATE_LIBC.

@@ -109,6 +109,18 @@ static constexpr float expf(float x) {
return static_cast<float>(exp_hi * exp_mid * exp_lo);
}

// Directional rounding version of expf.
LIBC_INLINE static float expf(float x, int rounding_mode) {
int current_rounding_mode = fputil::get_round();
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Use the pragma to enable fenv_access?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging this pull request may close these issues.

8 participants