[libcxx-commits] [libcxx] [libc++] Optimize `to_chars` for bases 2, 8 and 16 (PR #223222)
David Bayer via libcxx-commits
libcxx-commits at lists.llvm.org
Sun Sep 13 02:34:41 PDT 2026
https://github.com/davebayer created https://github.com/llvm/llvm-project/pull/223222
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.
>From 7b9c7422e53f29d402ef24e8fe4e13166ccfcf72 Mon Sep 17 00:00:00 2001
From: David Bayer <bayer.david99 at gmail.com>
Date: Sun, 13 Sep 2026 10:45:19 +0200
Subject: [PATCH] [libc++] Optimize `to_chars` for bases 2, 8 and 16
---
libcxx/include/__charconv/to_chars_integral.h | 114 ++++++++++++------
1 file changed, 78 insertions(+), 36 deletions(-)
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)};
}
};
More information about the libcxx-commits
mailing list