Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
29 commits
Select commit Hold shift + click to select a range
86958ab
benchmark
AlexGuteniev Aug 23, 2025
8b6f863
100 branch in to_string impl
AlexGuteniev Aug 23, 2025
84b3d1a
sort better
AlexGuteniev Aug 23, 2025
08b83da
hack around some linker issue in C++14
AlexGuteniev Aug 24, 2025
068234b
Merge branch 'microsoft:main' into integers
AlexGuteniev Sep 15, 2025
5de9ae1
Merge branch 'microsoft:main' into integers
AlexGuteniev Oct 24, 2025
74815fc
Merge branch 'microsoft:main' into integers
AlexGuteniev Oct 31, 2025
986ba2a
Merge branch 'microsoft:main' into integers
AlexGuteniev Nov 27, 2025
6966faf
Merge branch 'main' into integers
AlexGuteniev Nov 28, 2025
1ffa338
unshare table
AlexGuteniev Nov 28, 2025
f422b45
unrevert merge
AlexGuteniev Nov 28, 2025
b39a620
format
AlexGuteniev Nov 28, 2025
77d37ff
eliminate the tail loop, we need at most one iteration
AlexGuteniev Nov 28, 2025
c1efce4
size
AlexGuteniev Nov 28, 2025
ed71c09
Generate table
AlexGuteniev Nov 28, 2025
9f3e9bc
Let's consistently change 32-bit path
AlexGuteniev Nov 29, 2025
ad8e94f
Merge branch 'microsoft:main' into integers
AlexGuteniev Dec 19, 2025
edc2042
Use a multi-dim array with a constructor.
StephanTLavavej Jan 7, 2026
3708bbf
Transform control flow to be simpler. No additional branches.
StephanTLavavej Jan 7, 2026
6c29273
Use mt19937_64.
StephanTLavavej Jan 7, 2026
333e3f6
Use 20 chars, comment why.
StephanTLavavej Jan 7, 2026
395c8f6
Fix major bug: `_UIntegral_to_buff` takes the END of the buffer.
StephanTLavavej Jan 7, 2026
65138c1
Adjust header inclusions.
StephanTLavavej Jan 7, 2026
33fef86
Benchmark wchar_t.
StephanTLavavej Jan 7, 2026
4b706e5
Add additional correctness tests for every length.
StephanTLavavej Jan 7, 2026
5bf3370
For x86, print 8 digits at a time.
StephanTLavavej Jan 7, 2026
141449f
Refine x86 optimization to avoid final division.
StephanTLavavej Jan 7, 2026
77ab0a7
You want me to give him the CLAMPS, boss?
StephanTLavavej Jan 7, 2026
74064c9
Clamp a while. Clamp forever!
StephanTLavavej Jan 7, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions benchmarks/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -113,6 +113,7 @@ add_benchmark(find_and_count src/find_and_count.cpp)
add_benchmark(find_first_of src/find_first_of.cpp)
add_benchmark(has_single_bit src/has_single_bit.cpp)
add_benchmark(includes src/includes.cpp)
add_benchmark(integer_to_string src/integer_to_string.cpp)
add_benchmark(iota src/iota.cpp)
add_benchmark(is_sorted_until src/is_sorted_until.cpp)
add_benchmark(locale_classic src/locale_classic.cpp)
Expand Down
100 changes: 100 additions & 0 deletions benchmarks/src/integer_to_string.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,100 @@
// Copyright (c) Microsoft Corporation.
// SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception

#include <algorithm>
#include <array>
#include <benchmark/benchmark.h>
#include <cmath>
#include <cstdint>
#include <limits>
#include <random>
#include <string>
#include <type_traits>

using namespace std;

template <class T, double M, double S>
auto generate_array() {
array<T, 2000> a;

mt19937_64 gen;
lognormal_distribution<double> dis(M, S);
auto get_clamped_value = [&] {
for (;;) {
const double dbl = floor(dis(gen));
constexpr auto max_val = static_cast<double>(numeric_limits<T>::max());
if (dbl <= max_val) {
Comment on lines +25 to +26

@MattStephanson Matt Stephanson (MattStephanson) Jan 8, 2026

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

As pointed out by statementreply on Discord, for uint_64 this rounds to $2^{64}$, so to be rigorous you would need to either special-case that or make the test dbl < max_val.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Thanks, I can fix this in a followup.

return static_cast<T>(dbl);
}
}
};
ranges::generate(a, get_clamped_value);

if constexpr (is_signed_v<T>) {
bernoulli_distribution b(0.5);
ranges::for_each(a, [&](T& v) { v *= (b(gen) ? -1 : 1); });
}

return a;
}

template <class CharT, class T, double M, double S>
void internal_integer_to_buff(benchmark::State& state) {
auto a = generate_array<T, M, S>();

CharT buff[20]; // can hold -2^63 and 2^64 - 1
auto buff_end = end(buff);

auto it = a.begin();
for (auto _ : state) {
auto i = *it;
benchmark::DoNotOptimize(i);
auto s = std::_UIntegral_to_buff(buff_end, i);
benchmark::DoNotOptimize(s);

++it;
if (it == a.end()) {
it = a.begin();
}
}
}

template <class T, double M, double S>
void integer_to_string(benchmark::State& state) {
auto a = generate_array<T, M, S>();

auto it = a.begin();
for (auto _ : state) {
auto i = *it;
benchmark::DoNotOptimize(i);
auto s = to_string(i);
Comment thread
StephanTLavavej marked this conversation as resolved.
benchmark::DoNotOptimize(s);

++it;
if (it == a.end()) {
it = a.begin();
}
}
}

BENCHMARK(internal_integer_to_buff<char, uint8_t, 2.5, 1.5>);
BENCHMARK(internal_integer_to_buff<char, uint16_t, 5.0, 3.0>);
BENCHMARK(internal_integer_to_buff<char, uint32_t, 10.0, 6.0>);
BENCHMARK(internal_integer_to_buff<char, uint64_t, 20.0, 12.0>);

BENCHMARK(internal_integer_to_buff<wchar_t, uint8_t, 2.5, 1.5>);
BENCHMARK(internal_integer_to_buff<wchar_t, uint16_t, 5.0, 3.0>);
BENCHMARK(internal_integer_to_buff<wchar_t, uint32_t, 10.0, 6.0>);
BENCHMARK(internal_integer_to_buff<wchar_t, uint64_t, 20.0, 12.0>);

BENCHMARK(integer_to_string<uint8_t, 2.5, 1.5>);
BENCHMARK(integer_to_string<uint16_t, 5.0, 3.0>);
BENCHMARK(integer_to_string<uint32_t, 10.0, 6.0>);
BENCHMARK(integer_to_string<uint64_t, 20.0, 12.0>);

BENCHMARK(integer_to_string<int8_t, 2.5, 1.5>);
BENCHMARK(integer_to_string<int16_t, 5.0, 3.0>);
BENCHMARK(integer_to_string<int32_t, 10.0, 6.0>);
BENCHMARK(integer_to_string<int64_t, 20.0, 12.0>);

BENCHMARK_MAIN();
56 changes: 47 additions & 9 deletions stl/inc/xmemory
Original file line number Diff line number Diff line change
Expand Up @@ -2762,6 +2762,24 @@ namespace ranges {
} // namespace ranges
#endif // _HAS_CXX23

template <class _Elem>
struct _Digit_pair_table {
_Elem _Data[100][2];

constexpr explicit _Digit_pair_table() : _Data{} {
for (int _Idx = 0; _Idx != 100; ++_Idx) {
_Data[_Idx][0] = static_cast<_Elem>('0' + _Idx / 10);
_Data[_Idx][1] = static_cast<_Elem>('0' + _Idx % 10);
}
}

_Digit_pair_table(const _Digit_pair_table&) = delete;
_Digit_pair_table& operator=(const _Digit_pair_table&) = delete;
};

template <class _Elem>
constexpr _Digit_pair_table<_Elem> _Digit_pairs{};

template <class _Elem, class _UTy>
_NODISCARD _Elem* _UIntegral_to_buff(_Elem* _RNext, _UTy _UVal) { // used by both to_string and thread::id output
// format _UVal into buffer *ending at* _RNext
Expand All @@ -2773,23 +2791,43 @@ _NODISCARD _Elem* _UIntegral_to_buff(_Elem* _RNext, _UTy _UVal) { // used by bot

if constexpr (sizeof(_UTy) > 4) { // For 64-bit numbers, work in chunks to avoid 64-bit divisions.
while (_UVal > 0xFFFFFFFFU) {
auto _UVal_chunk = static_cast<unsigned long>(_UVal % 1000000000);
_UVal /= 1000000000;

for (int _Idx = 0; _Idx != 9; ++_Idx) {
*--_RNext = static_cast<_Elem>('0' + _UVal_chunk % 10);
_UVal_chunk /= 10;
auto _UVal_chunk = static_cast<unsigned long>(_UVal % 100000000);
_UVal /= 100000000;

for (int _Idx = 0; _Idx != 3; ++_Idx) {
const unsigned long _UVal_chunk_part = _UVal_chunk % 100;
_UVal_chunk /= 100;
_RNext -= 2;
_CSTD memcpy(_RNext, _Digit_pairs<_Elem>._Data[_UVal_chunk_part], 2 * sizeof(_Elem));
}

_RNext -= 2;
_CSTD memcpy(_RNext, _Digit_pairs<_Elem>._Data[_UVal_chunk], 2 * sizeof(_Elem));
}
}

auto _UVal_trunc = static_cast<unsigned long>(_UVal);
#endif // ^^^ !defined(_WIN64) ^^^

// If we have a single digit, print [0, 9] and return. (This is necessary to correctly handle 0.)
if (_UVal_trunc < 10) {
*--_RNext = static_cast<_Elem>('0' + _UVal_trunc);
return _RNext;
}

// Print one or more pairs of digits.
do {
*--_RNext = static_cast<_Elem>('0' + _UVal_trunc % 10);
_UVal_trunc /= 10;
} while (_UVal_trunc != 0);
const unsigned long _UVal_trunc_part = _UVal_trunc % 100;
_UVal_trunc /= 100;
_RNext -= 2;
_CSTD memcpy(_RNext, _Digit_pairs<_Elem>._Data[_UVal_trunc_part], 2 * sizeof(_Elem));
} while (_UVal_trunc >= 10);

// If we have an unpaired digit, print it. For example, 1729 is printed as 17 29, and 19937 is printed as 1 99 37.
if (_UVal_trunc != 0) {
*--_RNext = static_cast<_Elem>('0' + _UVal_trunc);
}

return _RNext;
}
_STD_END
Expand Down
28 changes: 28 additions & 0 deletions tests/std/tests/Dev11_0835323_to_string/test.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -144,6 +144,34 @@ int main() {
L"2304583236903222948165808559332123348274797826204144723168738177180919299881250404026184124858368.000000");
assert(to_wstring(numeric_limits<long double>::infinity()) == L"inf");

// Exercise the logic for printing digit pairs, and the optimization for printing 64-bit values on 32-bit targets.
assert(to_string(0ULL) == "0");
assert(to_string(1ULL) == "1");
assert(to_string(12ULL) == "12");
assert(to_string(123ULL) == "123");
assert(to_string(1234ULL) == "1234");
assert(to_string(12345ULL) == "12345");
assert(to_string(123456ULL) == "123456");
assert(to_string(1234567ULL) == "1234567");
assert(to_string(12345678ULL) == "12345678");
assert(to_string(123456789ULL) == "123456789");
assert(to_string(1234567890ULL) == "1234567890");
assert(to_string(4294967295ULL) == "4294967295"); // 2^32-1
assert(to_string(4294967296ULL) == "4294967296");
assert(to_string(12345678901ULL) == "12345678901");
assert(to_string(123456789012ULL) == "123456789012");
assert(to_string(1234567890123ULL) == "1234567890123");
assert(to_string(12345678901234ULL) == "12345678901234");
assert(to_string(123456789012345ULL) == "123456789012345");
assert(to_string(1234567890123456ULL) == "1234567890123456");
assert(to_string(12345678901234567ULL) == "12345678901234567");
assert(to_string(123456789012345678ULL) == "123456789012345678");
assert(to_string(429496729599999999ULL) == "429496729599999999"); // 2^32-1 followed by 8 digits
assert(to_string(429496729600000000ULL) == "429496729600000000");
assert(to_string(1234567890123456789ULL) == "1234567890123456789");
assert(to_string(4294967295999999999ULL) == "4294967295999999999"); // 2^32-1 followed by 9 digits
assert(to_string(4294967296000000000ULL) == "4294967296000000000");
assert(to_string(12345678901234567890ULL) == "12345678901234567890");

// Also test DevDiv-875295 "<string>: std::stof returns 1.#INF instead of throwing out_of_range [libcxx]".

Expand Down