[libcxx-commits] [libcxx] [libc++] Extend the scope of radix sorting inside std::stable_sort to floating-point types (PR #129452)
Дмитрий Изволов via libcxx-commits
libcxx-commits at lists.llvm.org
Thu Mar 27 00:27:51 PDT 2025
================
@@ -298,6 +301,84 @@ _LIBCPP_HIDE_FROM_ABI constexpr auto __shift_to_unsigned(_Ip __n) {
return static_cast<make_unsigned_t<_Ip> >(__n ^ __min_value);
}
+template <size_t _Size>
+struct __unsigned_integer_of_size {};
+
+template <>
+struct __unsigned_integer_of_size<1> {
+ using type = uint8_t;
+};
+
+template <>
+struct __unsigned_integer_of_size<2> {
+ using type = uint16_t;
+};
+
+template <>
+struct __unsigned_integer_of_size<4> {
+ using type = uint32_t;
+};
+
+template <>
+struct __unsigned_integer_of_size<8> {
+ using type = uint64_t;
+};
+
+template <>
+struct __unsigned_integer_of_size<16> {
+# if _LIBCPP_HAS_INT128
+ using type = __int128;
+# endif
+};
+
+template <size_t _Size>
+using __unsigned_integer_of_size_t _LIBCPP_NODEBUG = typename __unsigned_integer_of_size<_Size>::type;
+
+template <class _Sc>
+using __unsigned_representation_for_t _LIBCPP_NODEBUG = __unsigned_integer_of_size_t<sizeof(_Sc)>;
+
+// The function `__to_ordered_integral` is defined for integers and IEEE 754 floating-point numbers.
+// Returns an integer representation such that for any `x` and `y` such that `x < y`, the expression
+// `__to_ordered_integral(x) < __to_ordered_integral(y)` is true, where `x`, `y` are integers or IEEE 754 floats.
+template <class _Integral, enable_if_t< is_integral<_Integral>::value, int> = 0>
+_LIBCPP_HIDE_FROM_ABI constexpr auto __to_ordered_integral(_Integral __n) {
+ return __n;
+}
+
+// An overload for IEEE 754 floating-point numbers
+
+// For the floats conforming to IEEE 754 (IEC 559) standard, we know that:
+// 1. The bit representation of positive floats directly reflects their order:
+// When comparing floats by magnitude, the number with the larger exponent is greater, and if the exponents are
+// equal, the one with the larger mantissa is greater.
+// 2. The bit representation of negative floats reflects their reverse order (for the same reasons).
+// 3. The most significant bit (sign bit) is zero for positive floats and one for negative floats. Therefore, in the raw
+// bit representation, any negative number will be greater than any positive number.
+
+// The only exception from this rule is `NaN`, which is unordered by definition.
+
+// Based on the above, to obtain correctly ordered integral representation of floating-point numbers, we need to:
+// 1. Invert the bit representation (including the sign bit) of negative floats to switch from reverse order to direct
+// order;
+// 2. Invert the sign bit for positive floats.
+
+// Thus, in final integral representation, we have reversed the order for negative floats and made all negative floats
+// smaller than all positive numbers (by inverting the sign bit).
+template <class _Floating, enable_if_t< numeric_limits<_Floating>::is_iec559, int> = 0>
----------------
izvolov wrote:
Had to tinker with `long double`.
First, it turned out that `is_iec559` can be true not only for 128-bit `long double` but also for 80-bit ones.
At the same time, the actual size of an 80-bit `long double` (what `sizeof` returns) can be 10, 12, or 16.
So, in general, you can’t determine its bit width based on the type’s size.
Tried two approaches to determine the bit width:
1. **Using `numeric_limits`:**
Tried calculating it as the sum of the mantissa, exponent, and sign bit sizes.
The problem here is that for all types except `long double`, the mantissa size includes the hidden bit.
For example, `std::numeric_limits<float>::digits == 24`, even though the mantissa is actually stored in 23 bits.
But for 80-bit `long double`, this isn’t the case: its mantissa is stored in 64 bits, and `std::numeric_limits<long double>::digits == 64`.
As a result, the sum `std::numeric_limits<Float>::digits + bit_log2(static_cast<std::size_t>(std::numeric_limits<Float>::max_exponent) + 1)`
gives the correct bit count for all floating-point types except `long double`.
And it seems impossible to adjust this formula for the special case of `long double` because, on some architectures, discrepancies might arise again.
2. **Determining padding size:**
Online sources claimed that if an 80-bit `long double` is represented as a 12-byte or 16-byte value,
the unused bits are padded with zeros.
Decided to use this assumption and compute the bit width based on the padding size.
The idea was to take a negative number (its most significant bit—the sign bit—would be `1`) and count the number of leading zero bits after the sign bit.
This didn’t work either because I encountered cases where the padding wasn’t necessarily zeros but could contain arbitrary data.
Also I've encountered a problem that `bit_cast` from `long double` to `uint128` is not working:
```
# | /__w/llvm-project/llvm-project/build/generic-cxx26/libcxx/test-suite-install/include/c++/v1/__bit/bit_cast.h:26:10: note: indeterminate value can only initialize an object of type 'unsigned char' or 'std::byte'; 'unsigned __int128' is invalid
# | 26 | return __builtin_bit_cast(_ToType, __from);
# | | ^
# | /__w/llvm-project/llvm-project/build/generic-cxx26/libcxx/test-suite-install/include/c++/v1/__algorithm/radix_sort.h:353:29: note: in call to '__bit_cast<unsigned __int128, long double>(-numeric_limits<long double>::infinity())'
# | 353 | std::__countl_zero(std::__bit_cast<__integral_type>(-numeric_limits<_Floating>::infinity()));
# | | ^~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
```
Came to the conclusion that `long double` should be excluded for now, leaving only `float` and `double`.
Meanwhile, the new floating-point types from `<stdfloat>` should work "out of the box" since they are defined in compliance with IEEE 754 from the start.
https://github.com/llvm/llvm-project/pull/129452
More information about the libcxx-commits
mailing list