[libcxx-commits] [libcxx] [libc++] Optimize `to_chars` for bases 2, 8 and 16 (PR #223222)

via libcxx-commits libcxx-commits at lists.llvm.org
Sun Sep 13 02:36:07 PDT 2026


llvmorg-github-actions[bot] wrote:


<!--LLVM PR SUMMARY COMMENT-->

@llvm/pr-subscribers-libcxx

Author: David Bayer (davebayer)

<details>
<summary>Changes</summary>

I've come up with an optimization for `std::to_chars` that instead of character lookup computes the characters using SIMD within register. The idea is to use `std::bit_decompress` behaviour to distribute the selected bits to the start of each byte in a register and add the `0` or `a` character offset using a single addition.

However, this optimization is dependent on the `std::bit_decompress` implementation. For example, when BMI2 on x86 is available, it is compiled to a single `pext` instruction.

I've ran some benchmarks on my Intel i9-14900K with `-O3 -mbmi2` flags using 8 characters/register and the results are:

Type | Base 2 old → new | Base 8 old → new | Base 16 old → new
-- | -- | -- | --
u8 | 350 → 802 ns (0.44×) | 326 → 475 ns (0.69×) | 281 → 494 ns (0.57×)
u16 | 703 → 881 ns (0.80×) | 423 → 388 ns (1.09×) | 283 → 509 ns (0.56×)
u32 | 1320 → 1046 ns (1.26×) | 897 → 517 ns (1.74×) | 644 → 500 ns (1.29×)
u64 | 2024 → 1605 ns (1.26×) | 1538 → 593 ns (2.59×) | 841 → 563 ns (1.49×)
u128 | 4905 → 3747 ns (1.31×) | 3140 → 1313 ns (2.39×) | 1971 → 1141 ns (1.73×)

For small types, it's not worth it, but for 32-bit and wider types, we can get some substantial improvements.

Would you be interested in having this optimization when a `pext`-like instruction is available?

If yes, then I'll rework the implementation to change the chars/register based on the platform's word size as well.


I've used GPT-5.6 SOL to help me do the benchmarks & optimize the base 16 path.

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


1 Files Affected:

- (modified) libcxx/include/__charconv/to_chars_integral.h (+78-36) 


``````````diff
diff --git a/libcxx/include/__charconv/to_chars_integral.h b/libcxx/include/__charconv/to_chars_integral.h
index 6d425139260b6..3fbbd6a8f8c3c 100644
--- a/libcxx/include/__charconv/to_chars_integral.h
+++ b/libcxx/include/__charconv/to_chars_integral.h
@@ -110,6 +110,11 @@ namespace __itoa {
 template <unsigned _Base>
 struct _LIBCPP_HIDDEN __integral;
 
+template <size_t _Np>
+struct _LIBCPP_HIDDEN __chars_storage {
+  char __data[_Np];
+};
+
 template <>
 struct _LIBCPP_HIDDEN __integral<2> {
   template <typename _Tp>
@@ -126,21 +131,32 @@ struct _LIBCPP_HIDDEN __integral<2> {
     int __n         = __width(__value);
     if (__n > __cap)
       return {__last, errc::value_too_large};
-
     __last                   = __first + __n;
     char* __p                = __last;
-    const unsigned __divisor = 16;
-    while (__value > __divisor) {
-      unsigned __c = __value % __divisor;
+
+    constexpr auto __ncs_per_word   = 8u;
+    constexpr auto __divisor        = 1u << __ncs_per_word;
+    constexpr auto __dep_mask       = uint64_t{0x0101'0101'0101'0101};
+    constexpr auto __c0_offset_mask = uint64_t{0x3030'3030'3030'3030};
+    while (__value >= __divisor) {
+      const auto __digits = __builtin_elementwise_pdep(static_cast<uint64_t>(__value), __dep_mask);
+      auto __cs            = __digits | __c0_offset_mask;
+#ifdef _LIBCPP_LITTLE_ENDIAN
+      __cs = __builtin_bswap64(__cs);
+#endif
       __value /= __divisor;
-      __p -= 4;
-      std::copy_n(&__base_2_lut[4 * __c], 4, __p);
+      __p -= __ncs_per_word;
+      const auto __chars = __builtin_bit_cast(__chars_storage<__ncs_per_word>, __cs);
+      __builtin_memcpy(__p, __chars.__data, __ncs_per_word);
     }
-    do {
-      unsigned __c = __value % 2;
-      __value /= 2;
-      *--__p = "01"[__c];
-    } while (__value != 0);
+    const auto __digits = __builtin_elementwise_pdep(static_cast<uint64_t>(__value), __dep_mask);
+    auto __cs            = __digits | __c0_offset_mask;
+#ifdef _LIBCPP_LITTLE_ENDIAN
+    __cs = __builtin_bswap64(__cs);
+#endif
+    const auto __chars = __builtin_bit_cast(__chars_storage<__ncs_per_word>, __cs);
+    const auto __remaining = static_cast<size_t>(__p - __first);
+    __builtin_memcpy(__first, __chars.__data + __ncs_per_word - __remaining, __remaining);
     return {__last, errc(0)};
   }
 };
@@ -161,21 +177,32 @@ struct _LIBCPP_HIDDEN __integral<8> {
     int __n         = __width(__value);
     if (__n > __cap)
       return {__last, errc::value_too_large};
+    __last                   = __first + __n;
+    char* __p                = __last;
 
-    __last             = __first + __n;
-    char* __p          = __last;
-    unsigned __divisor = 64;
-    while (__value > __divisor) {
-      unsigned __c = __value % __divisor;
+    constexpr auto __ncs_per_word   = 8u;
+    constexpr auto __divisor        = 1u << (3 * __ncs_per_word);
+    constexpr auto __dep_mask       = uint64_t{0x0707'0707'0707'0707};
+    constexpr auto __c0_offset_mask = uint64_t{0x3030'3030'3030'3030};
+    while (__value >= __divisor) {
+      const auto __digits = __builtin_elementwise_pdep(static_cast<uint64_t>(__value), __dep_mask);
+      auto __cs            = __digits | __c0_offset_mask;
+#ifdef _LIBCPP_LITTLE_ENDIAN
+      __cs = __builtin_bswap64(__cs);
+#endif
       __value /= __divisor;
-      __p -= 2;
-      std::copy_n(&__base_8_lut[2 * __c], 2, __p);
+      __p -= __ncs_per_word;
+      const auto __chars = __builtin_bit_cast(__chars_storage<__ncs_per_word>, __cs);
+      __builtin_memcpy(__p, __chars.__data, __ncs_per_word);
     }
-    do {
-      unsigned __c = __value % 8;
-      __value /= 8;
-      *--__p = "01234567"[__c];
-    } while (__value != 0);
+    const auto __digits = __builtin_elementwise_pdep(static_cast<uint64_t>(__value), __dep_mask);
+    auto __cs            = __digits | __c0_offset_mask;
+#ifdef _LIBCPP_LITTLE_ENDIAN
+    __cs = __builtin_bswap64(__cs);
+#endif
+    const auto __chars = __builtin_bit_cast(__chars_storage<__ncs_per_word>, __cs);
+    const auto __remaining = static_cast<size_t>(__p - __first);
+    __builtin_memcpy(__first, __chars.__data + __ncs_per_word - __remaining, __remaining);
     return {__last, errc(0)};
   }
 };
@@ -197,21 +224,36 @@ struct _LIBCPP_HIDDEN __integral<16> {
     if (__n > __cap)
       return {__last, errc::value_too_large};
 
-    __last             = __first + __n;
-    char* __p          = __last;
-    unsigned __divisor = 256;
-    while (__value > __divisor) {
-      unsigned __c = __value % __divisor;
+    __last    = __first + __n;
+    char* __p = __last;
+
+    constexpr auto __ncs_per_word   = 8u;
+    constexpr auto __divisor        = uint64_t{1} << (4 * __ncs_per_word);
+    constexpr auto __dep_mask       = uint64_t{0x0f0f'0f0f'0f0f'0f0f};
+    constexpr auto __add_six_mask   = uint64_t{0x0606'0606'0606'0606};
+    constexpr auto __high_bit_mask  = uint64_t{0x1010'1010'1010'1010};
+    constexpr auto __c0_offset_mask = uint64_t{0x3030'3030'3030'3030};
+    while (__value >= __divisor) {
+      const auto __digits = __builtin_elementwise_pdep(static_cast<uint64_t>(__value), __dep_mask);
+    const auto __alpha = ((__digits + __add_six_mask) & __high_bit_mask) >> 4;
+    auto __cs          = __digits + __c0_offset_mask + __alpha * 0x27;
+#ifdef _LIBCPP_LITTLE_ENDIAN
+      __cs = __builtin_bswap64(__cs);
+#endif
       __value /= __divisor;
-      __p -= 2;
-      std::copy_n(&__base_16_lut[2 * __c], 2, __p);
+      __p -= __ncs_per_word;
+      const auto __chars = __builtin_bit_cast(__chars_storage<__ncs_per_word>, __cs);
+      __builtin_memcpy(__p, __chars.__data, __ncs_per_word);
     }
-    if (__first != __last)
-      do {
-        unsigned __c = __value % 16;
-        __value /= 16;
-        *--__p = "0123456789abcdef"[__c];
-      } while (__value != 0);
+    const auto __digits = __builtin_elementwise_pdep(static_cast<uint64_t>(__value), __dep_mask);
+    const auto __alpha = ((__digits + __add_six_mask) & __high_bit_mask) >> 4;
+    auto __cs          = __digits + __c0_offset_mask + __alpha * 0x27;
+#ifdef _LIBCPP_LITTLE_ENDIAN
+    __cs = __builtin_bswap64(__cs);
+#endif
+    const auto __chars     = __builtin_bit_cast(__chars_storage<__ncs_per_word>, __cs);
+    const auto __remaining = static_cast<size_t>(__p - __first);
+    __builtin_memcpy(__first, __chars.__data + __ncs_per_word - __remaining, __remaining);
     return {__last, errc(0)};
   }
 };

``````````

</details>


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


More information about the libcxx-commits mailing list