Compare commits

...
Author SHA1 Message Date
Niels Lohmann 92dc52162e Merge branch 'perf/lexer-simd-strings' into perf/lexer-dispatch
Signed-off-by: Niels Lohmann <mail@nlohmann.me>
2026-10-16 08:21:10 +02:00
Niels Lohmann 9d15d23131 Merge branch 'perf/lexer-simd-strings' into perf/lexer-dispatch
Signed-off-by: Niels Lohmann <mail@nlohmann.me>
2026-10-15 22:47:09 +02:00
Niels Lohmann 1b84568e5e Merge branch 'perf/lexer-simd-strings' into perf/lexer-dispatch
Signed-off-by: Niels Lohmann <mail@nlohmann.me>
2026-10-15 20:23:30 +02:00
Niels Lohmann 57424ab0b5 Match literals without a stack array and keep parser states in bytes
scan_after_whitespace() built the expected text of true, false, and null in
local std::array objects. With -fstack-protector-strong (the default of
Ubuntu's GCC, among others), a local array gives the function a stack canary
check, which then ran on every token. scan_literal() now takes the text
after the first character as a string literal, with its length as a template
parameter; for contiguous input it compares the rest at once and falls back
to the byte loop on a mismatch, so errors are reported at the same
character. It is out of line, which keeps the token dispatch small.

The parser kept the nesting of arrays and objects in a std::vector<bool>,
whose push_back() GCC calls out of line; it is a std::vector<std::uint8_t>
now.

Signed-off-by: Niels Lohmann <mail@nlohmann.me>
2026-10-15 18:37:05 +02:00
5 changed files with 152 additions and 51 deletions

No files matched your search

+40 -20
View File
@@ -12,6 +12,7 @@
#include <cstddef> // size_t #include <cstddef> // size_t
#include <cstdint> // uint32_t #include <cstdint> // uint32_t
#include <cstdio> // snprintf #include <cstdio> // snprintf
#include <cstring> // memcmp
#include <initializer_list> // initializer_list #include <initializer_list> // initializer_list
#include <string> // char_traits, string #include <string> // char_traits, string
#include <utility> // move #include <utility> // move
@@ -1854,18 +1855,23 @@ scan_number_done:
} }
/*! /*!
@param[in] literal_text the literal text to expect @param[in] rest the literal text after its first character, which
@param[in] length the length of the passed literal text is current (its length is a template parameter, so
that the comparison of the contiguous fast path is
a fixed-size load, not a call of memcmp)
@param[in] return_type the token type to return on success @param[in] return_type the token type to return on success
*/ */
JSON_HEDLEY_NON_NULL(2) template<std::size_t N>
token_type scan_literal(const char_type* literal_text, const std::size_t length, JSON_HEDLEY_NEVER_INLINE token_type scan_literal(const char (&rest)[N], token_type return_type)
token_type return_type)
{ {
JSON_ASSERT(char_traits<char_type>::to_char_type(current) == literal_text[0]); constexpr std::size_t length = N - 1;
for (std::size_t i = 1; i < length; ++i) if (scan_literal_bulk(rest, length, std::integral_constant<bool, bulk_scan> {}))
{ {
if (JSON_HEDLEY_UNLIKELY(char_traits<char_type>::to_char_type(get()) != literal_text[i])) return return_type;
}
for (std::size_t i = 0; i < length; ++i)
{
if (JSON_HEDLEY_UNLIKELY(char_traits<char_type>::to_char_type(get()) != static_cast<char_type>(rest[i])))
{ {
error_message = "invalid literal"; error_message = "invalid literal";
return token_type::parse_error; return token_type::parse_error;
@@ -1874,6 +1880,27 @@ scan_number_done:
return return_type; return return_type;
} }
/// contiguous input: compare the rest of a literal at once; false (and
/// nothing consumed) if it does not match, so that the byte path reports
/// the error at the right character
JSON_HEDLEY_ALWAYS_INLINE bool scan_literal_bulk(const char* rest, const std::size_t length, std::true_type /*bulk*/)
{
if (next_unget || ia.bulk_remaining() < length
|| std::memcmp(ia.bulk_data(), rest, length) != 0)
{
return false;
}
bulk_consume(reinterpret_cast<const char*>(ia.bulk_data()), length);
current = static_cast<char_int_type>(static_cast<unsigned char>(rest[length - 1]));
return true;
}
/// streaming input: no bulk fast path
bool scan_literal_bulk(const char* /*rest*/, const std::size_t /*length*/, std::false_type /*bulk*/) const noexcept
{
return false;
}
///////////////////// /////////////////////
// input management // input management
///////////////////// /////////////////////
@@ -2499,21 +2526,14 @@ scan_number_done:
return token_type::value_separator; return token_type::value_separator;
// literals // literals
// (the text after the first character; no local arrays, which
// would give this function a stack canary on every token)
case 't': case 't':
{ return scan_literal("rue", token_type::literal_true);
std::array<char_type, 4> true_literal = {{static_cast<char_type>('t'), static_cast<char_type>('r'), static_cast<char_type>('u'), static_cast<char_type>('e')}};
return scan_literal(true_literal.data(), true_literal.size(), token_type::literal_true);
}
case 'f': case 'f':
{ return scan_literal("alse", token_type::literal_false);
std::array<char_type, 5> false_literal = {{static_cast<char_type>('f'), static_cast<char_type>('a'), static_cast<char_type>('l'), static_cast<char_type>('s'), static_cast<char_type>('e')}};
return scan_literal(false_literal.data(), false_literal.size(), token_type::literal_false);
}
case 'n': case 'n':
{ return scan_literal("ull", token_type::literal_null);
std::array<char_type, 4> null_literal = {{static_cast<char_type>('n'), static_cast<char_type>('u'), static_cast<char_type>('l'), static_cast<char_type>('l')}};
return scan_literal(null_literal.data(), null_literal.size(), token_type::literal_null);
}
// string // string
case '\"': case '\"':
+6 -5
View File
@@ -244,8 +244,9 @@ class parser
bool sax_parse_internal(SAX* sax) bool sax_parse_internal(SAX* sax)
{ {
// stack to remember the hierarchy of structured values we are parsing // stack to remember the hierarchy of structured values we are parsing
// true = array; false = object // true = array; false = object (bytes, not std::vector<bool>, whose
std::vector<bool> states; // push_back() is a call with GCC)
std::vector<std::uint8_t> states;
// value to avoid a goto (see comment where set to true) // value to avoid a goto (see comment where set to true)
bool skip_to_state_evaluation = false; bool skip_to_state_evaluation = false;
@@ -290,7 +291,7 @@ class parser
} }
// remember we are now inside an object // remember we are now inside an object
states.push_back(false); states.push_back(0);
// parse values // parse values
get_token(); get_token();
@@ -315,7 +316,7 @@ class parser
} }
// remember we are now inside an array // remember we are now inside an array
states.push_back(true); states.push_back(1);
// parse values (no need to call get_token) // parse values (no need to call get_token)
continue; continue;
@@ -432,7 +433,7 @@ class parser
return true; return true;
} }
if (states.back()) // array if (states.back() != 0) // array
{ {
// comma -> next value // comma -> next value
// or end of array (ignore_trailing_commas = true) // or end of array (ignore_trailing_commas = true)
+46 -25
View File
@@ -9519,6 +9519,7 @@ NLOHMANN_JSON_NAMESPACE_END
#include <cstddef> // size_t #include <cstddef> // size_t
#include <cstdint> // uint32_t #include <cstdint> // uint32_t
#include <cstdio> // snprintf #include <cstdio> // snprintf
#include <cstring> // memcmp
#include <initializer_list> // initializer_list #include <initializer_list> // initializer_list
#include <string> // char_traits, string #include <string> // char_traits, string
#include <utility> // move #include <utility> // move
@@ -13802,18 +13803,23 @@ scan_number_done:
} }
/*! /*!
@param[in] literal_text the literal text to expect @param[in] rest the literal text after its first character, which
@param[in] length the length of the passed literal text is current (its length is a template parameter, so
that the comparison of the contiguous fast path is
a fixed-size load, not a call of memcmp)
@param[in] return_type the token type to return on success @param[in] return_type the token type to return on success
*/ */
JSON_HEDLEY_NON_NULL(2) template<std::size_t N>
token_type scan_literal(const char_type* literal_text, const std::size_t length, JSON_HEDLEY_NEVER_INLINE token_type scan_literal(const char (&rest)[N], token_type return_type)
token_type return_type)
{ {
JSON_ASSERT(char_traits<char_type>::to_char_type(current) == literal_text[0]); constexpr std::size_t length = N - 1;
for (std::size_t i = 1; i < length; ++i) if (scan_literal_bulk(rest, length, std::integral_constant<bool, bulk_scan> {}))
{ {
if (JSON_HEDLEY_UNLIKELY(char_traits<char_type>::to_char_type(get()) != literal_text[i])) return return_type;
}
for (std::size_t i = 0; i < length; ++i)
{
if (JSON_HEDLEY_UNLIKELY(char_traits<char_type>::to_char_type(get()) != static_cast<char_type>(rest[i])))
{ {
error_message = "invalid literal"; error_message = "invalid literal";
return token_type::parse_error; return token_type::parse_error;
@@ -13822,6 +13828,27 @@ scan_number_done:
return return_type; return return_type;
} }
/// contiguous input: compare the rest of a literal at once; false (and
/// nothing consumed) if it does not match, so that the byte path reports
/// the error at the right character
JSON_HEDLEY_ALWAYS_INLINE bool scan_literal_bulk(const char* rest, const std::size_t length, std::true_type /*bulk*/)
{
if (next_unget || ia.bulk_remaining() < length
|| std::memcmp(ia.bulk_data(), rest, length) != 0)
{
return false;
}
bulk_consume(reinterpret_cast<const char*>(ia.bulk_data()), length);
current = static_cast<char_int_type>(static_cast<unsigned char>(rest[length - 1]));
return true;
}
/// streaming input: no bulk fast path
bool scan_literal_bulk(const char* /*rest*/, const std::size_t /*length*/, std::false_type /*bulk*/) const noexcept
{
return false;
}
///////////////////// /////////////////////
// input management // input management
///////////////////// /////////////////////
@@ -14447,21 +14474,14 @@ scan_number_done:
return token_type::value_separator; return token_type::value_separator;
// literals // literals
// (the text after the first character; no local arrays, which
// would give this function a stack canary on every token)
case 't': case 't':
{ return scan_literal("rue", token_type::literal_true);
std::array<char_type, 4> true_literal = {{static_cast<char_type>('t'), static_cast<char_type>('r'), static_cast<char_type>('u'), static_cast<char_type>('e')}};
return scan_literal(true_literal.data(), true_literal.size(), token_type::literal_true);
}
case 'f': case 'f':
{ return scan_literal("alse", token_type::literal_false);
std::array<char_type, 5> false_literal = {{static_cast<char_type>('f'), static_cast<char_type>('a'), static_cast<char_type>('l'), static_cast<char_type>('s'), static_cast<char_type>('e')}};
return scan_literal(false_literal.data(), false_literal.size(), token_type::literal_false);
}
case 'n': case 'n':
{ return scan_literal("ull", token_type::literal_null);
std::array<char_type, 4> null_literal = {{static_cast<char_type>('n'), static_cast<char_type>('u'), static_cast<char_type>('l'), static_cast<char_type>('l')}};
return scan_literal(null_literal.data(), null_literal.size(), token_type::literal_null);
}
// string // string
case '\"': case '\"':
@@ -20575,8 +20595,9 @@ class parser
bool sax_parse_internal(SAX* sax) bool sax_parse_internal(SAX* sax)
{ {
// stack to remember the hierarchy of structured values we are parsing // stack to remember the hierarchy of structured values we are parsing
// true = array; false = object // true = array; false = object (bytes, not std::vector<bool>, whose
std::vector<bool> states; // push_back() is a call with GCC)
std::vector<std::uint8_t> states;
// value to avoid a goto (see comment where set to true) // value to avoid a goto (see comment where set to true)
bool skip_to_state_evaluation = false; bool skip_to_state_evaluation = false;
@@ -20621,7 +20642,7 @@ class parser
} }
// remember we are now inside an object // remember we are now inside an object
states.push_back(false); states.push_back(0);
// parse values // parse values
get_token(); get_token();
@@ -20646,7 +20667,7 @@ class parser
} }
// remember we are now inside an array // remember we are now inside an array
states.push_back(true); states.push_back(1);
// parse values (no need to call get_token) // parse values (no need to call get_token)
continue; continue;
@@ -20763,7 +20784,7 @@ class parser
return true; return true;
} }
if (states.back()) // array if (states.back() != 0) // array
{ {
// comma -> next value // comma -> next value
// or end of array (ignore_trailing_commas = true) // or end of array (ignore_trailing_commas = true)
+58
View File
@@ -13,6 +13,7 @@
using nlohmann::json; using nlohmann::json;
#include <array> // array #include <array> // array
#include <cctype> // toupper
#include <cstdint> // uint32_t, uint64_t #include <cstdint> // uint32_t, uint64_t
#include <cstdio> // snprintf #include <cstdio> // snprintf
#include <cmath> // isfinite #include <cmath> // isfinite
@@ -596,6 +597,63 @@ TEST_CASE("lexer number fast path")
#endif #endif
} }
TEST_CASE("lexer literal fast path")
{
// For contiguous input, the rest of a literal is compared at once, and a
// mismatch is left to the byte path, which reports the error at the
// character that differs. Every truncation and every wrong character of
// each literal, at the end of the input or followed by more text, must
// give the byte path's result (a std::list is read one character at a
// time).
const auto outcome = [](const std::string & doc, bool byte_path) -> std::string
{
JSON_TRY
{
const std::list<char> chars(doc.begin(), doc.end());
return (byte_path ? json::parse(chars.begin(), chars.end()) : json::parse(doc)).dump();
}
JSON_CATCH (const json::exception & e)
{
return e.what();
}
};
std::vector<std::string> mismatches;
for (const std::string literal :
{"true", "false", "null"
})
{
std::vector<std::string> variants;
for (std::size_t length = 1; length <= literal.size(); ++length)
{
variants.push_back(literal.substr(0, length));
}
for (std::size_t i = 1; i < literal.size(); ++i)
{
std::string changed = literal;
changed[i] = 'X';
variants.push_back(changed);
changed[i] = static_cast<char>(std::toupper(static_cast<unsigned char>(literal[i])));
variants.push_back(changed);
}
for (const auto& variant : variants)
{
for (const std::string& doc :
{
variant, "[" + variant + "]", "[" + variant, variant + " ", variant + "1", "{\"k\":" + variant + "}"
})
{
if (outcome(doc, false) != outcome(doc, true))
{
mismatches.push_back(doc);
}
}
}
}
CAPTURE(mismatches)
CHECK(mismatches.empty());
}
TEST_CASE("lexer string fast path") TEST_CASE("lexer string fast path")
{ {
// Build a byte string from explicit values: a hex escape in a string // Build a byte string from explicit values: a hex escape in a string
+2 -1
View File
@@ -171,7 +171,8 @@ std::vector<std::string> corpus()
R"("unterminated)", "\"control\x01character\"", "\"ill-formed \xc3\x28 UTF-8\"", R"("bad \x escape")", R"("unterminated)", "\"control\x01character\"", "\"ill-formed \xc3\x28 UTF-8\"", R"("bad \x escape")",
R"("\ud800 lone surrogate")", R"("\ud800 lone surrogate")",
// literals // literals
"true", "false", "null", "tru", "nul", "falsey", "true", "false", "null", "tru", "nul", "falsey", "[true,false,null]", "nulL", "truex", "[fals e]", "t",
"true1", "null null", "[false]",
// structures // structures
"[]", "{}", "[1,2,3]", R"({"a":1,"b":[true,false,null],"c":{"d":"e"}})", "[1,]", R"({"a" 1})", "[[[[[]]]]]", "[]", "{}", "[1,2,3]", R"({"a":1,"b":[true,false,null],"c":{"d":"e"}})", "[1,]", R"({"a" 1})", "[[[[[]]]]]",
// pretty-printed // pretty-printed