From a6ab196ad5ca170f93cfa32c7c53402d53f14d15 Mon Sep 17 00:00:00 2001 From: Niels Lohmann Date: Tue, 27 Oct 2026 21:39:40 +0100 Subject: [PATCH] Write integers of 17 to 20 digits eight digits at a time in dump() #5634 made dump() write integers below 10^16 eight digits at a time; larger ones still took the two-digits-per-division loop and count_digits(). Random 64-bit integers almost all have 17 to 20 digits (99.9 % of signed_ints.json and unsigned_ints.json), so Dump/signed_ints and Dump/unsigned_ints gained only 1.4x over 3.12.0, against 2.3x for small integers. A 64-bit magnitude is now written as its first one to four digits and two groups of eight. Wider magnitudes (__int128 and class types, #5805) keep the loop: number types of at most 64 bits are selected by type, so no comparison is reported as always true; for others the value is compared. The sign handling of #1447/#5815 (remove_sign() into number_magnitude_t) is unchanged. Signed-off-by: Niels Lohmann --- include/nlohmann/detail/output/serializer.hpp | 34 ++++++++++++++ single_include/nlohmann/json.hpp | 34 ++++++++++++++ tests/src/unit-serialization.cpp | 46 +++++++++++++++++++ 3 files changed, 114 insertions(+) diff --git a/include/nlohmann/detail/output/serializer.hpp b/include/nlohmann/detail/output/serializer.hpp index 598fda98a..4656dbdf1 100644 --- a/include/nlohmann/detail/output/serializer.hpp +++ b/include/nlohmann/detail/output/serializer.hpp @@ -1977,6 +1977,23 @@ class serializer return; } + // 17 to 20 digits: the first one to four, then twice eight + if (magnitude_fits_64_bits(abs_value, std::integral_constant())) + { + const auto value = static_cast(abs_value); + const std::uint64_t top = value / 10000000000000000u; // 1 to 1844 + const std::uint64_t low16 = value - (top * 10000000000000000u); + const std::uint64_t middle = low16 / 100000000u; + const std::uint64_t first = dtoa_impl::eight_digit_bytes(top); + const auto leading = static_cast(count_leading_zeros(first) / 8); // (first is not 0) + char* const p = buffer_ptr + n_chars; + dtoa_impl::store_msb_first(p, (first << (8 * leading)) + 0x3030303030303030u); + dtoa_impl::store_msb_first(p + 8 - leading, dtoa_impl::eight_digit_bytes(middle) + 0x3030303030303030u); + dtoa_impl::store_msb_first(p + 16 - leading, dtoa_impl::eight_digit_bytes(low16 - (middle * 100000000u)) + 0x3030303030303030u); + write_buffer_pos += n_chars + 24 - leading; + return; + } + n_chars += count_digits(abs_value); // spare 1 byte for '\0' @@ -2122,6 +2139,23 @@ class serializer } } + /// whether every number_magnitude_t fits 64 bits + static constexpr bool magnitude_has_at_most_64_bits = std::numeric_limits::is_specialized + && std::numeric_limits::digits <= 64; + + /// whether @a x fits 64 bits: always if number_magnitude_t has at most + /// 64 bits (selected by type, as the comparison would be reported as + /// always true), else compared + static bool magnitude_fits_64_bits(const number_magnitude_t& /*unused*/, std::true_type /*unused*/) noexcept + { + return true; + } + + static bool magnitude_fits_64_bits(const number_magnitude_t& x, std::false_type /*unused*/) noexcept + { + return x <= 18446744073709551615u; + } + /* * Helper function for dump_integer * diff --git a/single_include/nlohmann/json.hpp b/single_include/nlohmann/json.hpp index 98d6045fa..ce6dd6e8e 100644 --- a/single_include/nlohmann/json.hpp +++ b/single_include/nlohmann/json.hpp @@ -36205,6 +36205,23 @@ class serializer return; } + // 17 to 20 digits: the first one to four, then twice eight + if (magnitude_fits_64_bits(abs_value, std::integral_constant())) + { + const auto value = static_cast(abs_value); + const std::uint64_t top = value / 10000000000000000u; // 1 to 1844 + const std::uint64_t low16 = value - (top * 10000000000000000u); + const std::uint64_t middle = low16 / 100000000u; + const std::uint64_t first = dtoa_impl::eight_digit_bytes(top); + const auto leading = static_cast(count_leading_zeros(first) / 8); // (first is not 0) + char* const p = buffer_ptr + n_chars; + dtoa_impl::store_msb_first(p, (first << (8 * leading)) + 0x3030303030303030u); + dtoa_impl::store_msb_first(p + 8 - leading, dtoa_impl::eight_digit_bytes(middle) + 0x3030303030303030u); + dtoa_impl::store_msb_first(p + 16 - leading, dtoa_impl::eight_digit_bytes(low16 - (middle * 100000000u)) + 0x3030303030303030u); + write_buffer_pos += n_chars + 24 - leading; + return; + } + n_chars += count_digits(abs_value); // spare 1 byte for '\0' @@ -36350,6 +36367,23 @@ class serializer } } + /// whether every number_magnitude_t fits 64 bits + static constexpr bool magnitude_has_at_most_64_bits = std::numeric_limits::is_specialized + && std::numeric_limits::digits <= 64; + + /// whether @a x fits 64 bits: always if number_magnitude_t has at most + /// 64 bits (selected by type, as the comparison would be reported as + /// always true), else compared + static bool magnitude_fits_64_bits(const number_magnitude_t& /*unused*/, std::true_type /*unused*/) noexcept + { + return true; + } + + static bool magnitude_fits_64_bits(const number_magnitude_t& x, std::false_type /*unused*/) noexcept + { + return x <= 18446744073709551615u; + } + /* * Helper function for dump_integer * diff --git a/tests/src/unit-serialization.cpp b/tests/src/unit-serialization.cpp index 1173b851b..e2a41f272 100644 --- a/tests/src/unit-serialization.cpp +++ b/tests/src/unit-serialization.cpp @@ -23,6 +23,7 @@ using nlohmann::json; #include #include #include +#include #include #include @@ -248,6 +249,51 @@ TEST_CASE_TEMPLATE("serialization for extreme integer values", T, int32_t, uint3 } } +TEST_CASE("integers of every digit count are written like std::to_string") +{ + // dump_integer() writes up to 16 digits in two groups of eight and 17 to + // 20 digits as one to four digits and two groups of eight + std::vector values; + std::uint64_t power = 1; + for (int digits = 1; digits <= 20; ++digits) + { + values.push_back(power); // 1 followed by zeros + values.push_back(power + 1); + values.push_back((power * 2) - 1); + if (digits < 20) + { + values.push_back((power * 10) - 1); // all 9s + values.push_back((power * 10) - power); // 9 followed by zeros + power *= 10; + } + } + values.push_back((std::numeric_limits::max)()); + values.push_back(static_cast((std::numeric_limits::max)())); + values.push_back(static_cast((std::numeric_limits::max)()) + 1); + std::uint64_t state = 0x9E3779B97F4A7C15u; + for (int i = 0; i < 20000; ++i) + { + state = (state * 6364136223846793005u) + 1442695040888963407u; + values.push_back(state >> (state % 61)); // every bit length + } + + for (const auto v : values) + { + CAPTURE(v) + CHECK(json(v).dump() == std::to_string(v)); + if (v <= static_cast((std::numeric_limits::max)())) + { + const auto s = static_cast(v); + CHECK(json(s).dump() == std::to_string(s)); + CHECK(json(-s).dump() == std::to_string(-s)); + } + } + CHECK(json((std::numeric_limits::min)()).dump() == "-9223372036854775808"); + + // in a container, so the digits are followed by other output + CHECK(json::array({std::uint64_t{18446744073709551615u}, std::int64_t{-1234567890123456789}, 1}).dump() == "[18446744073709551615,-1234567890123456789,1]"); +} + TEST_CASE("dump with binary values") { auto binary = json::binary({1, 2, 3, 4});