Skip to content

Conversation

vogelsgesang
Copy link
Member

@vogelsgesang vogelsgesang commented Oct 10, 2025

This commit optimizes the performance for std::exception_ptr for an empty exception objects.

To do so, we use 3 high-level approaches:

  1. Moving the implementation from the library into the headers, thereby allowing the compiler to inline the function bodies.
  2. Adding fast paths to the (now inlineable) functions, checking for the empty case.
  3. Adding a move constructor, assignment and swap

Those optimizations were implemented for the libc++abi, libsupc++ and libstdc++ ABIs.

Fixes #44892

Performance

With this change, the compiler can now completely constant-fold https://godbolt.org/z/dvedE375r. Also in cases where the compiler cannot statically prove that exception_ptr is empty, it can at least generate a fast-path check if the exception_ptr is empty, without calling into the library.

ABI compatibility

We use a new visibility macro _LIBCPP_EXPORTED_FROM_ABI_INLINEABLE to ensure that the functions which are now declared in the header are still exported by the library. See the description in VisibilityMacros.rst for details. This approach was originally proposed by Nikolas Klauser in https://reviews.llvm.org/D122536

Moving implementations to the header

To move the implementation to the header, we must know the selected LIBCXX_CXX_ABI also in the headers. For that purpose, the LIBCXX_CXX_ABI configuration is now exposed via __config_site.

While the Microsoft ABI and the "unimplemented" APIs do not benefit from this optimizations, I also moved their implementation to the headers for more uniformity. Mid-term, we probably have to expose all implementations in the header, anyway, because P3068R6 mandates all methods of exception_ptr to be constexpr.

Unifying libc++abi, libsupc++ and none

Both libc++abi and libsupc++ are reference-counted. The primary difference is the function called for ref-counting:

  • libc++api uses __cxa_{in,de}crement_exception_refcount
  • libsupc++ uses __exception_ptr::_M_{addref,release}

This commit factors out the common reference-counting logic into a shared header.

Our libsupc++ implementation so far did not take advantage of _M_addref/_M_release. For the performance benefits of this PR it was necessary to start using them.

The same reference-counted approach is also reused for the none/unimplemented implementation. This has the side effect, that users can now use empty exception_ptrs even in the none ABI. The abort is only triggered for non-empty exception_ptrs. Given this simplifies the code, I think change is acceptable.

Unifying nested_exception

The implementation for nested_exception was effectively identical across all ABIs. For most ABIs, the source code was literally identical. There were only two deviations:

  • For libsupc++ or libstdc++, we did not use define the ~nested_exception destructor.
  • The abort in nested_exception::rethrow_nested was unnecessary as the same abort is also present in the rethrow_exception implementation. As such, we were able to simply remove that special casing.

The implementation is now unified directly in src/exception.cpp

Standard conformance

The available constructors, operators and methods of exception_ptr are not specified by the standard. As such, adding the move constructor and the assignment operator are standard conformant. libstdc++ made the same decision and also provides move constructor, assignment and swap.

Copy link

github-actions bot commented Oct 10, 2025

✅ With the latest revision this PR passed the C/C++ code formatter.

@vogelsgesang vogelsgesang force-pushed the avogelsgesang-std-exception-optimization branch 9 times, most recently from 0c0fde7 to 3815568 Compare October 10, 2025 16:27
@vogelsgesang vogelsgesang marked this pull request as ready for review October 10, 2025 18:26
@vogelsgesang vogelsgesang requested a review from a team as a code owner October 10, 2025 18:27
@llvmbot llvmbot added the libc++ libc++ C++ Standard Library. Not GNU libstdc++. Not libc++abi. label Oct 10, 2025
@llvmbot
Copy link
Member

llvmbot commented Oct 10, 2025

@llvm/pr-subscribers-libcxx

Author: Adrian Vogelsgesang (vogelsgesang)

Changes

This commit optimizes the performance for std::exception_ptr for an empty exception objects.

To do so, we use 3 high-level approaches:

  1. Moving the implementation from the libc++ library into the libc++ headers, thereby allowing the compiler to inline the function bodies.
  2. Adding fast paths to the (now inlineable) bodies, checking for the empty case.
  3. Adding move constuctor, assignment and swap

Those optimizations were implemented for the libc++abi, libsupc++ and libstdc++ ABIs.

Fixes #44892

Performance

With this change, the compiler can now completely constant-fold https://godbolt.org/z/NaNKe5. Also in cases where the compiler cannot statically prove that exception_ptr is empty, it can at least generate a fast-path check if the exception_ptr is empty, without calling into the library.

ABI compatibility

We use a new visibility macro _LIBCPP_EXPORTED_FROM_ABI_INLINEABLE to ensure that the functions which are now declared in the header are still exported by the library. See the description in VisibilityMacros.rst for details. This approach was originally proposed by Nikolas Klauser in https://reviews.llvm.org/D122536

Moving implementations to the header

To move the implementation to the header, we must know the selected LIBCXX_CXX_ABI also in the headers. For that purpose, the LIBCXX_CXX_ABI configuration is now exposed via __config_site.

While the Microsoft ABI and the "unimplemented" APIs do not benefit from this optimizations, I also moved their implementation to the headers for more uniformity. Mid-term, we probably have to expose all implementations in the header, anyway, because P3068R6 mandates all methods of exception_ptr to be constexpr.

Unifying libc++abi, libsupc++ and none

Both libc++abi and libsupc++ are reference-counted. The primary difference is the function called for ref-counting:

  • libc++api uses __cxa_{in,de}crement_exception_refcount
  • libsupc++ uses __exception_ptr::_M_{addref,release}

This commit factors out the common reference-counting logic into a shared header.

Our libsupc++ implementation so far did not take advantage of _M_addref/_M_release. For the performance benefits of this PR it was necessary to start using them.

The same reference-counted approach is also reused for the none/unimplemented implementation. This has the side effect, that users can now use empty exception_ptrs even in the none ABI. The abort is only triggered for non-empty exception_ptrs. Given this simplifies the code, I think change is acceptable.

Unifying nested_exception

The implementation for nested_exception was effectively identical across all ABIs. For most ABIs, the source code was literally identical. There were only two deviations:

  • For libsupc++ or libstdc++, we did not use define the ~nested_exception destructor.
  • The abort in nested_exception::rethrow_nested was unnecessary as the same abort is also present in the rethrow_exception implementation. As such, we were able to simply remove that special casing.

The implementation is now unified directly in the __nested_exception.h header.

Standard conformance

The available constructors, operators and methods of exception_ptr is not specified by the standard. As such, adding the move constructor and the assignment operator are standard conformant. libstdc++ made the same decision and also provides those headers.


Patch is 41.22 KiB, truncated to 20.00 KiB below, full version: https://github.com/llvm/llvm-project/pull/162773.diff

24 Files Affected:

  • (modified) libcxx/CMakeLists.txt (+14)
  • (modified) libcxx/cmake/Modules/HandleLibCXXABI.cmake (-4)
  • (modified) libcxx/docs/DesignDocs/VisibilityMacros.rst (+19)
  • (modified) libcxx/include/CMakeLists.txt (+4)
  • (modified) libcxx/include/__config_site.in (+8)
  • (modified) libcxx/include/__exception/exception_ptr.h (+111-24)
  • (added) libcxx/include/__exception/exception_ptr_cxxabi.ipp (+50)
  • (added) libcxx/include/__exception/exception_ptr_glibcxx.ipp (+47)
  • (renamed) libcxx/include/__exception/exception_ptr_msvc.ipp (+14-25)
  • (added) libcxx/include/__exception/exception_ptr_unimplemented.ipp (+36)
  • (modified) libcxx/include/__exception/operations.h (-5)
  • (modified) libcxx/src/CMakeLists.txt (-4)
  • (modified) libcxx/src/exception.cpp (+26-18)
  • (modified) libcxx/src/new_handler.cpp (+5-7)
  • (modified) libcxx/src/support/runtime/exception_libcxxabi.ipp (+3-5)
  • (modified) libcxx/src/support/runtime/exception_libcxxrt.ipp (-5)
  • (removed) libcxx/src/support/runtime/exception_pointer_cxxabi.ipp (-64)
  • (removed) libcxx/src/support/runtime/exception_pointer_glibcxx.ipp (-68)
  • (removed) libcxx/src/support/runtime/exception_pointer_unimplemented.ipp (-62)
  • (modified) libcxx/src/support/runtime/stdexcept_default.ipp (+1-1)
  • (modified) libcxx/src/typeinfo.cpp (+2-4)
  • (modified) libcxx/test/benchmarks/exception_ptr.bench.cpp (+16)
  • (modified) libcxx/test/tools/clang_tidy_checks/hide_from_abi.cpp (+2-1)
  • (modified) libcxx/utils/libcxx/header_information.py (+4)
diff --git a/libcxx/CMakeLists.txt b/libcxx/CMakeLists.txt
index a119850cd808e..4ff23f0038d0a 100644
--- a/libcxx/CMakeLists.txt
+++ b/libcxx/CMakeLists.txt
@@ -750,6 +750,20 @@ config_define(${LIBCXX_ENABLE_WIDE_CHARACTERS} _LIBCPP_HAS_WIDE_CHARACTERS)
 config_define(${LIBCXX_ENABLE_TIME_ZONE_DATABASE} _LIBCPP_HAS_TIME_ZONE_DATABASE)
 config_define(${LIBCXX_ENABLE_VENDOR_AVAILABILITY_ANNOTATIONS} _LIBCPP_HAS_VENDOR_AVAILABILITY_ANNOTATIONS)
 
+if (LIBCXX_CXX_ABI STREQUAL "none")
+  config_define(1 _LIBCPP_CXX_ABI_NONE)
+elseif (LIBCXX_CXX_ABI STREQUAL "libcxxabi" OR LIBCXX_CXX_ABI STREQUAL "system-libcxxabi")
+  config_define(1 _LIBCPP_CXX_ABI_LIBCXXABI)
+elseif (LIBCXX_CXX_ABI STREQUAL "libcxxrt")
+  config_define(1 _LIBCPP_CXX_ABI_LIBCXXRT)
+elseif (LIBCXX_CXX_ABI STREQUAL "libstdc++")
+  config_define(1 _LIBCPP_CXX_ABI_LIBSTDCXX)
+elseif (LIBCXX_CXX_ABI STREQUAL "libsupc++")
+  config_define(1 _LIBCPP_CXX_ABI_LIBSUPCXX)
+elseif (LIBCXX_CXX_ABI STREQUAL "vcruntime")
+  config_define(1 _LIBCPP_CXX_ABI_VCRUNTIME)
+endif()
+
 # TODO: Remove in LLVM 21. We're leaving an error to make this fail explicitly.
 if (LIBCXX_ENABLE_ASSERTIONS)
   message(FATAL_ERROR "LIBCXX_ENABLE_ASSERTIONS has been removed. Please use LIBCXX_HARDENING_MODE instead.")
diff --git a/libcxx/cmake/Modules/HandleLibCXXABI.cmake b/libcxx/cmake/Modules/HandleLibCXXABI.cmake
index 52236f473f35d..35287cf380da6 100644
--- a/libcxx/cmake/Modules/HandleLibCXXABI.cmake
+++ b/libcxx/cmake/Modules/HandleLibCXXABI.cmake
@@ -119,7 +119,6 @@ elseif ("${LIBCXX_CXX_ABI}" STREQUAL "libsupc++")
 elseif ("${LIBCXX_CXX_ABI}" STREQUAL "libcxxabi")
   add_library(libcxx-abi-headers INTERFACE)
   target_link_libraries(libcxx-abi-headers INTERFACE cxxabi-headers)
-  target_compile_definitions(libcxx-abi-headers INTERFACE "-DLIBCXX_BUILDING_LIBCXXABI")
 
   if (TARGET cxxabi_shared)
     add_library(libcxx-abi-shared INTERFACE)
@@ -156,7 +155,6 @@ elseif ("${LIBCXX_CXX_ABI}" STREQUAL "system-libcxxabi")
 
   add_library(libcxx-abi-headers INTERFACE)
   import_private_headers(libcxx-abi-headers "${LIBCXX_CXX_ABI_INCLUDE_PATHS}" "cxxabi.h;__cxxabi_config.h")
-  target_compile_definitions(libcxx-abi-headers INTERFACE "-DLIBCXX_BUILDING_LIBCXXABI")
 
   import_shared_library(libcxx-abi-shared c++abi)
   target_link_libraries(libcxx-abi-shared INTERFACE libcxx-abi-headers)
@@ -173,7 +171,6 @@ elseif ("${LIBCXX_CXX_ABI}" STREQUAL "libcxxrt")
   add_library(libcxx-abi-headers INTERFACE)
   import_private_headers(libcxx-abi-headers "${LIBCXX_CXX_ABI_INCLUDE_PATHS}"
     "cxxabi.h;unwind.h;unwind-arm.h;unwind-itanium.h")
-  target_compile_definitions(libcxx-abi-headers INTERFACE "-DLIBCXXRT")
 
   import_shared_library(libcxx-abi-shared cxxrt)
   target_link_libraries(libcxx-abi-shared INTERFACE libcxx-abi-headers)
@@ -191,7 +188,6 @@ elseif ("${LIBCXX_CXX_ABI}" STREQUAL "vcruntime")
 # Don't link against any ABI library
 elseif ("${LIBCXX_CXX_ABI}" STREQUAL "none")
   add_library(libcxx-abi-headers INTERFACE)
-  target_compile_definitions(libcxx-abi-headers INTERFACE "-D_LIBCPP_BUILDING_HAS_NO_ABI_LIBRARY")
 
   add_library(libcxx-abi-shared INTERFACE)
   target_link_libraries(libcxx-abi-shared INTERFACE libcxx-abi-headers)
diff --git a/libcxx/docs/DesignDocs/VisibilityMacros.rst b/libcxx/docs/DesignDocs/VisibilityMacros.rst
index db54b35386b19..949401012ae36 100644
--- a/libcxx/docs/DesignDocs/VisibilityMacros.rst
+++ b/libcxx/docs/DesignDocs/VisibilityMacros.rst
@@ -35,6 +35,25 @@ Visibility Macros
   used on class templates. On classes it should only be used if the vtable
   lives in the built library.
 
+**_LIBCPP_EXPORTED_FROM_LIB_INLINEABLE**
+  Mark a symbol as exported from the libc++ library, while still providing an
+  inlineable definition that can be used by the compiler for optimization
+  purposes.
+
+  To use this macro on a class method, define the method body
+  *outside* of the class definition and annotate that definition with
+  `_LIBCPP_EXPORTED_FROM_LIB_INLINEABLE`. Make sure to include the
+  header in at least one translation unit linked into the libc++ library.
+
+  This macro works by applying `[[gnu::gnu_inline]] inline` to the funciton
+  in the header, thereby suppressing code generation while still allowing the
+  compiler to use the function for optimization purposes.
+  During the build of libc++, we trigger code generation by expanding the
+  macro to `/*empty*/`. Since the function is no longer marked as `inline`,
+  it will be emitted even if not called. (For this reason its paramount to
+  not define methods in the class definition, since those definitions would
+  be implicitly `inline`.)
+
 **_LIBCPP_OVERRIDABLE_FUNC_VIS**
   Mark a symbol as being exported by the libc++ library, but allow it to be
   overridden locally. On non-Windows, this is equivalent to `_LIBCPP_FUNC_VIS`.
diff --git a/libcxx/include/CMakeLists.txt b/libcxx/include/CMakeLists.txt
index ddace8bf8c728..2baf6fca70a5f 100644
--- a/libcxx/include/CMakeLists.txt
+++ b/libcxx/include/CMakeLists.txt
@@ -343,6 +343,10 @@ set(files
   __debug_utils/strict_weak_ordering_check.h
   __exception/exception.h
   __exception/exception_ptr.h
+  __exception/exception_ptr_cxxabi.ipp
+  __exception/exception_ptr_glibcxx.ipp
+  __exception/exception_ptr_msvc.ipp
+  __exception/exception_ptr_unimplemented.ipp
   __exception/nested_exception.h
   __exception/operations.h
   __exception/terminate.h
diff --git a/libcxx/include/__config_site.in b/libcxx/include/__config_site.in
index b68c0c8258366..58714ed231b88 100644
--- a/libcxx/include/__config_site.in
+++ b/libcxx/include/__config_site.in
@@ -33,6 +33,14 @@
 #cmakedefine01 _LIBCPP_HAS_TIME_ZONE_DATABASE
 #cmakedefine01 _LIBCPP_INSTRUMENTED_WITH_ASAN
 
+// LIBCXX_CXX_ABI backends
+#cmakedefine _LIBCPP_CXX_ABI_NONE
+#cmakedefine _LIBCPP_CXX_ABI_LIBCXXABI
+#cmakedefine _LIBCPP_CXX_ABI_LIBCXXRT
+#cmakedefine _LIBCPP_CXX_ABI_LIBSTDCXX
+#cmakedefine _LIBCPP_CXX_ABI_LIBSUPCXX
+#cmakedefine _LIBCPP_CXX_ABI_VCRUNTIME
+
 // PSTL backends
 #cmakedefine _LIBCPP_PSTL_BACKEND_SERIAL
 #cmakedefine _LIBCPP_PSTL_BACKEND_STD_THREAD
diff --git a/libcxx/include/__exception/exception_ptr.h b/libcxx/include/__exception/exception_ptr.h
index 796fa924be121..5a30fc875bce0 100644
--- a/libcxx/include/__exception/exception_ptr.h
+++ b/libcxx/include/__exception/exception_ptr.h
@@ -23,9 +23,29 @@
 #  pragma GCC system_header
 #endif
 
-#ifndef _LIBCPP_ABI_MICROSOFT
+// Previously, parts of exception_ptr were defined out-of-line, which prevented
+// useful compiler optimizations. Changing the out-of-line definitions to inline
+// definitions is an ABI break, however. To prevent this, we have to make sure
+// the symbols remain available in the libc++ library, in addition to being
+// defined inline here in this header.
+// To this end, we use _LIBCPP_EXPORTED_FROM_LIB_INLINEABLE macro:
+// The macro is defined as empty for src/exception.cpp, forcing the definitions of
+// the functions to be emitted and included in the library. When users of libc++
+// compile their code, the __gnu_inline__ attribute will suppress generation of
+// these functions while making their definitions available for inlining.
+#ifdef _LIBCPP_EMIT_CODE_FOR_EXCEPTION_PTR
+#  define _LIBCPP_EXPORTED_FROM_LIB_INLINEABLE _LIBCPP_EXPORTED_FROM_ABI
+#else
+#  if !__has_cpp_attribute(__gnu__::__gnu_inline__)
+#    error "GNU inline attribute is not supported"
+#  endif
+#  define _LIBCPP_EXPORTED_FROM_LIB_INLINEABLE [[__gnu__::__gnu_inline__]] inline
+#endif
 
-#  if _LIBCPP_AVAILABILITY_HAS_INIT_PRIMARY_EXCEPTION
+_LIBCPP_DIAGNOSTIC_PUSH
+_LIBCPP_CLANG_DIAGNOSTIC_IGNORED("-Wgnu-inline-cpp-without-extern")
+
+#ifdef _LIBCPP_AVAILABILITY_HAS_INIT_PRIMARY_EXCEPTION
 
 namespace __cxxabiv1 {
 
@@ -49,18 +69,25 @@ _LIBCPP_OVERRIDABLE_FUNC_VIS __cxa_exception* __cxa_init_primary_exception(
 
 } // namespace __cxxabiv1
 
-#  endif
-
 #endif
 
 _LIBCPP_BEGIN_UNVERSIONED_NAMESPACE_STD
 
-#ifndef _LIBCPP_ABI_MICROSOFT
+class _LIBCPP_EXPORTED_FROM_ABI exception_ptr;
+_LIBCPP_EXPORTED_FROM_LIB_INLINEABLE exception_ptr current_exception() _NOEXCEPT;
+[[__noreturn__]] _LIBCPP_EXPORTED_FROM_LIB_INLINEABLE void rethrow_exception(exception_ptr);
+
+#ifndef _LIBCPP_CXX_ABI_VCRUNTIME
 
 class _LIBCPP_EXPORTED_FROM_ABI exception_ptr {
   void* __ptr_;
 
-  static exception_ptr __from_native_exception_pointer(void*) _NOEXCEPT;
+  // Customization points to adjust the reference counting for cxxabi or
+  // libsupc++/libstdc++
+  _LIBCPP_HIDE_FROM_ABI _LIBCPP_ALWAYS_INLINE static void __increment_refcount(void* __ptr) _NOEXCEPT;
+  _LIBCPP_HIDE_FROM_ABI _LIBCPP_ALWAYS_INLINE static void __decrement_refcount(void* __ptr) _NOEXCEPT;
+
+  _LIBCPP_EXPORTED_FROM_LIB_INLINEABLE static exception_ptr __from_native_exception_pointer(void*) _NOEXCEPT;
 
   template <class _Ep>
   friend _LIBCPP_HIDE_FROM_ABI exception_ptr __make_exception_ptr_explicit(_Ep&) _NOEXCEPT;
@@ -74,9 +101,11 @@ class _LIBCPP_EXPORTED_FROM_ABI exception_ptr {
   _LIBCPP_HIDE_FROM_ABI exception_ptr() _NOEXCEPT : __ptr_() {}
   _LIBCPP_HIDE_FROM_ABI exception_ptr(nullptr_t) _NOEXCEPT : __ptr_() {}
 
-  exception_ptr(const exception_ptr&) _NOEXCEPT;
-  exception_ptr& operator=(const exception_ptr&) _NOEXCEPT;
-  ~exception_ptr() _NOEXCEPT;
+  _LIBCPP_EXPORTED_FROM_LIB_INLINEABLE exception_ptr(const exception_ptr&) _NOEXCEPT;
+  _LIBCPP_HIDE_FROM_ABI exception_ptr(exception_ptr&&) _NOEXCEPT;
+  _LIBCPP_EXPORTED_FROM_LIB_INLINEABLE exception_ptr& operator=(const exception_ptr&) _NOEXCEPT;
+  _LIBCPP_HIDE_FROM_ABI exception_ptr& operator=(exception_ptr&&) _NOEXCEPT;
+  _LIBCPP_EXPORTED_FROM_LIB_INLINEABLE ~exception_ptr() _NOEXCEPT;
 
   _LIBCPP_HIDE_FROM_ABI explicit operator bool() const _NOEXCEPT { return __ptr_ != nullptr; }
 
@@ -88,10 +117,54 @@ class _LIBCPP_EXPORTED_FROM_ABI exception_ptr {
     return !(__x == __y);
   }
 
-  friend _LIBCPP_EXPORTED_FROM_ABI exception_ptr current_exception() _NOEXCEPT;
-  friend _LIBCPP_EXPORTED_FROM_ABI void rethrow_exception(exception_ptr);
+  friend _LIBCPP_HIDE_FROM_ABI void swap(exception_ptr& __x, exception_ptr& __y) {
+    void* __tmp = __x.__ptr_;
+    __x.__ptr_  = __y.__ptr_;
+    __y.__ptr_  = __tmp;
+  }
+
+  friend exception_ptr current_exception() _NOEXCEPT;
+  friend void rethrow_exception(exception_ptr);
 };
 
+// Must be defined outside the class definition due to _LIBCPP_EXPORTED_FROM_LIB_INLINEABLE
+_LIBCPP_EXPORTED_FROM_LIB_INLINEABLE exception_ptr exception_ptr::__from_native_exception_pointer(void* __e) _NOEXCEPT {
+  __increment_refcount(__e);
+  exception_ptr __ptr;
+  __ptr.__ptr_ = __e;
+  return __ptr;
+}
+
+// Must be defined outside the class definition due to _LIBCPP_EXPORTED_FROM_LIB_INLINEABLE
+_LIBCPP_EXPORTED_FROM_LIB_INLINEABLE exception_ptr::exception_ptr(const exception_ptr& __other) _NOEXCEPT
+    : __ptr_(__other.__ptr_) {
+  __increment_refcount(__ptr_);
+}
+
+_LIBCPP_HIDE_FROM_ABI inline exception_ptr::exception_ptr(exception_ptr&& __other) _NOEXCEPT : __ptr_(__other.__ptr_) {
+  __other.__ptr_ = nullptr;
+}
+
+// Must be defined outside the class definition due to _LIBCPP_EXPORTED_FROM_LIB_INLINEABLE
+_LIBCPP_EXPORTED_FROM_LIB_INLINEABLE exception_ptr& exception_ptr::operator=(const exception_ptr& __other) _NOEXCEPT {
+  if (__ptr_ != __other.__ptr_) {
+    __increment_refcount(__other.__ptr_);
+    __decrement_refcount(__ptr_);
+    __ptr_ = __other.__ptr_;
+  }
+  return *this;
+}
+
+_LIBCPP_HIDE_FROM_ABI inline exception_ptr& exception_ptr::operator=(exception_ptr&& __other) _NOEXCEPT {
+  __decrement_refcount(__ptr_);
+  __ptr_         = __other.__ptr_;
+  __other.__ptr_ = nullptr;
+  return *this;
+}
+
+// Must be defined outside the class definition due to _LIBCPP_EXPORTED_FROM_LIB_INLINEABLE
+_LIBCPP_EXPORTED_FROM_LIB_INLINEABLE exception_ptr::~exception_ptr() _NOEXCEPT { __decrement_refcount(__ptr_); }
+
 #  if _LIBCPP_HAS_EXCEPTIONS
 #    if _LIBCPP_AVAILABILITY_HAS_INIT_PRIMARY_EXCEPTION
 template <class _Ep>
@@ -159,7 +232,7 @@ _LIBCPP_HIDE_FROM_ABI exception_ptr make_exception_ptr(_Ep) _NOEXCEPT {
 
 #else // _LIBCPP_ABI_MICROSOFT
 
-class _LIBCPP_EXPORTED_FROM_ABI exception_ptr {
+class exception_ptr {
   _LIBCPP_DIAGNOSTIC_PUSH
   _LIBCPP_CLANG_DIAGNOSTIC_IGNORED("-Wunused-private-field")
   void* __ptr1_;
@@ -167,26 +240,26 @@ class _LIBCPP_EXPORTED_FROM_ABI exception_ptr {
   _LIBCPP_DIAGNOSTIC_POP
 
 public:
-  exception_ptr() _NOEXCEPT;
-  exception_ptr(nullptr_t) _NOEXCEPT;
-  exception_ptr(const exception_ptr& __other) _NOEXCEPT;
-  exception_ptr& operator=(const exception_ptr& __other) _NOEXCEPT;
-  exception_ptr& operator=(nullptr_t) _NOEXCEPT;
-  ~exception_ptr() _NOEXCEPT;
-  explicit operator bool() const _NOEXCEPT;
+  _LIBCPP_EXPORTED_FROM_LIB_INLINEABLE exception_ptr() _NOEXCEPT;
+  _LIBCPP_EXPORTED_FROM_LIB_INLINEABLE exception_ptr(nullptr_t) _NOEXCEPT;
+  _LIBCPP_EXPORTED_FROM_LIB_INLINEABLE exception_ptr(const exception_ptr& __other) _NOEXCEPT;
+  _LIBCPP_EXPORTED_FROM_LIB_INLINEABLE exception_ptr& operator=(const exception_ptr& __other) _NOEXCEPT;
+  _LIBCPP_EXPORTED_FROM_LIB_INLINEABLE exception_ptr& operator=(nullptr_t) _NOEXCEPT;
+  _LIBCPP_EXPORTED_FROM_LIB_INLINEABLE ~exception_ptr() _NOEXCEPT;
+  _LIBCPP_EXPORTED_FROM_LIB_INLINEABLE explicit operator bool() const _NOEXCEPT;
 };
 
-_LIBCPP_EXPORTED_FROM_ABI bool operator==(const exception_ptr& __x, const exception_ptr& __y) _NOEXCEPT;
+_LIBCPP_EXPORTED_FROM_LIB_INLINEABLE bool operator==(const exception_ptr& __x, const exception_ptr& __y) _NOEXCEPT;
 
 inline _LIBCPP_HIDE_FROM_ABI bool operator!=(const exception_ptr& __x, const exception_ptr& __y) _NOEXCEPT {
   return !(__x == __y);
 }
 
-_LIBCPP_EXPORTED_FROM_ABI void swap(exception_ptr&, exception_ptr&) _NOEXCEPT;
+_LIBCPP_EXPORTED_FROM_LIB_INLINEABLE void swap(exception_ptr&, exception_ptr&) _NOEXCEPT;
 
-_LIBCPP_EXPORTED_FROM_ABI exception_ptr __copy_exception_ptr(void* __except, const void* __ptr);
-_LIBCPP_EXPORTED_FROM_ABI exception_ptr current_exception() _NOEXCEPT;
-[[__noreturn__]] _LIBCPP_EXPORTED_FROM_ABI void rethrow_exception(exception_ptr);
+_LIBCPP_EXPORTED_FROM_LIB_INLINEABLE exception_ptr __copy_exception_ptr(void* __except, const void* __ptr);
+_LIBCPP_EXPORTED_FROM_LIB_INLINEABLE exception_ptr current_exception() _NOEXCEPT;
+[[__noreturn__]] _LIBCPP_EXPORTED_FROM_LIB_INLINEABLE void rethrow_exception(exception_ptr);
 
 // This is a built-in template function which automagically extracts the required
 // information.
@@ -201,4 +274,18 @@ _LIBCPP_HIDE_FROM_ABI exception_ptr make_exception_ptr(_Ep __e) _NOEXCEPT {
 #endif // _LIBCPP_ABI_MICROSOFT
 _LIBCPP_END_UNVERSIONED_NAMESPACE_STD
 
+#if defined(_LIBCPP_CXX_ABI_NONE)
+#  include <__exception/exception_ptr_unimplemented.ipp>
+#elif defined(_LIBCPP_CXX_ABI_LIBCXXABI) || defined(_LIBCPP_CXX_ABI_LIBCXXRT)
+#  include <__exception/exception_ptr_cxxabi.ipp>
+#elif defined(_LIBCPP_CXX_ABI_LIBSTDCXX) || defined(_LIBCPP_CXX_ABI_LIBSUPCXX)
+#  include <__exception/exception_ptr_glibcxx.ipp>
+#elif defined(_LIBCPP_CXX_ABI_VCRUNTIME)
+#  include <__exception/exception_ptr_msvc.ipp>
+#else
+#  error "Unsupported C++ ABI library"
+#endif
+
+_LIBCPP_DIAGNOSTIC_POP
+
 #endif // _LIBCPP___EXCEPTION_EXCEPTION_PTR_H
diff --git a/libcxx/include/__exception/exception_ptr_cxxabi.ipp b/libcxx/include/__exception/exception_ptr_cxxabi.ipp
new file mode 100644
index 0000000000000..187a6e0ad94c9
--- /dev/null
+++ b/libcxx/include/__exception/exception_ptr_cxxabi.ipp
@@ -0,0 +1,50 @@
+// -*- C++ -*-
+//===----------------------------------------------------------------------===//
+//
+// Part of the LLVM Project, under the Apache License v2.0 with LLVM Exceptions.
+// See https://llvm.org/LICENSE.txt for license information.
+// SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception
+//
+//===----------------------------------------------------------------------===//
+
+#include <__exception/terminate.h>
+
+namespace __cxxabiv1 {
+
+extern "C" {
+_LIBCPP_OVERRIDABLE_FUNC_VIS void __cxa_increment_exception_refcount(void*) _NOEXCEPT;
+_LIBCPP_OVERRIDABLE_FUNC_VIS void __cxa_decrement_exception_refcount(void*) _NOEXCEPT;
+_LIBCPP_OVERRIDABLE_FUNC_VIS void* __cxa_current_primary_exception() _NOEXCEPT;
+_LIBCPP_OVERRIDABLE_FUNC_VIS void __cxa_rethrow_primary_exception(void*);
+}
+  
+} // namespace __cxxabiv1
+
+namespace std {
+
+_LIBCPP_HIDE_FROM_ABI _LIBCPP_ALWAYS_INLINE inline void exception_ptr::__increment_refcount(void* __ptr) _NOEXCEPT {
+  if (__ptr)
+    __cxxabiv1::__cxa_increment_exception_refcount(__ptr);
+}
+
+_LIBCPP_HIDE_FROM_ABI _LIBCPP_ALWAYS_INLINE inline void exception_ptr::__decrement_refcount(void* __ptr) _NOEXCEPT {
+  if (__ptr)
+    __cxxabiv1::__cxa_decrement_exception_refcount(__ptr);
+}
+
+_LIBCPP_EXPORTED_FROM_LIB_INLINEABLE exception_ptr current_exception() _NOEXCEPT {
+  // It would be nicer if there was a constructor that took a ptr, then
+  // this whole function would be just:
+  //    return exception_ptr(__cxa_current_primary_exception());
+  exception_ptr __ptr;
+  __ptr.__ptr_ =  __cxxabiv1::__cxa_current_primary_exception();
+  return __ptr;
+}
+
+_LIBCPP_EXPORTED_FROM_LIB_INLINEABLE void rethrow_exception(exception_ptr __ptr) {
+  __cxxabiv1::__cxa_rethrow_primary_exception(__ptr.__ptr_);
+  // if __ptr.__ptr_ is NULL, above returns so we terminate.
+  terminate();
+}
+
+} // namespace std
diff --git a/libcxx/include/__exception/exception_ptr_glibcxx.ipp b/libcxx/include/__exception/exception_ptr_glibcxx.ipp
new file mode 100644
index 0000000000000..3f054249bca83
--- /dev/null
+++ b/libcxx/include/__exception/exception_ptr_glibcxx.ipp
@@ -0,0 +1,47 @@
+// -*- C++ -*-
+//===----------------------------------------------------------------------===//
+//
+// Part of the LLVM Project, under the Apache License v2.0 with LLVM Exceptions.
+// See https://llvm.org/LICENSE.txt for license information.
+// SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception
+//
+//===----------------------------------------------------------------------===//
+
+namespace std {
+
+// libsupc++ does not implement the dependent EH ABI and the functionality
+// it uses to implement std::exception_ptr (which it declares as an alias of
+// std::__exception_ptr::exception_ptr) is not directly exported to clients. So
+// we have little choice but to hijack std::__exception_ptr::exception_ptr's
+// _M_addref and _M_release (which are part of its ABI), and its
+// rethrow_exception(std::__exception_ptr::exception_ptr) function. Fortunately,
+// glibcxx's exception_ptr has the same layout as our exception_ptr and we can
+// reinterpret_cast between the two.
+namespace __exception_ptr {
+
+struct exception_ptr {
+  void* __ptr_;
+
+  void _M_addref() _GLIBCXX_USE_NOEXCEPT;
+  void _M_release() _GLIBCXX_USE_NOEXCEPT;
+};
+
+} // namespace __exception_ptr
+
+_LIBCPP_HIDE_FROM_ABI _LIBCPP_ALWAYS_INLINE inline void exception_ptr::__increment_refcount(void* __ptr) _NOEXCEPT {
+  if (__ptr)
+    reinterpret_cast<__exception_ptr::exception_ptr*>(this)->_M_addref();
+}
+
+_LIBCPP_HIDE_FROM_ABI _LIBCPP_ALWAYS_INLINE inline void exception_ptr::__decrement_refcount(void* __ptr) _NOEXCEPT {
+  if (__ptr)
+    reinterpret_cast<__exception_ptr::exception_ptr*>(this)->_M_release();
+}
+
+[[__noreturn__]] void rethrow_exception(__exception_ptr::exception_ptr);
+
+[[__noreturn__]] _LIBCPP_EXPORTED_FROM_LIB_INLINEABLE void rethrow_exception(exception_ptr __ptr) {
+  rethrow_exception(reinterpret_cast<__exception_ptr::exception_ptr&>(__ptr));
+}
+
+} // namespace std
diff --git a/libcxx/src/support/runtime/exception_pointer_msvc.ipp b/libcxx/include/__exception/exception_ptr_msvc.ipp
similarity index 50%
rename from libcxx/src/support/runtime/exception_pointer_msvc.ipp
rename to libcxx/include/__exception/exception_ptr_msvc.ipp
index 2be5136176e32..d893045ba17b0 100644
--- a/libcxx/src/support/runtime/exception_pointer_msvc.ipp
+++ b/libcxx/include/__exception/exception_ptr_msvc.ipp
@@ -7,7 +7,6 @@
 //
 //===----------------------------------------------------------------------===//
 
-#include <stdio.h>
 #include <stdlib.h>
 
 _LIBCPP_CRT_FUNC void __cdecl __ExceptionPtrCreate(void*);
@@ -23,54 +22,44 @@ _LIBCPP_CRT_FUNC void __cdecl __ExceptionPtrCopyException(void*, const void*, co
 
 namespace std {
 
-exception_ptr::exception_ptr() noexcept { __ExceptionPtrCreate(this); }
-exception_ptr::exception_ptr(nullptr_t) noexcept { __ExceptionPtrCreate(this); }
+_LIBCPP_EXPORTED_FROM_LIB_...
[truncated]

@vogelsgesang
Copy link
Member Author

This should be ready for a first round of reviews now :)

Pre-commit tests finally look good. The errors in buildkite seem to be infrastructure issues. Same for macos, which has been queued for 2+ hours now.

This commit optimizes the performance for `std::exception_ptr`
for an empty exception objects.

To do so, we use 3 high-level approaches:
1. Moving the implementation from the libc++ library into the libc++
   headers, thereby allowing the compiler to inline the function bodies.
2. Adding fast paths to the (now inlineable) bodies, checking for the
   empty case.
3. Adding move constuctor, assignment and `swap`

Those optimizations were implemented for the libc++abi, libsupc++ and
libstdc++ ABIs.

Fixes #XXX

Performance
-----------

With this change, the compiler can now completely constant-fold
https://godbolt.org/z/NaNKe5. Also in cases where the compiler cannot
statically prove that exception_ptr is empty, it can at least generate a
fast-path check if the exception_ptr is empty, without calling into the
library.

ABI compatibility
-----------------

We use a new visibility macro `_LIBCPP_EXPORTED_FROM_ABI_INLINEABLE` to
ensure that the functions which are now declared in the header are still
exported by the library. See the description in `VisibilityMacros.rst`
for details. This approach was originally proposed by Nikolas Klauser
in https://reviews.llvm.org/D122536

Moving implementations to the header
------------------------------------

To move the implementation to the header, we must know the selected
LIBCXX_CXX_ABI also in the headers. For that purpose, the LIBCXX_CXX_ABI
configuration is now exposed via `__config_site`.

While the Microsoft ABI and the "unimplemented" APIs do not benefit
from this optimizations, I also moved their implementation to the
headers for more uniformity. Mid-term, we probably have to expose all
implementations in the header, anyway, because P3068R6 mandates all
methods of `exception_ptr` to be constexpr.

Unifying libc++abi, libsupc++ and `none`
-----------------------------------------------

Both libc++abi and libsupc++ are reference-counted. The primary
difference is the function called for ref-counting:
* libc++api uses `__cxa_{in,de}crement_exception_refcount`
* libsupc++ uses `__exception_ptr::_M_{addref,release}`

This commit factors out the common reference-counting logic into a
shared header.

Our libsupc++ implementation so far did not take advantage of
`_M_addref`/`_M_release`. For the performance benefits of this PR it was
necessary to start using them.

The same reference-counted approach is also reused for the
`none`/`unimplemented` implementation. This has the side effect, that
users can now use empty `exception_ptr`s even in the `none` ABI. The
abort is only triggered for non-empty `exception_ptr`s. Given this
simplifies the code, I think change is acceptable.

Unifying nested_exception
-------------------------

The implementation for `nested_exception` was effectively identical
across all ABIs. For most ABIs, the source code was literally identical.
There were only two deviations:
* For libsupc++ or libstdc++, we did not use define the
  `~nested_exception` destructor.
* The abort in `nested_exception::rethrow_nested` was unnecessary as the
  same abort is also present in the `rethrow_exception` implementation.
  As such, we were able to simply remove that special casing.

The implementation is now unified directly in the `__nested_exception.h`
header.

Standard conformance
--------------------

The available constructors, operators and methods of `exception_ptr` is
not specified by the standard. As such, adding the move constructor and
the assignment operator are standard conformant. libstdc++ made the same
decision and also provides those headers.
@vogelsgesang vogelsgesang force-pushed the avogelsgesang-std-exception-optimization branch from 3815568 to 484cfaa Compare October 13, 2025 14:24
Copy link
Contributor

@philnik777 philnik777 left a comment

Choose a reason for hiding this comment

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

I feel like this is a very aggressive approach, and I'm not convinced it's actually necessary. Can we split this up into a few patches so we can determine better what parts are actually important and which ones we can drop?

I would suggest the following:

  1. Introduce move ctor/assign and possibly swap - this seems like a rather obvious improvement and should be in no way controversial (the swap may or may not actually be necessary, we'll have to check).
  2. inline trivial functions into the headers.
  3. consider whether we can add any attributes to allow improved code gen
  4. do the inlining of the rest of the functions you want, assuming you still think it's worth the cost.


namespace std {

_LIBCPP_HIDE_FROM_ABI _LIBCPP_ALWAYS_INLINE inline void exception_ptr::__increment_refcount(void* __ptr) _NOEXCEPT {
Copy link
Contributor

Choose a reason for hiding this comment

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

Please don't spray always_inline everywhere.

Copy link
Member Author

Choose a reason for hiding this comment

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

I only added it to exception_ptr::__{increment,decrement}_refcount since this was a net-new indirection which I introduced also into already pre-existing functions (such as exception_ptr::~exception_ptr) and I considered it to be less risky if I mark this indirection with _LIBCPP_ALWAYS_INLINE to make sure the compiler actually removes that indirection again.

That being said, I guess I was overly careful here. I will remove it

exception_ptr& operator=(nullptr_t) _NOEXCEPT;
~exception_ptr() _NOEXCEPT;
explicit operator bool() const _NOEXCEPT;
_LIBCPP_EXPORTED_FROM_LIB_INLINEABLE exception_ptr() _NOEXCEPT;
Copy link
Contributor

Choose a reason for hiding this comment

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

I'm not convinced this new macro is necessary. IIUC these functions are supposed to be inlined, so there isn't much point in providing an external version (except for ABI of course, but we don't need to use the dylib functions).

Copy link
Member Author

@vogelsgesang vogelsgesang Oct 18, 2025

Choose a reason for hiding this comment

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

Full disclosure: This isn't actually my idea. It was yours (modulo potential misunderstandings on my side). It was originally proposed back in April 2022 in https://reviews.llvm.org/D122536. Happy to adjust it however you prefer

IIUC these functions are supposed to be inlined, so there isn't much point in providing an external version

correct

except for ABI of course

How should we ensure that the functions are still emitted as part of the dylib? Currently I rely on _LIBCPP_EXPORTED_FROM_LIB_INLINEABLE to be marked inline in the headers but as non-inline in the dylib. By marking it non-inline in the dylib, I ensure that the compiler actually emits code for it. If I would mark them inline also in the dylib, the compiler wouldn't actually emit any code, since the functions are never called / instantiated by the dylib

except for ABI of course but we don't need to use the dylib functions

Right, I think we can let the compiler choose freely to either use the inlined or the dylib-provided version. This means we might not need [[gnu::inline]] / __attribute__((__gnu_inline__)).

I used it primarily because you proposed its usage back in April 2022 in https://reviews.llvm.org/D122536. It provides a slight benefit that the linker has less work to do (it doesn't have to discard many duplicated definitions of the same inline function). Not sure if we consider this to be worth it, though 🤷

@vogelsgesang
Copy link
Member Author

vogelsgesang commented Oct 18, 2025

Thanks for your review!

Splitting the PR

Can we split this up [...]?

Happy to split it up 🙂

I would suggest a slightly different splitting across the commits, though:

  1. introduce benchmark
  2. change src/exception_pointer_glibcxx.ipp to use _M_{add,release}ref (still leaving the implementation in the library)
  3. expose the selected ABI via __config_site.in and change existing code to use it
  4. introduce move ctor/assign and possibly swap (the swap may or may not actually be necessary, we'll have to check)
  5. inline functions into the header and add the if (ptr_) fast paths to the inlined functions

Are we aligned at least for steps (1)-(4)? If so I will go ahead and open the corresponding PRs for those steps right away

However, I am not sure if splitting (4) and (5) into separate steps is actually a good idea.
Step (4) would introduce new symbols to the library which would not be needed after step (5) anymore.
I guess we should still split it into two separate PRs, but make sure to ship them immediately after each other, so we don't have new symbols in the library?
Or maybe we should swap (4) and (5) around?

Scratch that - I thought about this incorrectly. I will just mark the move ctor/assign/swap as inline from the get-go, and I thereby won't run into that problem

Attributes for improving codegen

consider whether we can add any attributes to allow improved code gen

I am not quite sure what that step would actually entail. I am not currently aware of any specific attributes which would improve codegen. Do you have any specific attributes in mind?

Inlining of rethrow_exception & current_exception

do the inlining of the rest of the functions you want, assuming you still think it's worth the cost.

Inlining the destructor, move assignment etc. will be necessary for performance.
Inlining rethrow_exception and current_exception will not be worth it performance-wise.

I inlined rethrow_exception and current_exception primarily because

  1. it kept the per-ABI code more local / coherent. Instead of having two files per exception ABI (one in include and one in src), I preferred having a single file per ABI (in include)
  2. it needed less C++ boilerplate. I only needed the ABI dispatching #ifdefs in the include directory once instead of twice (once in include, once in src).

WDYT about those points? Does it make sense to inline rethrow_exception and current_exception for code simplicity, even if it is not necessary for performance? ("Let's see later, as soon as we reach step (5)" is also a perfectly fine answer for me)

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

Labels

libc++ libc++ C++ Standard Library. Not GNU libstdc++. Not libc++abi. performance

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Optimize exception_ptr, especially for common cases.

3 participants