From dd7686a26937acfafcdaf7c1c484798c1dedefc8 Mon Sep 17 00:00:00 2001 From: Niels Lohmann Date: Sat, 24 Oct 2026 07:41:06 +0200 Subject: [PATCH] json_view: faster reading, indexing and C-stream parsing (#5888) * Read C streams into a json_document in blocks json_document::parse(FILE*) (and read(), accept(), parse_copy()) went through the library's file input adapter and appended the text character by character with fgetc. Read it with fread in blocks instead, like the std::istream overload already does. Parsing citm_catalog.json (1.7 MB) from a FILE*: Apple M1: 80.6 M -> 8.4 M instructions, 4.8 ms -> 0.55 ms (parse of the same text from a std::string: 8.7 M) Linux x86-64 Ir (whole run), GCC 13 / Clang 18: 47.0 M -> 13.7 M / 45.8 M -> 14.2 M The parsed documents and the error cases are identical (checked over the test corpus and 30 mutants of each file). A null FILE* is handled as before (by the library's adapter). Belongs to the PR "json_view: read-side speedups". Signed-off-by: Niels Lohmann * Index arrays without subtrees directly operator[](idx), at(idx) and back() skipped the elements of an array one after the other, whatever they were. In an array none of whose elements is an array or object (numbers, strings, booleans, null) the elements are adjacent nodes, which the array node's own sizes tell: its subtree has exactly one node more than it has elements. Such an element is now the node idx after the array node; any other array is walked as before. Reading r[i] for every i of an array of 3000 integers: Apple M1: 28.8 M instructions, 3.1 ms -> 0.1 M instructions, 4 us Linux x86-64 Ir (whole run), GCC 13 / Clang 18: 84.1 M -> 3.1 M / 75.0 M -> 3.0 M Arrays that contain arrays are unchanged. Belongs to the PR "json_view: read-side speedups". Signed-off-by: Niels Lohmann * Skip the bounds checks of number and string tokens in NUL-terminated input For a NUL-terminated input (std::string, C string, string literal), the parser reads the terminator instead of comparing p with the end, via cursor::cur(). Number and string scanning still tested `p != e` before each look at the next byte (after the integer digits for '.', for 'e', after the exponent sign and digits, and after a string for its closing quote), although the terminator can never be one of those characters. They now use cur(), which is the plain load for NUL-terminated input and the bounds check for ptr+len input, as before. Parsing, Apple M1 instructions per parse: twitter 3.126 M -> 3.089 M (-1.2 %), citm_catalog 8.70 M -> 8.58 M (-1.3 %), jeopardy 306.1 M -> 302.5 M (-1.2 %), signed integers 281 M -> 266 M (-5.4 %), canada and floats -0.1 % / -0.9 %. Linux x86-64 Ir, GCC 13 / Clang 18: twitter -0.5 % / -0.0 %, signed integers +0.2 % / -0.7 %. Belongs to the PR "json_view: read-side speedups". Signed-off-by: Niels Lohmann * Make the index text of items() on arrays lazy basic_json_view::items() on an array formatted every element's index with std::to_string when the item was made, whether or not the loop asks for the key. The item now keeps the position and makes the text on the first key() call. Objects are not affected. Iterating over the items of an array of 3000 numbers and reading only the values: Apple M1: 25.5 us -> 4.6 us (0.45 M -> ~0 net instructions) Linux x86-64 Ir (whole run), GCC 13 / Clang 18: 14.9 M -> 4.1 M / 15.3 M -> 4.6 M Reading key() costs what it did (24.5 us). Belongs to the PR "json_view: read-side speedups". Signed-off-by: Niels Lohmann * Make the default of value() with a C string only if the member is missing basic_json_view::value(key, "text") and value(pointer, "text") built a std::string from the default first and passed it on, so a call allocated for the default even when the member exists, and copied it again when it does not. They now look the member up and make the result directly: from the member if there is one, else from the C string. Behavior, including the exceptions, is the same. Two calls (member found, member missing) with a 40 character default, Apple M1: 1.6 K -> 0.7 K instructions Linux x86-64 Ir (500 rounds), GCC 13 / Clang 18: 2.46 M -> 2.22 M / 2.40 M -> 2.15 M Belongs to the PR "json_view: read-side speedups". Signed-off-by: Niels Lohmann * Materialize scalars directly and keep the stack of small subtrees local basic_json_view::materialize() (and get()) always set up the SAX handler that parse() uses and a std::vector for the open containers, sized from the document's deepest nesting. For a scalar, which is most of what is materialized one value at a time, neither is needed: the value is made directly (the same constructors the handler ends up calling). For arrays and objects the stack of open containers is now a local array for the first 16 levels, and the vector is only used for deeper documents. Materializing five scalars of a small document, Apple M1: 3.6 K -> 0.9 K instructions Linux x86-64 Ir (500 rounds), GCC 13 / Clang 18: 2.96 M -> 2.37 M / 2.90 M -> 2.29 M Belongs to the PR "json_view: read-side speedups". Signed-off-by: Niels Lohmann * Copy the text between escapes while scanning it A string that contains an escape is decoded by builder::slow_string(). After each escape it scanned the next run of plain bytes (8 or 16 byte-by-byte steps, then a vector loop), and then copied that run to the decode arena in a second pass. The run is now copied 16 bytes at a time while it is scanned (one vector load, a store of the same 16 bytes, and the mask of the bytes that end the run), and the write position lives in a local variable, not in the builder, since a store of a character may alias the builder's members and made the compiler reload it after every escape. A run that ends at a quote needs no scan, and non-ASCII text goes through the UTF-8 check as before; the portable code is unchanged. Parsing, Apple M1 instructions: jeopardy.json 304.0 M -> 293.8 M (-3.4 %), 3000 strings with escapes 1.72 M -> 1.54 M (-10 %), twitter -0.6 %. Linux x86-64 Ir, GCC 13 / Clang 18: escaped strings -7.6 % / -11.4 %, twitter 0.0 % / 0.0 %. Documents and error messages are identical on 900 k random strings of escapes, UTF-8 and control characters (also under ASan), and on mutants of the test corpus. Belongs to the PR "json_view: read-side speedups". Signed-off-by: Niels Lohmann * Hash the last bytes of a key of a large object without a loop key_hash() mixed a key eight bytes at a time and then packed the remaining 0-7 bytes one by one, in a loop. The last word of a key of eight or more bytes is now read as the last eight bytes of the key (overlapping the word before), and a key of 2-7 bytes as two overlapping words of four or two bytes. A product only moves bits upwards, and the slot is taken from the low bits: the high bits of the last word are folded down before the multiplication, since keys that differ only in their last characters ("user_17", "user_18") would otherwise share slots. The hash is not stored, so its values may change. Looking up every key of an object of 1000 random keys, Apple M1 instructions per 1000 lookups: keys of 5 bytes 175 K -> 93 K, hex strings of 8 bytes 177 K -> 168 K, keys of 12 bytes 196 K -> 186 K Linux x86-64 Ir (whole run), GCC 13 / Clang 18: 5-byte keys -4.7 % / -8.8 %, 12-byte keys -3.3 % / -7.4 %. Building the tables while parsing such objects is cheaper as well (5-byte keys: 13.3 us -> 10.1 us). Belongs to the PR "json_view: read-side speedups". Signed-off-by: Niels Lohmann * Compare short keys of a large object without a call of memcmp find_indexed() compared the key of a member the table points to with memcmp, a library call for any length. Up to 16 bytes, the bytes are now compared as two overlapping words (the first and the last 8, 4, or 2 bytes), as find_member() already does for the objects without a table; longer keys still use memcmp. Looking up every key of an object of 1000 random keys, Apple M1 instructions per 1000 lookups (with the previous commit): keys of 5 bytes 93 K -> 72 K, hex strings of 8 bytes 168 K -> 148 K, keys of 12 bytes 186 K -> 170 K, keys of 24 bytes 198 K -> 193 K (unchanged, memcmp). Linux x86-64 Ir (whole run), GCC 13 / Clang 18: 5-byte keys -7.2 % / -7.3 %, 12-byte keys -5.9 % / -6.6 %. Belongs to the PR "json_view: read-side speedups". Signed-off-by: Niels Lohmann * Read an unedited editable document like a read-only one Views of a json_editable_document followed edits at every step: the lookup, the element access, back(), dump() and materialize() were built for nodes that can be moved or linked, though no edit changes the index until the first one (every edit creates the edit state before it touches a node). Until then these now take the read-only code: one check of the document's edit state per call, not per member or per node. After the first edit nothing changes. Views that were made earlier keep working, as the check is made when they are used. Unedited editable document, Apple M1: dump() of twitter.json 1.40 M -> 1.22 M instructions (-12.6 %); lookups unchanged. Linux x86-64 Ir, GCC 13 / Clang 18: dump -1.7 % / -1.0 %, lookups -0.1 % / 0.0 %. Walking with iterators is unchanged, and so is dump() after an edit. Belongs to the PR "json_view: read-side speedups". Signed-off-by: Niels Lohmann * Format the numbered test keys of json_view's index with a stream The keys were made with snprintf() and a format from a list, which Flawfinder reports as an error (CWE-134, a format string that is not a literal). A stream now formats them; the 30000 keys of the test are the same as before. Signed-off-by: Niels Lohmann * Put the key number of the json_view index test in parentheses astyle formatted i * step after << as a pointer declaration (i* step), which failed the amalgamation check. Signed-off-by: Niels Lohmann * Fix the compiler and lint findings of json_view's read side and tests - find() and contains() dispatch on Editable for the member lookup; for documents that cannot be edited both branches were the same call (GCC -Wduplicated-branches) - json_view.hpp includes for the FILE overload (cpplint) - the arena check macro of the builder is on one line (cpplint read it as an if body without braces) - tests: no nested conditional (also MSVC C4018), no useless cast, parentheses around mixed operators, raw string literals, += instead of temporary concatenations, and NOLINT for the fixed seeds and a find() != npos, as elsewhere in the tests Signed-off-by: Niels Lohmann * Grow the decode arena through a function and store its position back on every return slow_string() kept its write position in locals and grew the arena through the macro NLOHMANN_VIEW_ENSURE. The macro is now the always-inline member arena_ensure() (a lambda is not inlined at every call site by clang, which then keeps the position in memory: +11 % instructions on escape-heavy strings), and a guard stores the position back into m_arena_write on every return instead of by hand on the successful one. Instruction counts are unchanged (clang) or 1 % lower (GCC). Also word the complexity of back() as that of at(). Signed-off-by: Niels Lohmann --------- Signed-off-by: Niels Lohmann --- docs/mkdocs/docs/api/basic_json_view/at.md | 3 +- docs/mkdocs/docs/api/basic_json_view/back.md | 3 +- .../docs/api/basic_json_view/operator[].md | 3 +- include/nlohmann/detail/view/builder.hpp | 210 +++++-- include/nlohmann/detail/view/input.hpp | 34 ++ include/nlohmann/detail/view/iterator.hpp | 18 +- include/nlohmann/detail/view/lookup.hpp | 8 + include/nlohmann/detail/view/materialize.hpp | 50 +- include/nlohmann/detail/view/object_index.hpp | 112 +++- include/nlohmann/detail/view/simd.hpp | 28 + include/nlohmann/json_view.hpp | 94 ++- single_include/nlohmann/json_view.hpp | 554 +++++++++++++++--- tests/src/unit-json_view.cpp | 127 ++++ tests/src/unit-json_view_builder.cpp | 52 ++ tests/src/unit-json_view_dump.cpp | 148 +++++ tests/src/unit-json_view_edit.cpp | 109 ++++ 16 files changed, 1386 insertions(+), 167 deletions(-) diff --git a/docs/mkdocs/docs/api/basic_json_view/at.md b/docs/mkdocs/docs/api/basic_json_view/at.md index ce142c23f..afbf81620 100644 --- a/docs/mkdocs/docs/api/basic_json_view/at.md +++ b/docs/mkdocs/docs/api/basic_json_view/at.md @@ -80,7 +80,8 @@ None of these exceptions carry a [`JSON_DIAGNOSTICS`](../macros/json_diagnostics Objects with 128 or more members get a hash index of their keys while parsing, so that a lookup in them takes constant time on average. 2. Linear in `idx`: elements are skipped one at a time from the first one, since they are not a fixed size in the - index (unlike `BasicJsonType`'s array, which is random-access). + index (unlike `BasicJsonType`'s array, which is random-access). Constant if no element of the array is an array + or object, since the elements are then adjacent in the index. 3. Linear in the number of reference tokens of `ptr` and, for each token, in the number of members of the object at that level (as 1.) or the index into the array (as 2.). diff --git a/docs/mkdocs/docs/api/basic_json_view/back.md b/docs/mkdocs/docs/api/basic_json_view/back.md index 1d70e9feb..acb6ebd65 100644 --- a/docs/mkdocs/docs/api/basic_json_view/back.md +++ b/docs/mkdocs/docs/api/basic_json_view/back.md @@ -28,7 +28,8 @@ This exception does not carry a [`JSON_DIAGNOSTICS`](../macros/json_diagnostics. Linear in the size of the array or object: unlike [`front()`](front.md), which only ever looks at the first element, `back()` has to walk every element to find where they end, since elements are not a fixed size in the -index. +index. Constant if no element of the array is an array or object, since the elements are then adjacent +in the index. ## Notes diff --git a/docs/mkdocs/docs/api/basic_json_view/operator[].md b/docs/mkdocs/docs/api/basic_json_view/operator[].md index 354813bdf..8aacf94c9 100644 --- a/docs/mkdocs/docs/api/basic_json_view/operator[].md +++ b/docs/mkdocs/docs/api/basic_json_view/operator[].md @@ -86,7 +86,8 @@ None of these exceptions carry a [`JSON_DIAGNOSTICS`](../macros/json_diagnostics Objects with 128 or more members get a hash index of their keys while parsing, so that a lookup in them takes constant time on average. 2. Linear in `idx`: elements are skipped one at a time from the first one, since they are not a fixed size in the - index (unlike `BasicJsonType`'s array, which is random-access). + index (unlike `BasicJsonType`'s array, which is random-access). Constant if no element of the array is an array + or object, since the elements are then adjacent in the index. 3. Linear in the number of reference tokens of `ptr` and, for each token, in the number of members of the object at that level (as 1.) or the index into the array (as 2.). diff --git a/include/nlohmann/detail/view/builder.hpp b/include/nlohmann/detail/view/builder.hpp index 3eb85ce47..3e4ea268a 100644 --- a/include/nlohmann/detail/view/builder.hpp +++ b/include/nlohmann/detail/view/builder.hpp @@ -275,11 +275,34 @@ class builder 0, 0, '\b', 0, 0, 0, '\f', 0, 0, 0, 0, 0, 0, 0, '\n', 0, 0, 0, '\r', 0, '\t', 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 } }; + // The write position is kept in locals: the stores of characters into + // the arena may alias the members of the builder, which would then be + // reloaded after every one. It is stored back where the arena grows, + // and by `write_back` on every return. + const unsigned char* const end = m_end; const std::size_t start = arena_used(); - arena_run(s, static_cast(p - s)); + char* w = m_arena_write; + const char* w_end = m_arena_end; + const arena_write_back write_back(*this, w); + { + const auto n = static_cast(p - s); + if (n <= 16 && end - s >= 16) + { + arena_ensure(w, w_end, 16); + std::memcpy(w, s, 16); + } + else if (n != 0) + { + // (an empty run leaves an empty arena unallocated, and memcpy() + // must not be passed its null pointer even for zero bytes) + arena_ensure(w, w_end, n); + std::memcpy(w, s, n); + } + w += n; + } for (;;) { - if (p == m_end) + if (p == end) { return failed(error_code::string_missing_quote, p); } @@ -287,7 +310,9 @@ class builder if (c == '"') { ++p; - return decoded{p, start, arena_used() - start}; + // (from w: `write_back` stores it only after the return value + // is made; the arena is allocated, since an escape was written) + return decoded{p, start, static_cast(w - m_doc.arena.data()) - start}; } if (c != '\\') { @@ -297,7 +322,7 @@ class builder return failed(c < 0x20 ? error_code::string_control_character : error_code::string_utf8, p); } ++p; - if (p == m_end) + if (p == end) { return failed(error_code::string_missing_quote, p); } @@ -315,7 +340,7 @@ class builder { return failed(error_code::string_surrogate_low, p); } - if (m_end - p < 2 || p[0] != '\\' || p[1] != 'u') + if (end - p < 2 || p[0] != '\\' || p[1] != 'u') { return failed(error_code::string_surrogate_high, p); } @@ -331,26 +356,106 @@ class builder } cp = 0x10000 + ((cp - 0xD800) << 10) + (lo - 0xDC00); } - arena_ensure(4); - m_arena_write = put_utf8(m_arena_write, cp); + arena_ensure(w, w_end, 4); + w = put_utf8(w, cp); } else if (NLOHMANN_VIEW_LIKELY(d < 128 && simple_escape[d] != 0)) { - arena_ensure(1); - *m_arena_write++ = simple_escape[d]; + arena_ensure(w, w_end, 1); + *w++ = simple_escape[d]; } else { --p; return failed(error_code::string_escape, p); } - if (p != m_end && *p == '\\') +#if NLOHMANN_VIEW_NEON + if (p != end && *p == '\\') { continue; // consecutive escapes ("\u00e4\u00f6"): no run in between } + // The first 16 bytes of the run are checked one by one, in predicted branches, and copied + // as one block: where the runs between escapes are short, the vector step below would put + // its longer dependency chain (load, compare, mask, count, add) on p for each of them, + // and be slower than these steps (the scanner of the keys works the same way) + { + if (end - p >= 16) + { + const std::uint8_t* const plain = string_plain(); + std::size_t steps = 16; +#define NLOHMANN_VIEW_STEP(i) if (NLOHMANN_VIEW_LIKELY(plain[p[i]] != 0)) {} else { steps = (i); goto run_found; } + NLOHMANN_VIEW_REPEAT16(NLOHMANN_VIEW_STEP) +#undef NLOHMANN_VIEW_STEP +run_found: + arena_ensure(w, w_end, 16); + std::memcpy(w, p, 16); // (only the first `steps` bytes are kept) + w += steps; + p += steps; + if (steps < 16) + { + if (*p >= 0x80) + { + // non-ASCII: the UTF-8 check, and the rest of the run + const unsigned char* const r = p; + p = scan_string_run(p, end); + const auto n = static_cast(p - r); + arena_ensure(w, w_end, n); + std::memcpy(w, r, n); + w += n; + } + continue; + } + } + } +#else + if (p != end && (*p == '\\' || *p == '"')) + { + continue; // consecutive escapes ("\u00e4\u00f6"), or the end of the string: no run in between + } +#endif +#if NLOHMANN_VIEW_VECTOR + // the plain bytes up to the next quote, backslash, or control + // character are copied as they are scanned, 16 bytes at a time + for (;;) + { + if (NLOHMANN_VIEW_UNLIKELY(end - p < 16)) + { + const unsigned char* const r = p; + p = scan_string_run(p, end); + const auto n = static_cast(p - r); + arena_ensure(w, w_end, n); + std::memcpy(w, r, n); + w += n; + break; + } + arena_ensure(w, w_end, 16); + const unsigned k = copy_plain_run16(p, w); + w += k; + p += k; + if (k == 16) + { + continue; + } + if (NLOHMANN_VIEW_UNLIKELY(*p >= 0x80)) + { + // non-ASCII: the UTF-8 check, and the rest of the run + const unsigned char* const r = p; + p = scan_string_run(p, end); + const auto n = static_cast(p - r); + arena_ensure(w, w_end, n); + std::memcpy(w, r, n); + w += n; + } + break; + } +#else const unsigned char* const r = p; - p = scan_string_run(p, m_end); - arena_run(r, static_cast(p - r)); + p = scan_string_run(p, end); + const auto n = static_cast(p - r); + arena_ensure(w, w_end, n); + std::memcpy(w, r, n); + w += n; +#endif } } @@ -392,19 +497,49 @@ class builder char* m_arena_write = nullptr; char* m_arena_end = nullptr; + /// room for n more bytes at w, the write position kept in a local, with + /// w_end the end of the arena. (A lambda instead is not always inlined, + /// and then w would be kept in memory, as m_arena_write is.) + NLOHMANN_VIEW_ALWAYS_INLINE void arena_ensure(char*& w, const char*& w_end, std::size_t n) + { + if (NLOHMANN_VIEW_UNLIKELY(static_cast(w_end - w) < n)) + { + m_arena_write = w; + arena_grow(n); + w = m_arena_write; + w_end = m_arena_end; + } + } + + /// stores a write position kept in a local back into m_arena_write when + /// the function that keeps it returns, on every path + class arena_write_back + { + public: + arena_write_back(builder& b, char*& w) noexcept + : m_builder(b) + , m_w(w) + {} + ~arena_write_back() + { + m_builder.m_arena_write = m_w; + } + + arena_write_back(const arena_write_back&) = delete; + arena_write_back& operator=(const arena_write_back&) = delete; + arena_write_back(arena_write_back&&) = delete; + arena_write_back& operator=(arena_write_back&&) = delete; + + private: + builder& m_builder; + char*& m_w; + }; + NLOHMANN_VIEW_ALWAYS_INLINE std::size_t arena_used() const noexcept { return m_arena_write != nullptr ? static_cast(m_arena_write - m_doc.arena.data()) : 0; } - NLOHMANN_VIEW_ALWAYS_INLINE void arena_ensure(std::size_t n) - { - if (NLOHMANN_VIEW_UNLIKELY(static_cast(m_arena_end - m_arena_write) < n)) - { - arena_grow(n); - } - } - NLOHMANN_VIEW_NOINLINE void arena_grow(std::size_t n) { const std::size_t used = arena_used(); @@ -413,25 +548,6 @@ class builder m_arena_end = m_doc.arena.data() + m_doc.arena.size(); } - /// append the run [r, r + n) to the arena; short runs as one fixed-size - /// 16-byte move when both sides have the room (no library call) - NLOHMANN_VIEW_ALWAYS_INLINE void arena_run(const unsigned char* r, std::size_t n) - { - if (n <= 16 && m_end - r >= 16) - { - arena_ensure(16); - std::memcpy(m_arena_write, r, 16); - } - else if (n != 0) - { - // (an empty run leaves an empty arena unallocated, and memcpy() - // must not be passed its null pointer even for zero bytes) - arena_ensure(n); - std::memcpy(m_arena_write, r, n); - } - m_arena_write += n; - } - /// UTF-8 encoding of cp at w (room for 4 bytes); returns the position after /// it. (A returned pointer stays in a register; writing through a /// reference to m_arena_write would store and reload it around every byte.) @@ -954,11 +1070,11 @@ indent_done: ++p; } const unsigned char* const int_start = p; - if (p != e && *p == '0') + if (cur() == '0') { ++p; } - else if (NLOHMANN_VIEW_LIKELY(p != e && *p >= '1' && *p <= '9')) + else if (NLOHMANN_VIEW_LIKELY(cur() >= '1' && cur() <= '9')) { p = skip_digits(p + 1, e); } @@ -969,7 +1085,7 @@ indent_done: const auto int_digits = static_cast(p - int_start); std::size_t frac_digits = 0; bool is_float = false; - if (p != e && *p == '.') + if (cur() == '.') { ++p; const unsigned char* const f0 = p; @@ -982,20 +1098,20 @@ indent_done: is_float = true; } std::int64_t exponent = 0; - if (p != e && (*p | 0x20) == 'e') + if ((cur() | 0x20) == 'e') { ++p; bool exp_negative = false; - if (p != e && (*p == '+' || *p == '-')) + if (cur() == '+' || cur() == '-') { exp_negative = *p == '-'; ++p; } - if (NLOHMANN_VIEW_UNLIKELY(p == e || !is_digit(*p))) + if (NLOHMANN_VIEW_UNLIKELY(!is_digit(cur()))) { return fail(error_code::number_after_exponent); } - while (p != e && is_digit(*p)) + while (is_digit(cur())) { if (exponent < 100000) { @@ -1057,7 +1173,7 @@ indent_done: ++p; // opening quote const unsigned char* const s = p; p = scan_string_run(p, e); - if (NLOHMANN_VIEW_LIKELY(p != e && *p == '"')) + if (NLOHMANN_VIEW_LIKELY(cur() == '"')) { push_node(value_t::string, 0, 0, static_cast(s - b), static_cast(p - s)); ++p; diff --git a/include/nlohmann/detail/view/input.hpp b/include/nlohmann/detail/view/input.hpp index 7e1fea09a..d028e81e2 100644 --- a/include/nlohmann/detail/view/input.hpp +++ b/include/nlohmann/detail/view/input.hpp @@ -10,6 +10,9 @@ #include // max #include // size_t +#ifndef JSON_NO_IO + #include // FILE, fread +#endif #include // istream, streambuf #include // basic_string, char_traits, string #include // decay, integral_constant, is_array, is_const, is_integral, is_lvalue_reference, is_pointer, is_same, remove_cv, remove_extent, remove_pointer, remove_reference @@ -154,6 +157,37 @@ inline void append_stream(std::string& buf, std::istream& is) is.clear(is.rdstate() | std::ios::eofbit); } +/// a pointer to a C stream (never with JSON_NO_IO, which leaves out ) +template +struct is_c_stream +#ifndef JSON_NO_IO + : std::is_same +#else + : std::false_type +#endif +{}; + +#ifndef JSON_NO_IO +/// append the rest of a C stream: in blocks with fread, not character by +/// character (the library's file adapter reads with fgetc); +/// f is not null +inline void append_file(std::string& buf, std::FILE* f) +{ + for (;;) + { + const std::size_t used = buf.size(); + const std::size_t room = (std::max)(buf.capacity() - used, std::size_t{4096}); + buf.resize(used + room); + const std::size_t got = std::fread(&buf[used], 1, room, f); + buf.resize(used + got); + if (got < room) + { + break; // end of file, or an error (as fgetc, which returns EOF for both) + } + } +} +#endif + } // namespace view } // namespace detail NLOHMANN_JSON_NAMESPACE_END diff --git a/include/nlohmann/detail/view/iterator.hpp b/include/nlohmann/detail/view/iterator.hpp index 12906e9c1..069f50fd1 100644 --- a/include/nlohmann/detail/view/iterator.hpp +++ b/include/nlohmann/detail/view/iterator.hpp @@ -147,20 +147,21 @@ class view_item view_item(const iterator& it, std::size_t index) : m_it(it) - { - if (!it.is_object_iterator()) - { - m_index = std::to_string(index); - } - } + , m_position(index) + {} - /// the member key, or the element index for arrays + /// the member key, or the element index for arrays (as text made on the + /// first call, so that iterating over the values pays nothing for it) string_view_t key() const { if (m_it.is_object_iterator()) { return m_it.key(); } + if (m_index.empty()) + { + m_index = std::to_string(m_position); + } return string_view_t(m_index.data(), m_index.size()); } @@ -183,7 +184,8 @@ class view_item private: iterator m_it; - std::string m_index{}; // NOLINT(readability-redundant-member-init) + std::size_t m_position; ///< the index of an array element + mutable std::string m_index{}; ///< the text of m_position, once key() was called // NOLINT(readability-redundant-member-init) }; /// the range returned by basic_json_view::items() diff --git a/include/nlohmann/detail/view/lookup.hpp b/include/nlohmann/detail/view/lookup.hpp index 10f7ab400..e2038a956 100644 --- a/include/nlohmann/detail/view/lookup.hpp +++ b/include/nlohmann/detail/view/lookup.hpp @@ -166,6 +166,10 @@ const node* element_at(const document_data& d, const node* array, std::size_t id { return e + idx; // a growable block: one link per element } + if (array->next == array->len + 1u && (!nav::editable() || (array->flags & node_flags::moved) == 0)) + { + return e + idx; // no element has a subtree: the elements are the nodes after the array + } for (std::size_t i = 0; i < idx; ++i) { e = document_data::after(e); @@ -181,6 +185,10 @@ const node* last_child(const document_data& d, const node* container) noexcept const std::size_t value_offset = container->kind == static_cast(value_t::object) ? 1 : 0; const node* const end = navigation::end(d, container); const node* last = navigation::first(d, container); + if (value_offset == 0 && container->next == container->len + 1u && (!navigation::editable() || (container->flags & node_flags::moved) == 0)) + { + return last + (container->len - 1u); // an array without subtrees: the elements are the nodes after it + } for (const node* c = document_data::after(last + value_offset); c != end; c = document_data::after(c + value_offset)) { last = c; diff --git a/include/nlohmann/detail/view/materialize.hpp b/include/nlohmann/detail/view/materialize.hpp index b15bec551..deebbc098 100644 --- a/include/nlohmann/detail/view/materialize.hpp +++ b/include/nlohmann/detail/view/materialize.hpp @@ -8,7 +8,7 @@ #pragma once -#include // max +#include // array #include // size_t #include // int64_t, uint8_t #include // string @@ -44,8 +44,9 @@ BasicJsonType materialize(const document_data& d, const node* n) using nav = navigation; // the open containers: their next element (or key of the next member), - // their end, and whether they are objects; sized once from the - // document's deepest nesting (for a subtree an upper bound) + // their end, and whether they are objects; for the first 16 levels on the + // stack, else sized once from the document's deepest nesting (for a + // subtree an upper bound) struct frame { const node* pos; @@ -53,10 +54,43 @@ BasicJsonType materialize(const document_data& d, const node* n) bool object; }; + // a scalar needs neither the handler nor the stack of open containers + switch (static_cast(n->kind)) + { + case value_t::string: + return BasicJsonType(string_t(d.str(*n), n->len)); + case value_t::number_integer: + return BasicJsonType(static_cast(static_cast(integer_bits(*n)))); + case value_t::number_unsigned: + return BasicJsonType(static_cast(integer_bits(*n))); + case value_t::number_float: + return BasicJsonType(float_value(d, *n)); + case value_t::boolean: + return BasicJsonType((n->flags & node_flags::is_true) != 0); + case value_t::null: + return BasicJsonType(nullptr); + case value_t::object: + case value_t::array: + case value_t::binary: + case value_t::discarded: + default: + break; + } + BasicJsonType result; sax_t sax(result, true); const string_t no_token{}; - std::vector open((std::max)(d.max_depth, std::size_t{1})); + // (shallow subtrees on the stack: a call for a small value then allocates only for the value) + std::array shallow; // NOLINT(cppcoreguidelines-pro-type-member-init,hicpp-member-init): filled as containers open + std::vector deep{}; + frame* open = shallow.data(); + std::size_t capacity = shallow.size(); + if (d.max_depth > capacity) + { + deep.resize(d.max_depth); + open = deep.data(); + capacity = deep.size(); + } std::size_t depth = 0; for (;;) { @@ -76,9 +110,13 @@ BasicJsonType materialize(const document_data& d, const node* n) { sax.start_array(n->len); } - if (NLOHMANN_VIEW_UNLIKELY(depth == open.size())) + if (NLOHMANN_VIEW_UNLIKELY(depth == capacity)) { - open.resize(depth * 2); // (deeper than the recorded nesting, e.g. after edits) + // (deeper than the recorded nesting, e.g. after edits) + deep.assign(open, open + depth); + deep.resize(depth * 2); + open = deep.data(); + capacity = deep.size(); } open[depth++] = frame{nav::first(d, n), nav::end(d, n), object}; break; diff --git a/include/nlohmann/detail/view/object_index.hpp b/include/nlohmann/detail/view/object_index.hpp index 5c7551fe9..9b0ba63ea 100644 --- a/include/nlohmann/detail/view/object_index.hpp +++ b/include/nlohmann/detail/view/object_index.hpp @@ -47,27 +47,96 @@ constexpr std::size_t index_max_displacement = 64; static_assert(document_data::index_min_members >= 2 * index_max_displacement, "a table must be much larger than the probe window of a lookup"); -/// hash of a key: its bytes, eight at a time, in a fixed byte order +/// hash of a key: its bytes, eight at a time (in a fixed byte order), where the +/// last word may overlap the one before (so that no loop over single bytes is +/// needed); a key of fewer than eight bytes is read as two overlapping words of +/// four or two bytes (in the native byte order: the hash is not stored, and the +/// same on both sides of a lookup) inline std::uint64_t key_hash(const char* s, std::size_t n) noexcept { std::uint64_t h = 0x9E3779B97F4A7C15u * (n + 1); const auto* p = reinterpret_cast(s); // NOLINT(cppcoreguidelines-pro-type-reinterpret-cast) - while (n >= 8) - { - h = (h ^ read_eight_bytes(p)) * 0xBF58476D1CE4E5B9u; - h ^= h >> 29u; - p += 8; - n -= 8; - } std::uint64_t w = 0; - for (std::size_t i = 0; i < n; ++i) + if (n >= 8) { - w |= static_cast(p[i]) << (8u * i); + const unsigned char* const last = p + n - 8; + while (p < last) + { + h = (h ^ read_eight_bytes(p)) * 0xBF58476D1CE4E5B9u; + h ^= h >> 29u; + p += 8; + } + w = read_eight_bytes(last); } - h = (h ^ w) * 0x94D049BB133111EBu; + else if (n >= 4) + { + std::uint32_t head = 0; + std::uint32_t tail = 0; + std::memcpy(&head, p, 4); + std::memcpy(&tail, p + n - 4, 4); + w = static_cast(head) | (static_cast(tail) << 32u); + } + else if (n >= 2) + { + std::uint16_t head = 0; + std::uint16_t tail = 0; + std::memcpy(&head, p, 2); + std::memcpy(&tail, p + n - 2, 2); + w = static_cast(head) | (static_cast(tail) << 16u); + } + else if (n == 1) + { + w = p[0]; + } + // (a product only carries bits upwards, and the slot is taken from the low + // bits: the high bits of the last word are folded down before the multiplication) + h = (h ^ w ^ (w >> 29u)) * 0x94D049BB133111EBu; return h ^ (h >> 31u); } +/// whether the n <= 16 bytes at a and b are equal, as two overlapping words +/// (the first and the last 8, 4, or 2 bytes) and without a call +NLOHMANN_VIEW_ALWAYS_INLINE bool same_short_bytes(const unsigned char* a, const unsigned char* b, std::size_t n) noexcept +{ + if (n >= 8) + { + std::uint64_t x0 = 0; + std::uint64_t x1 = 0; + std::uint64_t y0 = 0; + std::uint64_t y1 = 0; + std::memcpy(&x0, a, 8); + std::memcpy(&x1, a + n - 8, 8); + std::memcpy(&y0, b, 8); + std::memcpy(&y1, b + n - 8, 8); + return x0 == y0 && x1 == y1; + } + if (n >= 4) + { + std::uint32_t x0 = 0; + std::uint32_t x1 = 0; + std::uint32_t y0 = 0; + std::uint32_t y1 = 0; + std::memcpy(&x0, a, 4); + std::memcpy(&x1, a + n - 4, 4); + std::memcpy(&y0, b, 4); + std::memcpy(&y1, b + n - 4, 4); + return x0 == y0 && x1 == y1; + } + if (n >= 2) + { + std::uint16_t x0 = 0; + std::uint16_t x1 = 0; + std::uint16_t y0 = 0; + std::uint16_t y1 = 0; + std::memcpy(&x0, a, 2); + std::memcpy(&x1, a + n - 2, 2); + std::memcpy(&y0, b, 2); + std::memcpy(&y1, b + n - 2, 2); + return x0 == y0 && x1 == y1; + } + return n == 0 || *a == *b; +} + /// build the table of a large object inline void build_object_index(document_data& d, node* obj) { @@ -132,6 +201,27 @@ inline const node* find_indexed(const document_data& d, const node* obj, const c const document_data::object_index& ix = d.indexes[obj->extra - 1u]; const std::uint32_t* const table = d.index_slots.data() + ix.start; std::size_t slot = conditional_static_cast(key_hash(key, n)) & ix.mask; + const auto* const k_bytes = reinterpret_cast(key); // NOLINT(cppcoreguidelines-pro-type-reinterpret-cast) + // (the loop for longer keys comes first, as the code is laid out; it is not the more common case) + if (NLOHMANN_VIEW_UNLIKELY(n <= 16)) + { + // (up to 16 bytes, the bytes are compared as two words, without a call) + for (std::size_t distance = 0; distance <= index_max_displacement; ++distance) + { + const std::uint32_t s = table[slot]; + if (s == 0) + { + return nullptr; + } + const node* const k = obj + s; + if (k->len == n && same_short_bytes(reinterpret_cast(d.str(*k)), k_bytes, n)) // NOLINT(cppcoreguidelines-pro-type-reinterpret-cast) + { + return k; + } + slot = (slot + 1) & ix.mask; + } + return nullptr; + } for (std::size_t distance = 0; distance <= index_max_displacement; ++distance) { const std::uint32_t s = table[slot]; diff --git a/include/nlohmann/detail/view/simd.hpp b/include/nlohmann/detail/view/simd.hpp index b83cd5a3b..7a0248f63 100644 --- a/include/nlohmann/detail/view/simd.hpp +++ b/include/nlohmann/detail/view/simd.hpp @@ -116,6 +116,34 @@ NLOHMANN_VIEW_ALWAYS_INLINE const unsigned char* vector_plain_run(const unsigned } return p; } + +/*! +@brief copy 16 bytes from p to w, and find the first quote, backslash, +control character, or non-ASCII byte among them + +The bytes are copied whether or not they are all plain (the caller has room +for 16 bytes and keeps only the plain ones). + +@return the offset of that byte, or 16 if there is none +*/ +NLOHMANN_VIEW_ALWAYS_INLINE unsigned copy_plain_run16(const unsigned char* p, char* w) noexcept +{ +#if NLOHMANN_VIEW_NEON + const uint8x16_t in = vld1q_u8(p); + vst1q_u8(reinterpret_cast(w), in); // NOLINT(cppcoreguidelines-pro-type-reinterpret-cast) + const uint8x16_t special = vorrq_u8(vorrq_u8(vceqq_u8(in, vdupq_n_u8('"')), vceqq_u8(in, vdupq_n_u8('\\'))), + vcltq_s8(vreinterpretq_s8_u8(in), vdupq_n_s8(0x20))); + const std::uint64_t bits = vget_lane_u64(vreinterpret_u64_u8(vshrn_n_u16(vreinterpretq_u16_u8(special), 4)), 0); + return bits != 0 ? static_cast(count_trailing_zeros(bits) >> 2u) : 16u; +#else + const __m128i in = _mm_loadu_si128(static_cast(static_cast(p))); // NOLINT(bugprone-casting-through-void) + _mm_storeu_si128(static_cast<__m128i*>(static_cast(w)), in); // NOLINT(bugprone-casting-through-void) + const __m128i special = _mm_or_si128(_mm_or_si128(_mm_cmpeq_epi8(in, _mm_set1_epi8('"')), _mm_cmpeq_epi8(in, _mm_set1_epi8('\\'))), + _mm_cmplt_epi8(in, _mm_set1_epi8(0x20))); + const auto bits = static_cast(static_cast(_mm_movemask_epi8(special))); + return bits != 0 ? static_cast(count_trailing_zeros(bits)) : 16u; +#endif +} #endif #if NLOHMANN_VIEW_SSSE3_DISPATCH diff --git a/include/nlohmann/json_view.hpp b/include/nlohmann/json_view.hpp index f0fc58dc9..86481d884 100644 --- a/include/nlohmann/json_view.hpp +++ b/include/nlohmann/json_view.hpp @@ -30,6 +30,7 @@ #include // array #include // nullptr_t, size_t // IWYU pragma: keep #include // uint8_t, uint32_t +#include // FILE #include // memcpy, strlen #include // istream // IWYU pragma: keep #include // distance, input_iterator_tag, iterator_traits @@ -295,7 +296,7 @@ class basic_json_view } detail::view::throw_type_error(305, "cannot use operator[] with a numeric argument with ", type_name()); } - return idx < m_node->len ? basic_json_view(m_doc, navigation::value(detail::view::element_at(*m_doc, m_node, idx))) : basic_json_view(); + return idx < m_node->len ? element(idx) : basic_json_view(); } /// any other integer type (int, unsigned, long, std::int64_t, ...; a @@ -358,7 +359,7 @@ class basic_json_view { detail::view::throw_out_of_range(401, detail::concat("array index ", std::to_string(idx), " is out of range")); } - return basic_json_view(m_doc, navigation::value(detail::view::element_at(*m_doc, m_node, idx))); + return element(idx); } /// any other integer type, see operator[]; negative values are out of @@ -392,7 +393,13 @@ class basic_json_view string_t value(string_view_t key, const char* default_value) const { - return value(key, string_t(default_value)); + if (NLOHMANN_VIEW_UNLIKELY(!is_object())) + { + detail::view::throw_type_error(306, "cannot use value() with ", type_name()); + } + // (the default is made only if there is no such member) + const basic_json_view r = lookup(key); + return r.is_discarded() ? string_t(default_value) : r.template get(); } /// the value a JSON pointer refers to converted to T, or the default @@ -411,7 +418,12 @@ class basic_json_view string_t value(const json_pointer& ptr, const char* default_value) const { - return value(ptr, string_t(default_value)); + if (NLOHMANN_VIEW_UNLIKELY(!is_structured())) + { + detail::view::throw_type_error(306, "cannot use value() with ", type_name()); + } + const basic_json_view r = detail::view::resolve_pointer(*this, detail::json_pointer_access::reference_tokens(ptr), detail::view::pointer_mode::value); + return r.is_discarded() ? string_t(default_value) : r.template get(); } /// the first element or member value; a primitive value itself. Throws @@ -433,7 +445,8 @@ class basic_json_view { if (is_structured() && m_node->len != 0) { - return basic_json_view(m_doc, navigation::value(detail::view::last_child(*m_doc, m_node) + (is_object() ? 1 : 0))); + return unedited() ? basic_json_view(m_doc, detail::view::last_child(*m_doc, m_node) + (is_object() ? 1 : 0)) + : basic_json_view(m_doc, navigation::value(detail::view::last_child(*m_doc, m_node) + (is_object() ? 1 : 0))); } return front(); } @@ -450,7 +463,7 @@ class basic_json_view { return end(); } - const node* const k = detail::view::find_member(*m_doc, m_node, key.data(), key.size()); + const node* const k = find_key(key); return k != nullptr ? iterator(m_doc, k, true) : end(); } @@ -467,7 +480,7 @@ class basic_json_view /// whether this is an object with a member with this key bool contains(string_view_t key) const { - return is_object() && detail::view::find_member(*m_doc, m_node, key.data(), key.size()) != nullptr; + return is_object() && find_key(key) != nullptr; } bool contains(const char* key) const @@ -627,7 +640,14 @@ class basic_json_view // the compact writer keeps 64 bytes of slack, so that it does not grow // the buffer just before the end const std::size_t estimate = source_extent() + (style.pretty ? source_extent() / 2 : 0) + 160; - detail::view::view_serializer(*m_doc, out, estimate, style).dump(m_node); + if (unedited()) + { + detail::view::view_serializer(*m_doc, out, estimate, style).dump(m_node); + } + else + { + detail::view::view_serializer(*m_doc, out, estimate, style).dump(m_node); + } return out; } @@ -707,7 +727,7 @@ class basic_json_view { return BasicJsonType(value_t::discarded); } - return detail::view::materialize(*m_doc, m_node); + return unedited() ? detail::view::materialize(*m_doc, m_node) : detail::view::materialize(*m_doc, m_node); } /// byte offset of this value in the source text (for strings: of the @@ -785,10 +805,50 @@ class basic_json_view /// (object required) NLOHMANN_VIEW_ALWAYS_INLINE basic_json_view lookup(string_view_t key) const noexcept { + if (unedited()) + { + const node* const k = detail::view::find_member(*m_doc, m_node, key.data(), key.size()); + return k != nullptr ? basic_json_view(m_doc, k + 1) : basic_json_view(); + } const node* const k = detail::view::find_member(*m_doc, m_node, key.data(), key.size()); return k != nullptr ? basic_json_view(m_doc, navigation::value(k + 1)) : basic_json_view(); } + /// whether the document has not been edited yet: until the first edit, its index is walked like that of a + /// read-only document (nothing is moved or linked), which is cheaper + NLOHMANN_VIEW_ALWAYS_INLINE bool unedited() const noexcept + { + return !navigation::editable() || m_doc->edits == nullptr; + } + + /// the member of this object with @a key (the last one if the key occurs more than once), or nullptr + NLOHMANN_VIEW_ALWAYS_INLINE const node* find_key(string_view_t key) const + { + return find_key(key, std::integral_constant {}); + } + + NLOHMANN_VIEW_ALWAYS_INLINE const node* find_key(string_view_t key, std::true_type /*editable*/) const + { + // (an editable document without edits is looked up as one that cannot be edited) + return unedited() ? detail::view::find_member(*m_doc, m_node, key.data(), key.size()) + : detail::view::find_member(*m_doc, m_node, key.data(), key.size()); + } + + NLOHMANN_VIEW_ALWAYS_INLINE const node* find_key(string_view_t key, std::false_type /*editable*/) const + { + return detail::view::find_member(*m_doc, m_node, key.data(), key.size()); + } + + /// the element of an array at an index below its size + NLOHMANN_VIEW_ALWAYS_INLINE basic_json_view element(size_type idx) const + { + if (unedited()) + { + return basic_json_view(m_doc, detail::view::element_at(*m_doc, m_node, idx)); + } + return basic_json_view(m_doc, navigation::value(detail::view::element_at(*m_doc, m_node, idx))); + } + // --- get() dispatch --- bool get_impl(detail::view::value_tag /*unused*/, detail::priority_tag<2> /*unused*/) const @@ -1528,7 +1588,8 @@ class basic_json_document // NOLINTBEGIN(readability-avoid-nested-conditional-operator): a constant expression of C++11 append_input_impl(buf, std::forward(input), std::integral_constant < int, detail::is_contiguous_byte_container::value ? 0 : std::is_base_of::value ? 1 - : detail::view::is_adapter_input::value ? 2 : 3 > {}); + : detail::view::is_adapter_input::value ? 2 + : detail::view::is_c_stream::value ? 4 : 3 > {}); // NOLINTEND(readability-avoid-nested-conditional-operator) } @@ -1550,6 +1611,19 @@ class basic_json_document detail::view::append_adapter(buf, std::move(input.ia)); } +#ifndef JSON_NO_IO + static void append_input_impl(std::string& buf, std::FILE* file, std::integral_constant /*C stream*/) + { + if (file == nullptr) + { + // (as before: the library's adapter decides what a null stream yields) + detail::view::append_adapter(buf, detail::input_adapter(file)); + return; + } + detail::view::append_file(buf, file); + } +#endif + template static void append_input_impl(std::string& buf, T&& input, std::integral_constant /*other*/) { diff --git a/single_include/nlohmann/json_view.hpp b/single_include/nlohmann/json_view.hpp index 7b058469d..65459b072 100644 --- a/single_include/nlohmann/json_view.hpp +++ b/single_include/nlohmann/json_view.hpp @@ -30,6 +30,7 @@ #include // array #include // nullptr_t, size_t // IWYU pragma: keep #include // uint8_t, uint32_t +#include // FILE #include // memcpy, strlen #include // istream // IWYU pragma: keep #include // distance, input_iterator_tag, iterator_traits @@ -756,6 +757,34 @@ NLOHMANN_VIEW_ALWAYS_INLINE const unsigned char* vector_plain_run(const unsigned } return p; } + +/*! +@brief copy 16 bytes from p to w, and find the first quote, backslash, +control character, or non-ASCII byte among them + +The bytes are copied whether or not they are all plain (the caller has room +for 16 bytes and keeps only the plain ones). + +@return the offset of that byte, or 16 if there is none +*/ +NLOHMANN_VIEW_ALWAYS_INLINE unsigned copy_plain_run16(const unsigned char* p, char* w) noexcept +{ +#if NLOHMANN_VIEW_NEON + const uint8x16_t in = vld1q_u8(p); + vst1q_u8(reinterpret_cast(w), in); // NOLINT(cppcoreguidelines-pro-type-reinterpret-cast) + const uint8x16_t special = vorrq_u8(vorrq_u8(vceqq_u8(in, vdupq_n_u8('"')), vceqq_u8(in, vdupq_n_u8('\\'))), + vcltq_s8(vreinterpretq_s8_u8(in), vdupq_n_s8(0x20))); + const std::uint64_t bits = vget_lane_u64(vreinterpret_u64_u8(vshrn_n_u16(vreinterpretq_u16_u8(special), 4)), 0); + return bits != 0 ? static_cast(count_trailing_zeros(bits) >> 2u) : 16u; +#else + const __m128i in = _mm_loadu_si128(static_cast(static_cast(p))); // NOLINT(bugprone-casting-through-void) + _mm_storeu_si128(static_cast<__m128i*>(static_cast(w)), in); // NOLINT(bugprone-casting-through-void) + const __m128i special = _mm_or_si128(_mm_or_si128(_mm_cmpeq_epi8(in, _mm_set1_epi8('"')), _mm_cmpeq_epi8(in, _mm_set1_epi8('\\'))), + _mm_cmplt_epi8(in, _mm_set1_epi8(0x20))); + const auto bits = static_cast(static_cast(_mm_movemask_epi8(special))); + return bits != 0 ? static_cast(count_trailing_zeros(bits)) : 16u; +#endif +} #endif #if NLOHMANN_VIEW_SSSE3_DISPATCH @@ -1437,11 +1466,34 @@ class builder 0, 0, '\b', 0, 0, 0, '\f', 0, 0, 0, 0, 0, 0, 0, '\n', 0, 0, 0, '\r', 0, '\t', 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 } }; + // The write position is kept in locals: the stores of characters into + // the arena may alias the members of the builder, which would then be + // reloaded after every one. It is stored back where the arena grows, + // and by `write_back` on every return. + const unsigned char* const end = m_end; const std::size_t start = arena_used(); - arena_run(s, static_cast(p - s)); + char* w = m_arena_write; + const char* w_end = m_arena_end; + const arena_write_back write_back(*this, w); + { + const auto n = static_cast(p - s); + if (n <= 16 && end - s >= 16) + { + arena_ensure(w, w_end, 16); + std::memcpy(w, s, 16); + } + else if (n != 0) + { + // (an empty run leaves an empty arena unallocated, and memcpy() + // must not be passed its null pointer even for zero bytes) + arena_ensure(w, w_end, n); + std::memcpy(w, s, n); + } + w += n; + } for (;;) { - if (p == m_end) + if (p == end) { return failed(error_code::string_missing_quote, p); } @@ -1449,7 +1501,9 @@ class builder if (c == '"') { ++p; - return decoded{p, start, arena_used() - start}; + // (from w: `write_back` stores it only after the return value + // is made; the arena is allocated, since an escape was written) + return decoded{p, start, static_cast(w - m_doc.arena.data()) - start}; } if (c != '\\') { @@ -1459,7 +1513,7 @@ class builder return failed(c < 0x20 ? error_code::string_control_character : error_code::string_utf8, p); } ++p; - if (p == m_end) + if (p == end) { return failed(error_code::string_missing_quote, p); } @@ -1477,7 +1531,7 @@ class builder { return failed(error_code::string_surrogate_low, p); } - if (m_end - p < 2 || p[0] != '\\' || p[1] != 'u') + if (end - p < 2 || p[0] != '\\' || p[1] != 'u') { return failed(error_code::string_surrogate_high, p); } @@ -1493,26 +1547,106 @@ class builder } cp = 0x10000 + ((cp - 0xD800) << 10) + (lo - 0xDC00); } - arena_ensure(4); - m_arena_write = put_utf8(m_arena_write, cp); + arena_ensure(w, w_end, 4); + w = put_utf8(w, cp); } else if (NLOHMANN_VIEW_LIKELY(d < 128 && simple_escape[d] != 0)) { - arena_ensure(1); - *m_arena_write++ = simple_escape[d]; + arena_ensure(w, w_end, 1); + *w++ = simple_escape[d]; } else { --p; return failed(error_code::string_escape, p); } - if (p != m_end && *p == '\\') +#if NLOHMANN_VIEW_NEON + if (p != end && *p == '\\') { continue; // consecutive escapes ("\u00e4\u00f6"): no run in between } + // The first 16 bytes of the run are checked one by one, in predicted branches, and copied + // as one block: where the runs between escapes are short, the vector step below would put + // its longer dependency chain (load, compare, mask, count, add) on p for each of them, + // and be slower than these steps (the scanner of the keys works the same way) + { + if (end - p >= 16) + { + const std::uint8_t* const plain = string_plain(); + std::size_t steps = 16; +#define NLOHMANN_VIEW_STEP(i) if (NLOHMANN_VIEW_LIKELY(plain[p[i]] != 0)) {} else { steps = (i); goto run_found; } + NLOHMANN_VIEW_REPEAT16(NLOHMANN_VIEW_STEP) +#undef NLOHMANN_VIEW_STEP +run_found: + arena_ensure(w, w_end, 16); + std::memcpy(w, p, 16); // (only the first `steps` bytes are kept) + w += steps; + p += steps; + if (steps < 16) + { + if (*p >= 0x80) + { + // non-ASCII: the UTF-8 check, and the rest of the run + const unsigned char* const r = p; + p = scan_string_run(p, end); + const auto n = static_cast(p - r); + arena_ensure(w, w_end, n); + std::memcpy(w, r, n); + w += n; + } + continue; + } + } + } +#else + if (p != end && (*p == '\\' || *p == '"')) + { + continue; // consecutive escapes ("\u00e4\u00f6"), or the end of the string: no run in between + } +#endif +#if NLOHMANN_VIEW_VECTOR + // the plain bytes up to the next quote, backslash, or control + // character are copied as they are scanned, 16 bytes at a time + for (;;) + { + if (NLOHMANN_VIEW_UNLIKELY(end - p < 16)) + { + const unsigned char* const r = p; + p = scan_string_run(p, end); + const auto n = static_cast(p - r); + arena_ensure(w, w_end, n); + std::memcpy(w, r, n); + w += n; + break; + } + arena_ensure(w, w_end, 16); + const unsigned k = copy_plain_run16(p, w); + w += k; + p += k; + if (k == 16) + { + continue; + } + if (NLOHMANN_VIEW_UNLIKELY(*p >= 0x80)) + { + // non-ASCII: the UTF-8 check, and the rest of the run + const unsigned char* const r = p; + p = scan_string_run(p, end); + const auto n = static_cast(p - r); + arena_ensure(w, w_end, n); + std::memcpy(w, r, n); + w += n; + } + break; + } +#else const unsigned char* const r = p; - p = scan_string_run(p, m_end); - arena_run(r, static_cast(p - r)); + p = scan_string_run(p, end); + const auto n = static_cast(p - r); + arena_ensure(w, w_end, n); + std::memcpy(w, r, n); + w += n; +#endif } } @@ -1554,19 +1688,49 @@ class builder char* m_arena_write = nullptr; char* m_arena_end = nullptr; + /// room for n more bytes at w, the write position kept in a local, with + /// w_end the end of the arena. (A lambda instead is not always inlined, + /// and then w would be kept in memory, as m_arena_write is.) + NLOHMANN_VIEW_ALWAYS_INLINE void arena_ensure(char*& w, const char*& w_end, std::size_t n) + { + if (NLOHMANN_VIEW_UNLIKELY(static_cast(w_end - w) < n)) + { + m_arena_write = w; + arena_grow(n); + w = m_arena_write; + w_end = m_arena_end; + } + } + + /// stores a write position kept in a local back into m_arena_write when + /// the function that keeps it returns, on every path + class arena_write_back + { + public: + arena_write_back(builder& b, char*& w) noexcept + : m_builder(b) + , m_w(w) + {} + ~arena_write_back() + { + m_builder.m_arena_write = m_w; + } + + arena_write_back(const arena_write_back&) = delete; + arena_write_back& operator=(const arena_write_back&) = delete; + arena_write_back(arena_write_back&&) = delete; + arena_write_back& operator=(arena_write_back&&) = delete; + + private: + builder& m_builder; + char*& m_w; + }; + NLOHMANN_VIEW_ALWAYS_INLINE std::size_t arena_used() const noexcept { return m_arena_write != nullptr ? static_cast(m_arena_write - m_doc.arena.data()) : 0; } - NLOHMANN_VIEW_ALWAYS_INLINE void arena_ensure(std::size_t n) - { - if (NLOHMANN_VIEW_UNLIKELY(static_cast(m_arena_end - m_arena_write) < n)) - { - arena_grow(n); - } - } - NLOHMANN_VIEW_NOINLINE void arena_grow(std::size_t n) { const std::size_t used = arena_used(); @@ -1575,25 +1739,6 @@ class builder m_arena_end = m_doc.arena.data() + m_doc.arena.size(); } - /// append the run [r, r + n) to the arena; short runs as one fixed-size - /// 16-byte move when both sides have the room (no library call) - NLOHMANN_VIEW_ALWAYS_INLINE void arena_run(const unsigned char* r, std::size_t n) - { - if (n <= 16 && m_end - r >= 16) - { - arena_ensure(16); - std::memcpy(m_arena_write, r, 16); - } - else if (n != 0) - { - // (an empty run leaves an empty arena unallocated, and memcpy() - // must not be passed its null pointer even for zero bytes) - arena_ensure(n); - std::memcpy(m_arena_write, r, n); - } - m_arena_write += n; - } - /// UTF-8 encoding of cp at w (room for 4 bytes); returns the position after /// it. (A returned pointer stays in a register; writing through a /// reference to m_arena_write would store and reload it around every byte.) @@ -2116,11 +2261,11 @@ indent_done: ++p; } const unsigned char* const int_start = p; - if (p != e && *p == '0') + if (cur() == '0') { ++p; } - else if (NLOHMANN_VIEW_LIKELY(p != e && *p >= '1' && *p <= '9')) + else if (NLOHMANN_VIEW_LIKELY(cur() >= '1' && cur() <= '9')) { p = skip_digits(p + 1, e); } @@ -2131,7 +2276,7 @@ indent_done: const auto int_digits = static_cast(p - int_start); std::size_t frac_digits = 0; bool is_float = false; - if (p != e && *p == '.') + if (cur() == '.') { ++p; const unsigned char* const f0 = p; @@ -2144,20 +2289,20 @@ indent_done: is_float = true; } std::int64_t exponent = 0; - if (p != e && (*p | 0x20) == 'e') + if ((cur() | 0x20) == 'e') { ++p; bool exp_negative = false; - if (p != e && (*p == '+' || *p == '-')) + if (cur() == '+' || cur() == '-') { exp_negative = *p == '-'; ++p; } - if (NLOHMANN_VIEW_UNLIKELY(p == e || !is_digit(*p))) + if (NLOHMANN_VIEW_UNLIKELY(!is_digit(cur()))) { return fail(error_code::number_after_exponent); } - while (p != e && is_digit(*p)) + while (is_digit(cur())) { if (exponent < 100000) { @@ -2219,7 +2364,7 @@ indent_done: ++p; // opening quote const unsigned char* const s = p; p = scan_string_run(p, e); - if (NLOHMANN_VIEW_LIKELY(p != e && *p == '"')) + if (NLOHMANN_VIEW_LIKELY(cur() == '"')) { push_node(value_t::string, 0, 0, static_cast(s - b), static_cast(p - s)); ++p; @@ -3168,27 +3313,96 @@ constexpr std::size_t index_max_displacement = 64; static_assert(document_data::index_min_members >= 2 * index_max_displacement, "a table must be much larger than the probe window of a lookup"); -/// hash of a key: its bytes, eight at a time, in a fixed byte order +/// hash of a key: its bytes, eight at a time (in a fixed byte order), where the +/// last word may overlap the one before (so that no loop over single bytes is +/// needed); a key of fewer than eight bytes is read as two overlapping words of +/// four or two bytes (in the native byte order: the hash is not stored, and the +/// same on both sides of a lookup) inline std::uint64_t key_hash(const char* s, std::size_t n) noexcept { std::uint64_t h = 0x9E3779B97F4A7C15u * (n + 1); const auto* p = reinterpret_cast(s); // NOLINT(cppcoreguidelines-pro-type-reinterpret-cast) - while (n >= 8) - { - h = (h ^ read_eight_bytes(p)) * 0xBF58476D1CE4E5B9u; - h ^= h >> 29u; - p += 8; - n -= 8; - } std::uint64_t w = 0; - for (std::size_t i = 0; i < n; ++i) + if (n >= 8) { - w |= static_cast(p[i]) << (8u * i); + const unsigned char* const last = p + n - 8; + while (p < last) + { + h = (h ^ read_eight_bytes(p)) * 0xBF58476D1CE4E5B9u; + h ^= h >> 29u; + p += 8; + } + w = read_eight_bytes(last); } - h = (h ^ w) * 0x94D049BB133111EBu; + else if (n >= 4) + { + std::uint32_t head = 0; + std::uint32_t tail = 0; + std::memcpy(&head, p, 4); + std::memcpy(&tail, p + n - 4, 4); + w = static_cast(head) | (static_cast(tail) << 32u); + } + else if (n >= 2) + { + std::uint16_t head = 0; + std::uint16_t tail = 0; + std::memcpy(&head, p, 2); + std::memcpy(&tail, p + n - 2, 2); + w = static_cast(head) | (static_cast(tail) << 16u); + } + else if (n == 1) + { + w = p[0]; + } + // (a product only carries bits upwards, and the slot is taken from the low + // bits: the high bits of the last word are folded down before the multiplication) + h = (h ^ w ^ (w >> 29u)) * 0x94D049BB133111EBu; return h ^ (h >> 31u); } +/// whether the n <= 16 bytes at a and b are equal, as two overlapping words +/// (the first and the last 8, 4, or 2 bytes) and without a call +NLOHMANN_VIEW_ALWAYS_INLINE bool same_short_bytes(const unsigned char* a, const unsigned char* b, std::size_t n) noexcept +{ + if (n >= 8) + { + std::uint64_t x0 = 0; + std::uint64_t x1 = 0; + std::uint64_t y0 = 0; + std::uint64_t y1 = 0; + std::memcpy(&x0, a, 8); + std::memcpy(&x1, a + n - 8, 8); + std::memcpy(&y0, b, 8); + std::memcpy(&y1, b + n - 8, 8); + return x0 == y0 && x1 == y1; + } + if (n >= 4) + { + std::uint32_t x0 = 0; + std::uint32_t x1 = 0; + std::uint32_t y0 = 0; + std::uint32_t y1 = 0; + std::memcpy(&x0, a, 4); + std::memcpy(&x1, a + n - 4, 4); + std::memcpy(&y0, b, 4); + std::memcpy(&y1, b + n - 4, 4); + return x0 == y0 && x1 == y1; + } + if (n >= 2) + { + std::uint16_t x0 = 0; + std::uint16_t x1 = 0; + std::uint16_t y0 = 0; + std::uint16_t y1 = 0; + std::memcpy(&x0, a, 2); + std::memcpy(&x1, a + n - 2, 2); + std::memcpy(&y0, b, 2); + std::memcpy(&y1, b + n - 2, 2); + return x0 == y0 && x1 == y1; + } + return n == 0 || *a == *b; +} + /// build the table of a large object inline void build_object_index(document_data& d, node* obj) { @@ -3253,6 +3467,27 @@ inline const node* find_indexed(const document_data& d, const node* obj, const c const document_data::object_index& ix = d.indexes[obj->extra - 1u]; const std::uint32_t* const table = d.index_slots.data() + ix.start; std::size_t slot = conditional_static_cast(key_hash(key, n)) & ix.mask; + const auto* const k_bytes = reinterpret_cast(key); // NOLINT(cppcoreguidelines-pro-type-reinterpret-cast) + // (the loop for longer keys comes first, as the code is laid out; it is not the more common case) + if (NLOHMANN_VIEW_UNLIKELY(n <= 16)) + { + // (up to 16 bytes, the bytes are compared as two words, without a call) + for (std::size_t distance = 0; distance <= index_max_displacement; ++distance) + { + const std::uint32_t s = table[slot]; + if (s == 0) + { + return nullptr; + } + const node* const k = obj + s; + if (k->len == n && same_short_bytes(reinterpret_cast(d.str(*k)), k_bytes, n)) // NOLINT(cppcoreguidelines-pro-type-reinterpret-cast) + { + return k; + } + slot = (slot + 1) & ix.mask; + } + return nullptr; + } for (std::size_t distance = 0; distance <= index_max_displacement; ++distance) { const std::uint32_t s = table[slot]; @@ -3421,6 +3656,10 @@ const node* element_at(const document_data& d, const node* array, std::size_t id { return e + idx; // a growable block: one link per element } + if (array->next == array->len + 1u && (!nav::editable() || (array->flags & node_flags::moved) == 0)) + { + return e + idx; // no element has a subtree: the elements are the nodes after the array + } for (std::size_t i = 0; i < idx; ++i) { e = document_data::after(e); @@ -3436,6 +3675,10 @@ const node* last_child(const document_data& d, const node* container) noexcept const std::size_t value_offset = container->kind == static_cast(value_t::object) ? 1 : 0; const node* const end = navigation::end(d, container); const node* last = navigation::first(d, container); + if (value_offset == 0 && container->next == container->len + 1u && (!navigation::editable() || (container->flags & node_flags::moved) == 0)) + { + return last + (container->len - 1u); // an array without subtrees: the elements are the nodes after it + } for (const node* c = document_data::after(last + value_offset); c != end; c = document_data::after(c + value_offset)) { last = c; @@ -5301,6 +5544,9 @@ NLOHMANN_JSON_NAMESPACE_END #include // max #include // size_t +#ifndef JSON_NO_IO + #include // FILE, fread +#endif #include // istream, streambuf #include // basic_string, char_traits, string #include // decay, integral_constant, is_array, is_const, is_integral, is_lvalue_reference, is_pointer, is_same, remove_cv, remove_extent, remove_pointer, remove_reference @@ -5446,6 +5692,37 @@ inline void append_stream(std::string& buf, std::istream& is) is.clear(is.rdstate() | std::ios::eofbit); } +/// a pointer to a C stream (never with JSON_NO_IO, which leaves out ) +template +struct is_c_stream +#ifndef JSON_NO_IO + : std::is_same +#else + : std::false_type +#endif +{}; + +#ifndef JSON_NO_IO +/// append the rest of a C stream: in blocks with fread, not character by +/// character (the library's file adapter reads with fgetc); +/// f is not null +inline void append_file(std::string& buf, std::FILE* f) +{ + for (;;) + { + const std::size_t used = buf.size(); + const std::size_t room = (std::max)(buf.capacity() - used, std::size_t{4096}); + buf.resize(used + room); + const std::size_t got = std::fread(&buf[used], 1, room, f); + buf.resize(used + got); + if (got < room) + { + break; // end of file, or an error (as fgetc, which returns EOF for both) + } + } +} +#endif + } // namespace view } // namespace detail NLOHMANN_JSON_NAMESPACE_END @@ -5604,20 +5881,21 @@ class view_item view_item(const iterator& it, std::size_t index) : m_it(it) - { - if (!it.is_object_iterator()) - { - m_index = std::to_string(index); - } - } + , m_position(index) + {} - /// the member key, or the element index for arrays + /// the member key, or the element index for arrays (as text made on the + /// first call, so that iterating over the values pays nothing for it) string_view_t key() const { if (m_it.is_object_iterator()) { return m_it.key(); } + if (m_index.empty()) + { + m_index = std::to_string(m_position); + } return string_view_t(m_index.data(), m_index.size()); } @@ -5640,7 +5918,8 @@ class view_item private: iterator m_it; - std::string m_index{}; // NOLINT(readability-redundant-member-init) + std::size_t m_position; ///< the index of an array element + mutable std::string m_index{}; ///< the text of m_position, once key() was called // NOLINT(readability-redundant-member-init) }; /// the range returned by basic_json_view::items() @@ -5734,7 +6013,7 @@ NLOHMANN_JSON_NAMESPACE_END -#include // max +#include // array #include // size_t #include // int64_t, uint8_t #include // string @@ -5774,8 +6053,9 @@ BasicJsonType materialize(const document_data& d, const node* n) using nav = navigation; // the open containers: their next element (or key of the next member), - // their end, and whether they are objects; sized once from the - // document's deepest nesting (for a subtree an upper bound) + // their end, and whether they are objects; for the first 16 levels on the + // stack, else sized once from the document's deepest nesting (for a + // subtree an upper bound) struct frame { const node* pos; @@ -5783,10 +6063,43 @@ BasicJsonType materialize(const document_data& d, const node* n) bool object; }; + // a scalar needs neither the handler nor the stack of open containers + switch (static_cast(n->kind)) + { + case value_t::string: + return BasicJsonType(string_t(d.str(*n), n->len)); + case value_t::number_integer: + return BasicJsonType(static_cast(static_cast(integer_bits(*n)))); + case value_t::number_unsigned: + return BasicJsonType(static_cast(integer_bits(*n))); + case value_t::number_float: + return BasicJsonType(float_value(d, *n)); + case value_t::boolean: + return BasicJsonType((n->flags & node_flags::is_true) != 0); + case value_t::null: + return BasicJsonType(nullptr); + case value_t::object: + case value_t::array: + case value_t::binary: + case value_t::discarded: + default: + break; + } + BasicJsonType result; sax_t sax(result, true); const string_t no_token{}; - std::vector open((std::max)(d.max_depth, std::size_t{1})); + // (shallow subtrees on the stack: a call for a small value then allocates only for the value) + std::array shallow; // NOLINT(cppcoreguidelines-pro-type-member-init,hicpp-member-init): filled as containers open + std::vector deep{}; + frame* open = shallow.data(); + std::size_t capacity = shallow.size(); + if (d.max_depth > capacity) + { + deep.resize(d.max_depth); + open = deep.data(); + capacity = deep.size(); + } std::size_t depth = 0; for (;;) { @@ -5806,9 +6119,13 @@ BasicJsonType materialize(const document_data& d, const node* n) { sax.start_array(n->len); } - if (NLOHMANN_VIEW_UNLIKELY(depth == open.size())) + if (NLOHMANN_VIEW_UNLIKELY(depth == capacity)) { - open.resize(depth * 2); // (deeper than the recorded nesting, e.g. after edits) + // (deeper than the recorded nesting, e.g. after edits) + deep.assign(open, open + depth); + deep.resize(depth * 2); + open = deep.data(); + capacity = deep.size(); } open[depth++] = frame{nav::first(d, n), nav::end(d, n), object}; break; @@ -7708,7 +8025,7 @@ class basic_json_view } detail::view::throw_type_error(305, "cannot use operator[] with a numeric argument with ", type_name()); } - return idx < m_node->len ? basic_json_view(m_doc, navigation::value(detail::view::element_at(*m_doc, m_node, idx))) : basic_json_view(); + return idx < m_node->len ? element(idx) : basic_json_view(); } /// any other integer type (int, unsigned, long, std::int64_t, ...; a @@ -7771,7 +8088,7 @@ class basic_json_view { detail::view::throw_out_of_range(401, detail::concat("array index ", std::to_string(idx), " is out of range")); } - return basic_json_view(m_doc, navigation::value(detail::view::element_at(*m_doc, m_node, idx))); + return element(idx); } /// any other integer type, see operator[]; negative values are out of @@ -7805,7 +8122,13 @@ class basic_json_view string_t value(string_view_t key, const char* default_value) const { - return value(key, string_t(default_value)); + if (NLOHMANN_VIEW_UNLIKELY(!is_object())) + { + detail::view::throw_type_error(306, "cannot use value() with ", type_name()); + } + // (the default is made only if there is no such member) + const basic_json_view r = lookup(key); + return r.is_discarded() ? string_t(default_value) : r.template get(); } /// the value a JSON pointer refers to converted to T, or the default @@ -7824,7 +8147,12 @@ class basic_json_view string_t value(const json_pointer& ptr, const char* default_value) const { - return value(ptr, string_t(default_value)); + if (NLOHMANN_VIEW_UNLIKELY(!is_structured())) + { + detail::view::throw_type_error(306, "cannot use value() with ", type_name()); + } + const basic_json_view r = detail::view::resolve_pointer(*this, detail::json_pointer_access::reference_tokens(ptr), detail::view::pointer_mode::value); + return r.is_discarded() ? string_t(default_value) : r.template get(); } /// the first element or member value; a primitive value itself. Throws @@ -7846,7 +8174,8 @@ class basic_json_view { if (is_structured() && m_node->len != 0) { - return basic_json_view(m_doc, navigation::value(detail::view::last_child(*m_doc, m_node) + (is_object() ? 1 : 0))); + return unedited() ? basic_json_view(m_doc, detail::view::last_child(*m_doc, m_node) + (is_object() ? 1 : 0)) + : basic_json_view(m_doc, navigation::value(detail::view::last_child(*m_doc, m_node) + (is_object() ? 1 : 0))); } return front(); } @@ -7863,7 +8192,7 @@ class basic_json_view { return end(); } - const node* const k = detail::view::find_member(*m_doc, m_node, key.data(), key.size()); + const node* const k = find_key(key); return k != nullptr ? iterator(m_doc, k, true) : end(); } @@ -7880,7 +8209,7 @@ class basic_json_view /// whether this is an object with a member with this key bool contains(string_view_t key) const { - return is_object() && detail::view::find_member(*m_doc, m_node, key.data(), key.size()) != nullptr; + return is_object() && find_key(key) != nullptr; } bool contains(const char* key) const @@ -8040,7 +8369,14 @@ class basic_json_view // the compact writer keeps 64 bytes of slack, so that it does not grow // the buffer just before the end const std::size_t estimate = source_extent() + (style.pretty ? source_extent() / 2 : 0) + 160; - detail::view::view_serializer(*m_doc, out, estimate, style).dump(m_node); + if (unedited()) + { + detail::view::view_serializer(*m_doc, out, estimate, style).dump(m_node); + } + else + { + detail::view::view_serializer(*m_doc, out, estimate, style).dump(m_node); + } return out; } @@ -8120,7 +8456,7 @@ class basic_json_view { return BasicJsonType(value_t::discarded); } - return detail::view::materialize(*m_doc, m_node); + return unedited() ? detail::view::materialize(*m_doc, m_node) : detail::view::materialize(*m_doc, m_node); } /// byte offset of this value in the source text (for strings: of the @@ -8198,10 +8534,50 @@ class basic_json_view /// (object required) NLOHMANN_VIEW_ALWAYS_INLINE basic_json_view lookup(string_view_t key) const noexcept { + if (unedited()) + { + const node* const k = detail::view::find_member(*m_doc, m_node, key.data(), key.size()); + return k != nullptr ? basic_json_view(m_doc, k + 1) : basic_json_view(); + } const node* const k = detail::view::find_member(*m_doc, m_node, key.data(), key.size()); return k != nullptr ? basic_json_view(m_doc, navigation::value(k + 1)) : basic_json_view(); } + /// whether the document has not been edited yet: until the first edit, its index is walked like that of a + /// read-only document (nothing is moved or linked), which is cheaper + NLOHMANN_VIEW_ALWAYS_INLINE bool unedited() const noexcept + { + return !navigation::editable() || m_doc->edits == nullptr; + } + + /// the member of this object with @a key (the last one if the key occurs more than once), or nullptr + NLOHMANN_VIEW_ALWAYS_INLINE const node* find_key(string_view_t key) const + { + return find_key(key, std::integral_constant {}); + } + + NLOHMANN_VIEW_ALWAYS_INLINE const node* find_key(string_view_t key, std::true_type /*editable*/) const + { + // (an editable document without edits is looked up as one that cannot be edited) + return unedited() ? detail::view::find_member(*m_doc, m_node, key.data(), key.size()) + : detail::view::find_member(*m_doc, m_node, key.data(), key.size()); + } + + NLOHMANN_VIEW_ALWAYS_INLINE const node* find_key(string_view_t key, std::false_type /*editable*/) const + { + return detail::view::find_member(*m_doc, m_node, key.data(), key.size()); + } + + /// the element of an array at an index below its size + NLOHMANN_VIEW_ALWAYS_INLINE basic_json_view element(size_type idx) const + { + if (unedited()) + { + return basic_json_view(m_doc, detail::view::element_at(*m_doc, m_node, idx)); + } + return basic_json_view(m_doc, navigation::value(detail::view::element_at(*m_doc, m_node, idx))); + } + // --- get() dispatch --- bool get_impl(detail::view::value_tag /*unused*/, detail::priority_tag<2> /*unused*/) const @@ -8941,7 +9317,8 @@ class basic_json_document // NOLINTBEGIN(readability-avoid-nested-conditional-operator): a constant expression of C++11 append_input_impl(buf, std::forward(input), std::integral_constant < int, detail::is_contiguous_byte_container::value ? 0 : std::is_base_of::value ? 1 - : detail::view::is_adapter_input::value ? 2 : 3 > {}); + : detail::view::is_adapter_input::value ? 2 + : detail::view::is_c_stream::value ? 4 : 3 > {}); // NOLINTEND(readability-avoid-nested-conditional-operator) } @@ -8963,6 +9340,19 @@ class basic_json_document detail::view::append_adapter(buf, std::move(input.ia)); } +#ifndef JSON_NO_IO + static void append_input_impl(std::string& buf, std::FILE* file, std::integral_constant /*C stream*/) + { + if (file == nullptr) + { + // (as before: the library's adapter decides what a null stream yields) + detail::view::append_adapter(buf, detail::input_adapter(file)); + return; + } + detail::view::append_file(buf, file); + } +#endif + template static void append_input_impl(std::string& buf, T&& input, std::integral_constant /*other*/) { diff --git a/tests/src/unit-json_view.cpp b/tests/src/unit-json_view.cpp index caadee629..e87162d98 100644 --- a/tests/src/unit-json_view.cpp +++ b/tests/src/unit-json_view.cpp @@ -185,6 +185,33 @@ TEST_CASE("json_view") // duplicate keys: the last value, at the position of the first key CHECK(materialized(R"({"a":1,"b":2,"a":3})") == json::parse(R"({"a":1,"b":2,"a":3})")); CHECK(materialized(R"({"a":1,"b":2,"a":3})").dump() == R"({"a":3,"b":2})"); + // scalars (made without the handler), and nesting around the depth up to which the stack of open containers is local + for (const char* scalar : + {"null", "true", "false", "0", "-7", "18446744073709551615", "1.5e300", "-0.0", "\"\"", "\"short\"", "\"a string that is longer than fifteen bytes\"", R"("\u00e4\n")" + }) + { + CAPTURE(scalar) + CHECK(materialized(scalar) == json::parse(scalar)); + const std::string in_array = std::string("[1,") + scalar + "]"; + const json_document d = json_document::parse(in_array); + CHECK(d.root()[1].materialize() == json::parse(scalar)); + CHECK(d.root()[1].get() == json::parse(scalar)); + } + for (const std::size_t levels : + { + std::size_t{1}, std::size_t{15}, std::size_t{16}, std::size_t{17}, std::size_t{32}, std::size_t{33} + }) + { + CAPTURE(levels) + const std::string nested = std::string(levels, '[') + R"({"k":[1,{"j":2}]})" + std::string(levels, ']'); + CHECK(materialized(nested) == json::parse(nested)); + std::string wide = R"({"a":)"; + wide += nested; + wide += R"(,"b":)"; + wide += nested; + wide += "}"; + CHECK(materialized(wide) == json::parse(wide)); + } // very deep nesting (iterative, as parse()) const std::string deep = std::string(100000, '[') + std::string(100000, ']'); // (one check per step, so that a failure shows which step it is) @@ -410,6 +437,31 @@ TEST_CASE("json_view") std::istringstream rvalue_source("[1]"); CHECK(json_document::parse_copy(std::move(rvalue_source)).owns_source()); + // a C stream is read in blocks as well (more than one block, and a + // text whose length is a multiple of the block size) + for (const std::size_t extra : + { + std::size_t{0}, std::size_t{1} + }) + { + std::FILE* long_file = std::tmpfile(); // NOLINT(cppcoreguidelines-owning-memory) + if (long_file != nullptr) + { + std::string padded = text; + padded.append(((4096 - (padded.size() % 4096)) % 4096) + extra, ' '); + CHECK(std::fwrite(padded.data(), 1, padded.size(), long_file) == padded.size()); + CHECK(std::fseek(long_file, 0, SEEK_SET) == 0); + const json_document from_long_file = json_document::parse(long_file); + CHECK(from_long_file.source() == padded); + CHECK(from_long_file.root().materialize() == json::parse(text)); + CHECK(std::fclose(long_file) == 0); // NOLINT(cppcoreguidelines-owning-memory) + } + } + { + json_document from_null; + CHECK_THROWS_AS(from_null = json_document::parse(static_cast(nullptr)), json::parse_error&); + } + // a stream without a buffer is an empty input std::istream no_buffer(nullptr); CHECK(json_document::parse(no_buffer, false).is_discarded()); @@ -1006,6 +1058,49 @@ TEST_CASE("json_view element access and iteration") #endif } + SECTION("arrays whose elements have no subtrees: index and back()") + { + // (the elements are adjacent in the index; an array, an object, or an + // empty container anywhere makes the lookup skip element by element) + const std::vector texts = + { + "[]", "[1]", "[1,2,3]", "[1,\"two\",3.5,true,null,-7,18446744073709551615]", "[[]]", "[{}]", "[1,[2],3]", + "[1,2,{\"a\":3}]", "[[1,2],3,4]", "[1,2,3,[4,5,6,7,8],9,10,11,12]", "[{\"a\":[1]},2]", R"(["a\u00e4","b"])" + }; + for (const std::string& text : texts) + { + CAPTURE(text) + const json_document d = json_document::parse(text); + const json_view v = d.root(); + const json expected = json::parse(text); + for (std::size_t i = 0; i < expected.size(); ++i) + { + CHECK(v[i].materialize() == expected[i]); + CHECK(v.at(i).materialize() == expected[i]); + } + CHECK(v[expected.size()].is_discarded()); + if (!expected.empty()) + { + CHECK(v.back().materialize() == expected.back()); + } + } + // a long array (the direct lookup is not limited to short ones), also with a container at its end + std::string text = "[0"; + for (int i = 1; i < 1000; ++i) + { + text += "," + std::to_string(i); + } + const json_document scalars = json_document::parse(text + "]"); + CHECK(scalars.root()[999].get() == 999); + CHECK(scalars.root()[500].get() == 500); + CHECK(scalars.root().at(1).get() == 1); + CHECK(scalars.root().back().get() == 999); + const json_document container_last = json_document::parse(text + ",[1000]]"); + CHECK(container_last.root()[999].get() == 999); + CHECK(container_last.root()[1000][0].get() == 1000); + CHECK(container_last.root().back()[0].get() == 1000); + } + SECTION("iterators") { const json_document d = json_document::parse(R"({"x":[1,{"y":2}],"z":null})"); @@ -1045,6 +1140,28 @@ TEST_CASE("json_view element access and iteration") CHECK((*first++).key() == "0"); CHECK((*first).key() == "1"); CHECK(++first == items.end()); + + // the keys of array elements are their indexes, whether or not they are asked for + std::string numbers = "[0"; + for (int i = 1; i < 1234; ++i) + { + numbers += "," + std::to_string(i); + } + const json_document array = json_document::parse(numbers + "]"); + std::size_t position = 0; + for (const auto& item : array.root().items()) + { + CHECK(item.value().get() == position); + if (position % 3 != 0) // (not for every item: values alone do not make the key) + { + CHECK(item.key() == std::to_string(position)); + CHECK(item.key().data() == item.key().data()); // (the same text every time) + const json_view::item copy = item; + CHECK(copy.key() == std::to_string(position)); + } + ++position; + } + CHECK(position == 1234); #ifdef JSON_HAS_CPP_17 std::string pairs; for (const auto [key, value] : d.root().items()) @@ -1383,7 +1500,17 @@ TEST_CASE("json_view values") CHECK(exception_of([&] { static_cast(v.value("s", 0)); }) == without_path(exception_of([&] { static_cast(j.value("s", 0)); }))); CHECK(exception_of([&] { static_cast(v["n"].value("x", 0)); }) == without_path(exception_of([&] { static_cast(j["n"].value("x", 0)); }))); CHECK(exception_of([&] { static_cast(v["n"].value(json::json_pointer("/x"), 0)); }) == without_path(exception_of([&] { static_cast(j["n"].value(json::json_pointer("/x"), 0)); }))); + // string defaults: the member is not a string, or the value is neither an object nor an array + CHECK(exception_of([&] { static_cast(v.value("n", "default")); }) == without_path(exception_of([&] { static_cast(j.value("n", "default")); }))); + CHECK(exception_of([&] { static_cast(v["n"].value("x", "default")); }) == without_path(exception_of([&] { static_cast(j["n"].value("x", "default")); }))); + CHECK(exception_of([&] { static_cast(v.value(json::json_pointer("/n"), "default")); }) == without_path(exception_of([&] { static_cast(j.value(json::json_pointer("/n"), "default")); }))); + CHECK(exception_of([&] { static_cast(v["s"].value(json::json_pointer("/x"), "default")); }) == without_path(exception_of([&] { static_cast(j["s"].value(json::json_pointer("/x"), "default")); }))); #endif + // (defaults and members longer than the short string optimization) + CHECK(v.value("s", "a default longer than fifteen characters") == "text"); + CHECK(v.value("missing", "a default longer than fifteen characters") == "a default longer than fifteen characters"); + CHECK(v.value(json::json_pointer("/s"), "a default longer than fifteen characters") == "text"); + CHECK(v.value(json::json_pointer("/zz"), "a default longer than fifteen characters") == "a default longer than fifteen characters"); } } diff --git a/tests/src/unit-json_view_builder.cpp b/tests/src/unit-json_view_builder.cpp index b43af739d..6935981fb 100644 --- a/tests/src/unit-json_view_builder.cpp +++ b/tests/src/unit-json_view_builder.cpp @@ -523,6 +523,58 @@ TEST_CASE("json_view builder: strings across vector blocks") } } +TEST_CASE("json_view builder: escapes between runs of text") +{ + // The text between escapes is copied 16 bytes at a time where there are + // that many bytes left, and with the UTF-8 check where there is + // non-ASCII text. Runs of every length from 0 to 40 between valid and + // invalid escapes, UTF-8 sequences, and control characters, in strings + // that are cut at all places near the end of the text. + const std::array valid = {{"\\n", "\\\"", "\\\\", "\\/", "\\t", "\\u00e4", "\\u20ac", "\\ud83d\\ude00", "\\u0000"}}; + const std::array invalid = {{"\\x", "\\ud800", "\\udc00", "\\u12G4", "\\u12", "\\ud83dx", "\\ud83d\\u0041", "\\", "\x01"}}; + const std::array utf8 = {{"\xc3\xa4", "\xe2\x82\xac", "\xf0\x9f\x98\x80", "\xc3", "\xff", "\x80"}}; + std::mt19937 rng(2026); // NOLINT(cert-msc32-c,cert-msc51-cpp,bugprone-random-generator-seed): reproducible + for (std::size_t run = 0; run <= 40; ++run) + { + for (int i = 0; i < 60; ++i) + { + std::string content = std::string(rng() % 20, 'a'); + const auto pieces = 1 + (rng() % 6); + for (unsigned k = 0; k < pieces; ++k) + { + content += valid[rng() % valid.size()]; + content += std::string(k == 0 ? run : rng() % 41, static_cast('b' + (rng() % 20))); + switch (rng() % 8) + { + case 0: + content += utf8[rng() % 3]; + break; + case 1: + content += utf8[rng() % utf8.size()]; + break; + case 2: + content += invalid[rng() % invalid.size()]; + break; + default: + break; + } + } + check_string(content); + check_string(content.substr(0, content.size() - (rng() % 4))); + } + } + // the text before the first escape, and runs and escapes at the end of the text + for (std::size_t before = 0; before <= 40; ++before) + { + for (const char* escape : valid) + { + check_string(std::string(before, 'a') + escape); + check_string(std::string(before, 'a') + escape + std::string(before % 19, 'z')); + check_string(std::string(before, 'a') + escape + escape + std::string(before % 19, 'z')); + } + } +} + TEST_CASE("json_view node integer bits") { using nlohmann::detail::view::integer_bits; diff --git a/tests/src/unit-json_view_dump.cpp b/tests/src/unit-json_view_dump.cpp index 850dba3c7..f3ccc004e 100644 --- a/tests/src/unit-json_view_dump.cpp +++ b/tests/src/unit-json_view_dump.cpp @@ -21,6 +21,7 @@ using nlohmann::ordered_json_view; using json_view_test::generator; using json_view_test::has_duplicate_keys; +#include #include #include #include @@ -29,6 +30,7 @@ using json_view_test::has_duplicate_keys; #include #include #include +#include #include #include #include @@ -725,6 +727,152 @@ TEST_CASE("json_view large objects") CHECK(without_collisions.memory_usage() >= with_collisions.memory_usage() + (table_size * sizeof(std::uint32_t))); } + SECTION("keys that differ in their last bytes are spread over the table") + { + // The hash must spread the usual families of keys (numbered names, hex + // strings, dates, short names): a key farther than index_max_displacement + // from its home slot would leave the object without a table. The keys + // are placed with linear probing, as the table does. + const auto max_displacement = [](const std::vector& keys) + { + std::size_t slots = 16; + while (slots < 2 * keys.size()) + { + slots *= 2; + } + std::vector used(slots, false); + std::size_t farthest = 0; + for (const std::string& key : keys) + { + std::size_t i = nlohmann::detail::conditional_static_cast(nlohmann::detail::view::key_hash(key.data(), key.size())) & (slots - 1); + std::size_t distance = 0; + while (used[i]) + { + i = (i + 1) & (slots - 1); + ++distance; + } + used[i] = true; + farthest = (std::max)(farthest, distance); + } + return farthest; + }; + // the keys of a printf-like pattern with one %d, %0Nd or %0Nx, formatted with a stream + // (a format string that is not a literal is a Flawfinder error) + const auto numbered = [](const char* format, int count, int step) + { + const std::string pattern(format); + const std::size_t percent = pattern.find('%'); + std::size_t conversion = percent + 1; + std::size_t width = 0; + while (pattern[conversion] >= '0' && pattern[conversion] <= '9') + { + width = (width * 10) + static_cast(pattern[conversion] - '0'); + ++conversion; + } + const bool hex = pattern[conversion] == 'x'; + std::vector keys; + for (int i = 0; i < count; ++i) + { + std::ostringstream key; + key << pattern.substr(0, percent) << std::setfill('0') << std::setw(static_cast(width)) + << (hex ? std::hex : std::dec) << (i * step) << pattern.substr(conversion + 1); + keys.push_back(key.str()); + } + return keys; + }; + for (const char* format : + {"key%d", "%d", "item_%04d", "a%05d", "%03dxyz", "id%d", "user_%d", "%d_suffix", "0x%04x", "A%07d", "12345678%d", "1234567%d", "123456789%d", + "configuration_parameter_value_%04d_extra", "2026-01-%02dT00:00:00Z" + }) + { + CAPTURE(format) + CHECK(max_displacement(numbered(format, std::string(format).find("%02d") != std::string::npos ? 60 : 1000, 1)) < nlohmann::detail::view::index_max_displacement); // NOLINT(abseil-string-find-str-contains) + CHECK(max_displacement(numbered(format, 600, 7)) < nlohmann::detail::view::index_max_displacement); + } + std::mt19937_64 rng(7); // NOLINT(cert-msc32-c,cert-msc51-cpp,bugprone-random-generator-seed) + for (const std::size_t length : + { + std::size_t{1}, std::size_t{2}, std::size_t{3}, std::size_t{5}, std::size_t{7}, std::size_t{8}, std::size_t{9}, std::size_t{12}, std::size_t{16}, std::size_t{17}, std::size_t{33} + }) + { + CAPTURE(length) + std::set distinct; + // (there are 26 and 676 keys of one and two letters) + std::size_t wanted = 700; + if (length == 1) + { + wanted = 26; + } + else if (length == 2) + { + wanted = 500; + } + while (distinct.size() < wanted) + { + std::string key(length, 'a'); + for (char& c : key) + { + c = static_cast('a' + (rng() % 26)); + } + distinct.insert(key); + } + CHECK(max_displacement(std::vector(distinct.begin(), distinct.end())) < nlohmann::detail::view::index_max_displacement); + } + } + + SECTION("keys of every length that differ in one byte") + { + // In a large object, a key is found by comparing its bytes with those of the member the + // table points to: up to 16 bytes as two overlapping words, longer ones with memcmp. + // Keys of every length from 0 to 40, and keys that differ from them in one byte. + std::mt19937_64 rng(2026); // NOLINT(cert-msc32-c,cert-msc51-cpp,bugprone-random-generator-seed) + std::vector keys; + std::set distinct; + for (std::size_t length = 0; length <= 40; ++length) + { + for (int copy = 0; copy < 5; ++copy) + { + std::string key(length, 'a'); + for (char& c : key) + { + c = static_cast('a' + (rng() % 26)); + } + if (distinct.insert(key).second) + { + keys.push_back(key); + } + } + } + std::string text = "{"; + for (std::size_t i = 0; i < keys.size(); ++i) + { + text += (i != 0 ? ",\"" : "\"") + keys[i] + "\":" + std::to_string(i); + } + text += "}"; + REQUIRE(keys.size() >= 128); // (a table is built for 128 members) + const json_document d = json_document::parse(text); + const json_view v = d.root(); + for (std::size_t i = 0; i < keys.size(); ++i) + { + CAPTURE(keys[i]) + CHECK(v[keys[i]].get() == i); + CHECK(v.at(keys[i]).get() == i); + CHECK(v.contains(keys[i])); + for (std::size_t at = 0; at < keys[i].size(); ++at) + { + std::string other = keys[i]; + other[at] = other[at] == 'z' ? 'y' : 'z'; + CAPTURE(other) + CHECK(v.contains(other) == (distinct.count(other) != 0)); + } + CHECK(v.contains(keys[i] + "a") == (distinct.count(keys[i] + "a") != 0)); + if (!keys[i].empty()) + { + CHECK(v.contains(keys[i].substr(1)) == (distinct.count(keys[i].substr(1)) != 0)); + } + } + } + SECTION("shrink_to_fit releases the tables' spare capacity") { const auto make_text = [](int objects, int members) diff --git a/tests/src/unit-json_view_edit.cpp b/tests/src/unit-json_view_edit.cpp index 5c3b4396a..4a6fbe69d 100644 --- a/tests/src/unit-json_view_edit.cpp +++ b/tests/src/unit-json_view_edit.cpp @@ -661,6 +661,115 @@ TEST_CASE("json_view edits: replacing arrays and objects by scalars") CHECK(d.root().materialize() == expected); } +TEST_CASE("json_view edits: arrays whose elements have no subtrees") +{ + // such an array is indexed directly while it is in its parsed layout; edits + // that replace elements by scalars keep it, others move it + json_editable_document d = json_editable_document::parse("[10,20,30,40]"); + json expected = json::array({10, 20, 30, 40}); + const auto check = [&] + { + CHECK(d.root().size() == expected.size()); + for (std::size_t i = 0; i < expected.size(); ++i) + { + CHECK(d.root()[i].materialize() == expected[i]); + CHECK(d.root().at(i).materialize() == expected[i]); + } + CHECK(d.root()[expected.size()].is_discarded()); + CHECK(d.root().back().materialize() == expected.back()); + CHECK(d.root().dump() == expected.dump()); + }; + check(); + d.set(d.root()[1], "x"); + expected[1] = "x"; + check(); + d.set(d.root()[3], 2.5); + expected[3] = 2.5; + check(); + d.set(d.root()[2], json::array({1, 2})); + expected[2] = json::array({1, 2}); + check(); + d.push_back(d.root(), 50); + expected.push_back(50); + check(); + d.insert(d.root(), 0, true); + expected.insert(expected.begin(), true); + check(); + d.erase(d.root(), 1); + expected.erase(expected.begin() + 1); + check(); + d.set(d.root()[2], nullptr); + expected[2] = nullptr; + check(); +} + +TEST_CASE("json_view edits: materializing values nested deeper than the document was parsed") +{ + // the stack of open containers of materialize() is sized from the nesting of the parsed text; + // a value added by an edit can be deeper than that (and than the stack that is local to the call) + json_editable_document d = json_editable_document::parse("[1,[2]]"); + json expected = json::array({1, json::array({2})}); + json nested = 3; + for (int level = 0; level < 40; ++level) + { + nested = json::array({nested, json{{"k", level}}}); + } + d.push_back(d.root(), nested); + expected.push_back(nested); + CHECK(d.root().materialize() == expected); + CHECK(d.root()[2].materialize() == nested); + d.set(d.root()[1][0], nested); + expected[1][0] = nested; + CHECK(d.root().materialize() == expected); + CHECK(d.root().get() == expected); +} + +TEST_CASE("json_view edits: an editable document before and after its first edit") +{ + // until the first edit, an editable document is read like a read-only one; views made before the edit + // keep working after it + const std::string text = R"({"a":[1,2,{"x":"y"},4],"b":{"c":true,"d":null},"e":"text","f":[]})"; + const json_document read_only = json_document::parse(text); + json_editable_document d = json_editable_document::parse(text); + const auto root = d.root(); + const auto same_as_read_only = [&] + { + CHECK(root.dump() == read_only.root().dump()); + CHECK(root.dump(2) == read_only.root().dump(2)); + CHECK(root.materialize() == read_only.root().materialize()); + CHECK(root["a"][2]["x"].get_string() == "y"); + CHECK(root["a"].at(3).get() == 4); + CHECK(root["a"].back().get() == 4); + CHECK(root.contains("b")); + CHECK(!root.contains("zz")); + CHECK(root.find("e")->get_string() == "text"); + CHECK(root.find("zz") == root.end()); + CHECK(root["b"]["c"].get()); + CHECK(root.value("e", "none") == "text"); + std::size_t count = 0; + for (const auto m : root["a"]) + { + CHECK(m.materialize() == read_only.root()["a"][count].materialize()); + ++count; + } + CHECK(count == 4); + }; + same_as_read_only(); + // an edit that changes a scalar in place, then one that moves an array + d.set(root["e"], "other"); + CHECK(root["e"].get_string() == "other"); + CHECK(root["a"][2]["x"].get_string() == "y"); + CHECK(root["a"].back().get() == 4); + d.push_back(root["f"], 7); + CHECK(root["f"][0].get() == 7); + CHECK(root["f"].back().get() == 7); + CHECK(root.dump() == R"({"a":[1,2,{"x":"y"},4],"b":{"c":true,"d":null},"e":"other","f":[7]})"); + d.set(root["a"], 2, json::array({1, 2})); + CHECK(root["a"][2][1].get() == 2); + CHECK(root["a"].dump() == "[1,2,[1,2],4]"); + CHECK(root.materialize() == json::parse(R"({"a":[1,2,[1,2],4],"b":{"c":true,"d":null},"e":"other","f":[7]})")); +} + namespace { // reads of "a" in an object that repeats it: the first member wins, however the