mirror of
https://github.com/nlohmann/json.git
synced 2026-08-02 07:32:15 +00:00
Add a contiguous fast path for scanning numbers
scan_number() reads a number one character at a time through the input adapter (get()) and appends each byte to token_buffer (add()) before converting. For contiguous input, the per-character get()/add() overhead dominates: it is roughly two thirds of the time spent on number-heavy parsing, far more than the value conversion itself. Add scan_number_bulk_contiguous(), which parses the whole number token straight from the input buffer: it validates and classifies the extent with the same grammar as scan_number()'s state machine, materializes token_buffer in one copy (substituting the locale decimal point exactly as scan_number() does), advances the adapter, and reuses the shared convert_number() tail. On anything it does not recognize as a well-formed number it makes no state change and returns token_type::uninitialized, so the caller falls back to scan_number(), which then produces the exact diagnostic. Errors and their positions are therefore unchanged. The conversion tail is factored out of scan_number() into convert_number() so both scanners share it; the fast path is selected by tag dispatch on the existing bulk_scan capability, so streaming/wide/user adapters are unaffected. Measured on pointer input, g++ 13 -O3: - integers: parse +65%, accept +98% - floats: parse +39%, accept +70% Verified: 2,000,000 randomized number documents (including overflow-range integers, long digit strings and %.17g doubles) parse identically via the contiguous path and the streaming byte path, matching value, type and round-trip text; the locale suite and existing parser/lexer/conversions/ deserialization tests pass; a new "lexer number fast path" test checks contiguous-vs-streaming parity, token classification, and that malformed numbers are rejected identically. Pure C++11, no intrinsics. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01AXcDtEma2PjxgmPS9cQGzA Signed-off-by: Niels Lohmann <mail@nlohmann.me>
This commit is contained in:
co-authored by
Claude Opus 4.8
parent
190f4b6a7b
commit
84939c11be
@@ -1769,12 +1769,26 @@ scan_number_done:
|
||||
// we are done scanning a number)
|
||||
unget();
|
||||
|
||||
return convert_number(number_type);
|
||||
}
|
||||
|
||||
/*!
|
||||
@brief convert the number text in token_buffer to its value and token type
|
||||
|
||||
The digit sequence in token_buffer has already been validated (by the
|
||||
scan_number() state machine or by the contiguous fast path) and holds the
|
||||
locale decimal point in place of '.'. Integers are parsed first and fall
|
||||
back to floating point on overflow. This is shared so both scanners produce
|
||||
identical results.
|
||||
*/
|
||||
token_type convert_number(token_type number_type)
|
||||
{
|
||||
const char* const num_begin = token_buffer.data();
|
||||
const char* const num_end = num_begin + token_buffer.size();
|
||||
|
||||
// try to parse integers first and fall back to floats; the digit
|
||||
// sequence has already been validated by the state machine above, so
|
||||
// a dedicated parser can avoid the locale/errno overhead of strtoull
|
||||
// sequence has already been validated, so a dedicated parser can avoid
|
||||
// the locale/errno overhead of strtoull
|
||||
if (number_type == token_type::value_unsigned)
|
||||
{
|
||||
if (parse_integer_unsigned(num_begin, num_end, value_unsigned))
|
||||
@@ -1807,6 +1821,128 @@ scan_number_done:
|
||||
return token_type::value_float;
|
||||
}
|
||||
|
||||
/*!
|
||||
@brief contiguous fast path for scanning a number
|
||||
|
||||
Parses the whole number token straight from the input buffer, avoiding the
|
||||
per-character get()/add() of scan_number(). On success it fills token_buffer
|
||||
(with the locale decimal point substituted, as scan_number() does) and
|
||||
returns the token type. On anything it does not fully recognize as a
|
||||
well-formed number it makes no state change and returns
|
||||
token_type::uninitialized, so the caller falls back to scan_number(), which
|
||||
then produces the exact diagnostic. @a current is the first digit or the
|
||||
leading minus (already read); the remaining bytes are taken from the adapter.
|
||||
*/
|
||||
token_type scan_number_bulk_contiguous()
|
||||
{
|
||||
// a pending unget offsets the buffer position from current; fall back
|
||||
if (next_unget)
|
||||
{
|
||||
return token_type::uninitialized;
|
||||
}
|
||||
const std::size_t rem = ia.bulk_remaining();
|
||||
if (rem == 0)
|
||||
{
|
||||
// the first digit is the last input byte; let scan_number() finish
|
||||
return token_type::uninitialized;
|
||||
}
|
||||
// the byte before the next unread one is current (contiguous input)
|
||||
const char* const data = reinterpret_cast<const char*>(ia.bulk_data()) - 1;
|
||||
const std::size_t avail = rem + 1;
|
||||
|
||||
// validate + classify the number extent (mirrors scan_number()'s grammar)
|
||||
std::size_t i = 0;
|
||||
std::size_t dot_index = std::string::npos;
|
||||
token_type number_type = token_type::value_unsigned;
|
||||
if (data[0] == '-')
|
||||
{
|
||||
number_type = token_type::value_integer;
|
||||
i = 1;
|
||||
if (i >= avail)
|
||||
{
|
||||
return token_type::uninitialized;
|
||||
}
|
||||
}
|
||||
if (data[i] == '0')
|
||||
{
|
||||
++i;
|
||||
}
|
||||
else if (data[i] >= '1' && data[i] <= '9')
|
||||
{
|
||||
++i;
|
||||
while (i < avail && data[i] >= '0' && data[i] <= '9')
|
||||
{
|
||||
++i;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
return token_type::uninitialized;
|
||||
}
|
||||
if (i < avail && data[i] == '.')
|
||||
{
|
||||
number_type = token_type::value_float;
|
||||
dot_index = i;
|
||||
++i;
|
||||
if (i >= avail || !(data[i] >= '0' && data[i] <= '9'))
|
||||
{
|
||||
return token_type::uninitialized;
|
||||
}
|
||||
while (i < avail && data[i] >= '0' && data[i] <= '9')
|
||||
{
|
||||
++i;
|
||||
}
|
||||
}
|
||||
if (i < avail && (data[i] == 'e' || data[i] == 'E'))
|
||||
{
|
||||
number_type = token_type::value_float;
|
||||
++i;
|
||||
if (i < avail && (data[i] == '+' || data[i] == '-'))
|
||||
{
|
||||
++i;
|
||||
}
|
||||
if (i >= avail || !(data[i] >= '0' && data[i] <= '9'))
|
||||
{
|
||||
return token_type::uninitialized;
|
||||
}
|
||||
while (i < avail && data[i] >= '0' && data[i] <= '9')
|
||||
{
|
||||
++i;
|
||||
}
|
||||
}
|
||||
const std::size_t len = i;
|
||||
|
||||
// materialize the token exactly as scan_number() would, substituting the
|
||||
// locale decimal point so convert_number()'s strtof fallback stays valid
|
||||
reset();
|
||||
token_buffer.assign(data, len);
|
||||
if (dot_index != std::string::npos)
|
||||
{
|
||||
token_buffer[dot_index] = static_cast<typename string_t::value_type>(decimal_point_char);
|
||||
decimal_point_position = dot_index;
|
||||
}
|
||||
|
||||
// consume the remaining bytes of the number (current was already read)
|
||||
ia.bulk_skip(len - 1);
|
||||
position.chars_read_total += (len - 1);
|
||||
position.chars_read_current_line += (len - 1);
|
||||
|
||||
return convert_number(number_type);
|
||||
}
|
||||
|
||||
/// contiguous input: try the number fast path, else the byte-path scanner
|
||||
token_type scan_number_dispatch(std::true_type /*bulk*/)
|
||||
{
|
||||
const token_type t = scan_number_bulk_contiguous();
|
||||
return (t != token_type::uninitialized) ? t : scan_number();
|
||||
}
|
||||
|
||||
/// streaming input: always use the byte-path scanner
|
||||
token_type scan_number_dispatch(std::false_type /*bulk*/)
|
||||
{
|
||||
return scan_number();
|
||||
}
|
||||
|
||||
/*!
|
||||
@param[in] literal_text the literal text to expect
|
||||
@param[in] length the length of the passed literal text
|
||||
@@ -2161,7 +2297,7 @@ scan_number_done:
|
||||
case '7':
|
||||
case '8':
|
||||
case '9':
|
||||
return scan_number();
|
||||
return scan_number_dispatch(std::integral_constant<bool, bulk_scan> {});
|
||||
|
||||
// end of input (the null byte is needed when parsing from
|
||||
// string literals)
|
||||
|
||||
Reference in New Issue
Block a user