Compare commits

..
Author SHA1 Message Date
Niels Lohmann 8bf710053e Merge branch 'perf/lexer-dispatch' into perf/dom-move-keys
Signed-off-by: Niels Lohmann <mail@nlohmann.me>
2026-10-16 08:21:29 +02:00
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 07be943802 Merge branch 'perf/lexer-dispatch' into perf/dom-move-keys
Signed-off-by: Niels Lohmann <mail@nlohmann.me>
2026-10-15 22:47:13 +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 38130afca9 Merge branch 'perf/lexer-dispatch' into perf/dom-move-keys
Signed-off-by: Niels Lohmann <mail@nlohmann.me>
2026-10-15 20:23:35 +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 fa5ecbae53 Move object keys into the value when parsing
json_sax_dom_parser::key() copied the key from the lexer's buffer into the
object. The lexer starts every token with an empty buffer (reset()), so the
key can be moved instead, which saves an allocation and a copy for every key
that does not fit into the string's inline buffer. json_sax_dom_parser
already moved string values the same way. ordered_map has no rvalue
operator[], so ordered_json still copies keys.

Signed-off-by: Niels Lohmann <mail@nlohmann.me>
2026-10-15 18:37:06 +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
7 changed files with 190 additions and 57 deletions

No files matched your search

+3 -2
View File
@@ -418,8 +418,9 @@ class json_sax_dom_parser
JSON_ASSERT(!ref_stack.empty());
JSON_ASSERT(ref_stack.back()->is_object());
// add null at the given key and store the reference for later
object_element = &(ref_stack.back()->m_data.m_value.object->operator[](val));
// add null at the given key and store the reference for later; the
// key is moved, as the lexer starts the next token with reset()
object_element = &(ref_stack.back()->m_data.m_value.object->operator[](std::move(val)));
return true;
}
+41 -21
View File
@@ -12,6 +12,7 @@
#include <cstddef> // size_t
#include <cstdint> // uint32_t
#include <cstdio> // snprintf
#include <cstring> // memcmp
#include <initializer_list> // initializer_list
#include <string> // char_traits, string
#include <utility> // move
@@ -1854,18 +1855,23 @@ scan_number_done:
}
/*!
@param[in] literal_text the literal text to expect
@param[in] length the length of the passed literal text
@param[in] return_type the token type to return on success
@param[in] rest the literal text after its first character, which
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
*/
JSON_HEDLEY_NON_NULL(2)
token_type scan_literal(const char_type* literal_text, const std::size_t length,
token_type return_type)
template<std::size_t N>
JSON_HEDLEY_NEVER_INLINE token_type scan_literal(const char (&rest)[N], token_type return_type)
{
JSON_ASSERT(char_traits<char_type>::to_char_type(current) == literal_text[0]);
for (std::size_t i = 1; i < length; ++i)
constexpr std::size_t length = N - 1;
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";
return token_type::parse_error;
@@ -1874,6 +1880,27 @@ scan_number_done:
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
/////////////////////
@@ -2499,21 +2526,14 @@ scan_number_done:
return token_type::value_separator;
// literals
// (the text after the first character; no local arrays, which
// would give this function a stack canary on every token)
case 't':
{
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);
}
return scan_literal("rue", token_type::literal_true);
case 'f':
{
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);
}
return scan_literal("alse", token_type::literal_false);
case 'n':
{
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);
}
return scan_literal("ull", token_type::literal_null);
// string
case '\"':
+6 -5
View File
@@ -244,8 +244,9 @@ class parser
bool sax_parse_internal(SAX* sax)
{
// stack to remember the hierarchy of structured values we are parsing
// true = array; false = object
std::vector<bool> states;
// true = array; false = object (bytes, not std::vector<bool>, whose
// push_back() is a call with GCC)
std::vector<std::uint8_t> states;
// value to avoid a goto (see comment where set to true)
bool skip_to_state_evaluation = false;
@@ -290,7 +291,7 @@ class parser
}
// remember we are now inside an object
states.push_back(false);
states.push_back(0);
// parse values
get_token();
@@ -315,7 +316,7 @@ class parser
}
// remember we are now inside an array
states.push_back(true);
states.push_back(1);
// parse values (no need to call get_token)
continue;
@@ -432,7 +433,7 @@ class parser
return true;
}
if (states.back()) // array
if (states.back() != 0) // array
{
// comma -> next value
// or end of array (ignore_trailing_commas = true)
+50 -28
View File
@@ -9519,6 +9519,7 @@ NLOHMANN_JSON_NAMESPACE_END
#include <cstddef> // size_t
#include <cstdint> // uint32_t
#include <cstdio> // snprintf
#include <cstring> // memcmp
#include <initializer_list> // initializer_list
#include <string> // char_traits, string
#include <utility> // move
@@ -13802,18 +13803,23 @@ scan_number_done:
}
/*!
@param[in] literal_text the literal text to expect
@param[in] length the length of the passed literal text
@param[in] return_type the token type to return on success
@param[in] rest the literal text after its first character, which
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
*/
JSON_HEDLEY_NON_NULL(2)
token_type scan_literal(const char_type* literal_text, const std::size_t length,
token_type return_type)
template<std::size_t N>
JSON_HEDLEY_NEVER_INLINE token_type scan_literal(const char (&rest)[N], token_type return_type)
{
JSON_ASSERT(char_traits<char_type>::to_char_type(current) == literal_text[0]);
for (std::size_t i = 1; i < length; ++i)
constexpr std::size_t length = N - 1;
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";
return token_type::parse_error;
@@ -13822,6 +13828,27 @@ scan_number_done:
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
/////////////////////
@@ -14447,21 +14474,14 @@ scan_number_done:
return token_type::value_separator;
// literals
// (the text after the first character; no local arrays, which
// would give this function a stack canary on every token)
case 't':
{
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);
}
return scan_literal("rue", token_type::literal_true);
case 'f':
{
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);
}
return scan_literal("alse", token_type::literal_false);
case 'n':
{
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);
}
return scan_literal("ull", token_type::literal_null);
// string
case '\"':
@@ -14963,8 +14983,9 @@ class json_sax_dom_parser
JSON_ASSERT(!ref_stack.empty());
JSON_ASSERT(ref_stack.back()->is_object());
// add null at the given key and store the reference for later
object_element = &(ref_stack.back()->m_data.m_value.object->operator[](val));
// add null at the given key and store the reference for later; the
// key is moved, as the lexer starts the next token with reset()
object_element = &(ref_stack.back()->m_data.m_value.object->operator[](std::move(val)));
return true;
}
@@ -20575,8 +20596,9 @@ class parser
bool sax_parse_internal(SAX* sax)
{
// stack to remember the hierarchy of structured values we are parsing
// true = array; false = object
std::vector<bool> states;
// true = array; false = object (bytes, not std::vector<bool>, whose
// push_back() is a call with GCC)
std::vector<std::uint8_t> states;
// value to avoid a goto (see comment where set to true)
bool skip_to_state_evaluation = false;
@@ -20621,7 +20643,7 @@ class parser
}
// remember we are now inside an object
states.push_back(false);
states.push_back(0);
// parse values
get_token();
@@ -20646,7 +20668,7 @@ class parser
}
// remember we are now inside an array
states.push_back(true);
states.push_back(1);
// parse values (no need to call get_token)
continue;
@@ -20763,7 +20785,7 @@ class parser
return true;
}
if (states.back()) // array
if (states.back() != 0) // array
{
// comma -> next value
// or end of array (ignore_trailing_commas = true)
+58
View File
@@ -13,6 +13,7 @@
using nlohmann::json;
#include <array> // array
#include <cctype> // toupper
#include <cstdint> // uint32_t, uint64_t
#include <cstdio> // snprintf
#include <cmath> // isfinite
@@ -596,6 +597,63 @@ TEST_CASE("lexer number fast path")
#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")
{
// Build a byte string from explicit values: a hex escape in a string
+30
View File
@@ -2452,6 +2452,36 @@ std::string parse_error_message_range(IteratorType first, IteratorType last)
}
} // namespace
TEST_CASE("object keys are moved into the value")
{
// json_sax_dom_parser::key() moves the key out of the lexer's buffer into
// the object, and the lexer starts the next token with an empty buffer.
// Keys of every length (inline and allocated strings), keys with escapes,
// and duplicate keys must give the same objects as reading the input one
// character at a time, and as ordered_json, whose map copies the key.
const std::string long_key(100, 'k');
const std::string doc = R"({"a": 1, ")" + long_key + R"(": {"b": [2, 3], "sixteen_chars_xx": 4}, )"
R"("esc\"apedé": 5, "a": 6, ")" + long_key + R"(x": 7, "": 8, "dup": 9, "dup": 10})";
const json j = json::parse(doc);
const std::list<char> chars(doc.begin(), doc.end());
CHECK(j == json::parse(chars.begin(), chars.end()));
CHECK(j.size() == 6);
CHECK(j["a"] == 6);
CHECK(j[long_key]["b"] == json({2, 3}));
CHECK(j[long_key]["sixteen_chars_xx"] == 4);
CHECK(j["esc\"aped\xc3\xa9"] == 5);
CHECK(j[long_key + "x"] == 7);
CHECK(j[""] == 8);
CHECK(j["dup"] == 10);
const auto o = nlohmann::ordered_json::parse(doc);
CHECK(o.size() == 6);
CHECK(o[long_key + "x"] == 7);
CHECK(json(o) == j);
}
TEST_CASE("text of float tokens")
{
// For contiguous input, the lexer copies the text of a float token into
+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"("\ud800 lone surrogate")",
// 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
"[]", "{}", "[1,2,3]", R"({"a":1,"b":[true,false,null],"c":{"d":"e"}})", "[1,]", R"({"a" 1})", "[[[[[]]]]]",
// pretty-printed