accept(): skip digits without computing their value

accept() needs the value of a number only for a float whose exponent may
reach the largest value. skip_digits() finds the end of the digits without
the multiply-adds of accumulate_digits(), and once an 8-byte word fails the
digit test, the remaining loop needs no bounds check: a non-digit is in
that word. The value is computed afterwards for the rare floats that need
it.

accept() of canada.json: -16 % on Apple M1, -22 % with GCC 15 and -21 %
with Clang 21 on x86-64; files with many numbers -9 to -15 %.

Signed-off-by: Niels Lohmann <mail@nlohmann.me>
This commit is contained in:
Niels Lohmann committed 2026-11-06 12:16:38 +01:00
1 parent 2eb9aae537
commit 788b12213b
4 files changed
+103 -4

No files matched your search

@@ -646,7 +646,9 @@ done:
}
else if (i < avail && data[i] >= '1' && data[i] <= '9')
{
i = accumulate_digits(data, i, avail, w);
// accept() needs the value only for a float with a large exponent,
// which computes it below; its handler ignores the values
i = keep_values() ? accumulate_digits(data, i, avail, w) : skip_digits(data, i, avail);
}
else
{
@@ -664,7 +666,7 @@ done:
return false;
}
const std::size_t fraction_begin = i;
i = accumulate_digits(data, i, avail, w);
i = keep_values() ? accumulate_digits(data, i, avail, w) : skip_digits(data, i, avail);
fraction_digits = i - fraction_begin;
digit_count += fraction_digits;
}
@@ -753,6 +755,16 @@ done:
{
return report(sax->number_float(value, number_text(data, data + i)));
}
if (!keep_values())
{
// the digits were skipped: their value now
s.w = 0;
const std::size_t integer_end = accumulate_digits(data, integer_begin, avail, s.w);
if (dot_index != std::string::npos)
{
static_cast<void>(accumulate_digits(data, integer_end + 1, avail, s.w));
}
}
value = decimal_to_float<number_float_t>(s);
}
else
@@ -316,6 +316,30 @@ JSON_HEDLEY_ALWAYS_INLINE std::size_t accumulate_digits(const char* data, std::s
return i;
}
/// the index of the first byte at or after @a i that is not a digit: the
/// digits of accumulate_digits() without their value
JSON_HEDLEY_ALWAYS_INLINE std::size_t skip_digits(const char* data, std::size_t i, std::size_t avail) noexcept
{
while (avail - i >= 8)
{
if (!is_eight_digits(read_eight_bytes(data + i)))
{
// a byte of the word is not a digit: no bounds checks before it
while (static_cast<unsigned char>(data[i] - '0') <= 9)
{
++i;
}
return i;
}
i += 8;
}
while (i < avail && data[i] >= '0' && data[i] <= '9')
{
++i;
}
return i;
}
/// whether [first, last) contains a digit other than '0'; a plain loop, as
/// GCC calls std::any_of out of line even for the usual empty range
JSON_HEDLEY_ALWAYS_INLINE bool has_nonzero_digit(const char* first, const char* last) noexcept
+38 -2
View File
@@ -11257,6 +11257,30 @@ JSON_HEDLEY_ALWAYS_INLINE std::size_t accumulate_digits(const char* data, std::s
return i;
}
/// the index of the first byte at or after @a i that is not a digit: the
/// digits of accumulate_digits() without their value
JSON_HEDLEY_ALWAYS_INLINE std::size_t skip_digits(const char* data, std::size_t i, std::size_t avail) noexcept
{
while (avail - i >= 8)
{
if (!is_eight_digits(read_eight_bytes(data + i)))
{
// a byte of the word is not a digit: no bounds checks before it
while (static_cast<unsigned char>(data[i] - '0') <= 9)
{
++i;
}
return i;
}
i += 8;
}
while (i < avail && data[i] >= '0' && data[i] <= '9')
{
++i;
}
return i;
}
/// whether [first, last) contains a digit other than '0'; a plain loop, as
/// GCC calls std::any_of out of line even for the usual empty range
JSON_HEDLEY_ALWAYS_INLINE bool has_nonzero_digit(const char* first, const char* last) noexcept
@@ -24488,7 +24512,9 @@ done:
}
else if (i < avail && data[i] >= '1' && data[i] <= '9')
{
i = accumulate_digits(data, i, avail, w);
// accept() needs the value only for a float with a large exponent,
// which computes it below; its handler ignores the values
i = keep_values() ? accumulate_digits(data, i, avail, w) : skip_digits(data, i, avail);
}
else
{
@@ -24506,7 +24532,7 @@ done:
return false;
}
const std::size_t fraction_begin = i;
i = accumulate_digits(data, i, avail, w);
i = keep_values() ? accumulate_digits(data, i, avail, w) : skip_digits(data, i, avail);
fraction_digits = i - fraction_begin;
digit_count += fraction_digits;
}
@@ -24595,6 +24621,16 @@ done:
{
return report(sax->number_float(value, number_text(data, data + i)));
}
if (!keep_values())
{
// the digits were skipped: their value now
s.w = 0;
const std::size_t integer_end = accumulate_digits(data, integer_begin, avail, s.w);
if (dot_index != std::string::npos)
{
static_cast<void>(accumulate_digits(data, integer_end + 1, avail, s.w));
}
}
value = decimal_to_float<number_float_t>(s);
}
else
+27
View File
@@ -518,6 +518,33 @@ TEST_CASE("contiguous parser")
}
}
SECTION("accept() of floats near the largest value")
{
// accept() skips digits without their value and computes it only for
// a float whose exponent may reach the largest value
for (const char* number :
{"17976931348623158e292", "17976931348623159e292", "1797693134862315.8e293", "1797693134862315.9e293",
"0.17976931348623158e309", "0.17976931348623159e309", "-17976931348623159e292", "9999999999999999999e289"
})
{
check_both(number);
check_both(std::string("[") + number + "]");
}
CHECK(json::accept("17976931348623158e292"));
CHECK_FALSE(json::accept("17976931348623159e292"));
CHECK(json::accept("[1797693134862315.8e293]"));
CHECK_FALSE(json::accept("[1797693134862315.9e293]"));
CHECK(json::accept("0.17976931348623158e309"));
CHECK_FALSE(json::accept("0.17976931348623159e309"));
CHECK_FALSE(json::accept("-17976931348623159e292"));
using narrow_json = nlohmann::basic_json<std::map, std::vector, std::string, bool, std::int32_t, std::uint32_t, float>;
check_same<narrow_json>("34028235e31");
check_same<narrow_json>("34028236e31");
CHECK(narrow_json::accept("34028235e31"));
CHECK_FALSE(narrow_json::accept("34028236e31"));
}
SECTION("callbacks, comments and trailing commas take the library parser")
{
// (the results are those of the options; the contiguous parser does not support them)