From 21c8fb4318902bf90fb069fbe8e7fbdfdb46d358 Mon Sep 17 00:00:00 2001 From: Niels Lohmann Date: Fri, 9 Oct 2026 16:30:26 +0200 Subject: [PATCH] Replace a container by a scalar in constant time once it is linked Replacing an array or object that spans several nodes by a scalar looks up its parent from the root (find_parent), which is linear in the size of the document: setting every element of a 20,000 element array took more than half a second. The parent only has to switch to links once; afterward the extent of the replaced value no longer matters. Nodes that an entry of a moved sequence links to are now marked (node_flags::linked), and assign() skips the lookup for them. Signed-off-by: Niels Lohmann --- .../docs/api/basic_json_document/set.md | 5 ++- include/nlohmann/detail/view/edit.hpp | 14 +++++-- include/nlohmann/detail/view/edit_storage.hpp | 4 +- include/nlohmann/detail/view/node.hpp | 5 ++- tests/src/unit-json_view_edit.cpp | 42 +++++++++++++++++++ 5 files changed, 62 insertions(+), 8 deletions(-) diff --git a/docs/mkdocs/docs/api/basic_json_document/set.md b/docs/mkdocs/docs/api/basic_json_document/set.md index 067fa9fef..ae26ed516 100644 --- a/docs/mkdocs/docs/api/basic_json_document/set.md +++ b/docs/mkdocs/docs/api/basic_json_document/set.md @@ -131,7 +131,10 @@ document") if `target`/`object`/`array` is a [discarded](../basic_json_view/is_d 1. Linear in the size of `value` (encoding it into the document's storage): constant for a scalar, linear in the number of nested values for an array or object. If `target` is itself an array or object that spans more than one node in its parent's original, unedited layout, and `value` is a scalar, replacing it additionally costs time - linear in the number of elements of that parent, the *first* time -- see [Notes](#notes). + linear in the size of the document, the *first* time (the parent of `target` is looked up from + [`root()`](root.md), and then switches to links) -- see [Notes](#notes). Once the parent has links, `target` is + replaced in constant time: setting every element of a large array one after the other is linear overall. To avoid + the lookup altogether, use 3. (or 2. for an object), which know the parent. 2. Linear in the number of members of `object`, to find an existing member with `key`, plus the complexity of 1. for `value`. 3. Constant, plus the complexity of 1. for `value`. diff --git a/include/nlohmann/detail/view/edit.hpp b/include/nlohmann/detail/view/edit.hpp index b41999d41..7a9daa6bc 100644 --- a/include/nlohmann/detail/view/edit.hpp +++ b/include/nlohmann/detail/view/edit.hpp @@ -348,9 +348,10 @@ class editor /// turn a null into an empty array/object in place static void become_empty(node* n, value_t k) noexcept { + const std::uint8_t linked = n->flags & node_flags::linked; *n = node{}; n->kind = static_cast(k); - n->flags = node_flags::is_new; + n->flags = static_cast(node_flags::is_new | linked); n->next = 1; } @@ -358,13 +359,17 @@ class editor /// include slot (if known). void assign(node* slot, const encoded& e, node* parent, bool parent_known) { + // an entry of a moved sequence links to the slot: it can take any extent + const std::uint8_t linked = slot->flags & node_flags::linked; if (e.region == nullptr) { - if (is_container(*slot) && slot->next > 1 && slot != m_doc.tape) + if (is_container(*slot) && slot->next > 1 && slot != m_doc.tape && linked == 0) { // The slot spans its old elements in the enclosing sequence, but // a scalar is one node: the enclosing container first switches to - // links (then the extent of the slot no longer matters). + // links (then the extent of the slot no longer matters). Looking + // for the container is linear in the size of the document, so + // links (which are marked in the slot) avoid it. node* const p = parent_known ? parent : find_parent(m_doc, slot); if (p != nullptr && ((p->flags & node_flags::moved) == 0 || moved_capacity(m_doc, p) == 0)) { @@ -372,6 +377,7 @@ class editor } } *slot = e.scalar; + slot->flags = static_cast(slot->flags | linked); return; } // an array/object: the slot keeps its extent (so that the enclosing @@ -394,7 +400,7 @@ class editor slot->len = r->len; slot->next = extent; // (set_moved() adds the moved flag to a slot that does not have it yet) - slot->flags = was_moved ? static_cast(node_flags::moved | node_flags::is_new) : std::uint8_t{0}; + slot->flags = static_cast((was_moved ? node_flags::moved | node_flags::is_new : 0) | linked); set_moved(m_doc, slot, e.region, 0); edit_state_of(m_doc).regions[e.region] = slot; } diff --git a/include/nlohmann/detail/view/edit_storage.hpp b/include/nlohmann/detail/view/edit_storage.hpp index e6a4c4971..58587de3b 100644 --- a/include/nlohmann/detail/view/edit_storage.hpp +++ b/include/nlohmann/detail/view/edit_storage.hpp @@ -169,7 +169,7 @@ inline node* block_of(document_data& d, node* n, std::size_t extra) set_moved(d, n, nh, cap); return nh; } - reserve_moved(d); // (so that set_moved() below cannot throw) + reserve_moved(d); // (so that set_moved() below cannot throw: the links are marked before) const bool object = n->kind == static_cast(value_t::object); const std::size_t used = 1 + (static_cast(n->len) * (object ? 2 : 1)); const std::size_t cap = used + extra; @@ -185,7 +185,7 @@ inline node* block_of(document_data& d, node* n, std::size_t extra) { *o++ = *c++; // the key } - make_link(*o, document_data::deref(c)); + make_link(*o, const_cast(document_data::deref(c))); // NOLINT(cppcoreguidelines-pro-type-const-cast): the nodes belong to the document ++o; c = document_data::after(c); } diff --git a/include/nlohmann/detail/view/node.hpp b/include/nlohmann/detail/view/node.hpp index 82774dac8..35e83fbe5 100644 --- a/include/nlohmann/detail/view/node.hpp +++ b/include/nlohmann/detail/view/node.hpp @@ -38,6 +38,7 @@ struct node_flags static constexpr std::uint8_t is_true = 4; ///< boolean value static constexpr std::uint8_t moved = 8; ///< array/object: the elements live in a separate sequence (editable documents) static constexpr std::uint8_t is_new = 16; ///< written by an edit: no source position + static constexpr std::uint8_t linked = 32; ///< an entry of a moved sequence links to this value (editable documents): its extent in the parsed layout no longer matters }; /// kind of an entry of an edited sequence that stands for a value stored @@ -72,8 +73,10 @@ NLOHMANN_VIEW_ALWAYS_INLINE const node* link_target(const node& n) noexcept return t; } -inline void make_link(node& n, const node* target) noexcept +/// let the entry n stand for the value at target (and mark the value) +inline void make_link(node& n, node* target) noexcept { + target->flags = static_cast(target->flags | node_flags::linked); n = node{}; n.kind = kind_link; std::memcpy(reinterpret_cast(&n) + 8, static_cast(&target), sizeof(const node*)); // NOLINT(cppcoreguidelines-pro-type-reinterpret-cast) diff --git a/tests/src/unit-json_view_edit.cpp b/tests/src/unit-json_view_edit.cpp index 4116462b5..d58d4b3ca 100644 --- a/tests/src/unit-json_view_edit.cpp +++ b/tests/src/unit-json_view_edit.cpp @@ -745,3 +745,45 @@ TEST_CASE("json_view edits: the size of a text arena") CHECK_THROWS_WITH_AS(text_capacity(limit, limit, 1), "[json.exception.out_of_range.416] edits of 4 GiB or more are not supported by json_document", json::out_of_range&); } #endif + +TEST_CASE("json_view edits: replacing arrays and objects by scalars") +{ + // the first assignment switches the parent to links; the later ones do + // not need to look for the parent again + std::string text = "["; + json expected = json::array(); + for (int i = 0; i < 300; ++i) + { + text += (i != 0 ? ",[" : "[") + std::to_string(i) + ",{\"k\":" + std::to_string(i) + "}]"; + expected.push_back(json::array({i, json{{"k", i}}})); + } + text += ']'; + json_editable_document d = json_editable_document::parse(text); + CHECK(d.root().dump() == expected.dump()); + for (int i = 0; i < 300; i += 2) + { + d.set(d.root()[static_cast(i)], i); + expected[static_cast(i)] = i; + } + CHECK(d.root().dump() == expected.dump()); + for (int i = 1; i < 300; i += 2) // (elements that are still in the parsed layout of their parent) + { + d.set(d.root()[static_cast(i)][1], "x"); // replaces an object + expected[static_cast(i)][1] = "x"; + } + CHECK(d.root().dump() == expected.dump()); + + // new values, and values in new values + d.push_back(d.root(), json::parse(R"([[1,2],{"a":[3]}])")); + expected.push_back(json::parse(R"([[1,2],{"a":[3]}])")); + d.set(d.root()[300][0], 7); + expected[300][0] = 7; + d.insert(d.root(), 0, json::array({1, 2})); + expected.insert(expected.begin(), json::array({1, 2})); + d.set(d.root()[0], nullptr); + expected[0] = nullptr; + d.set(d.root()[301], 5); + expected[301] = 5; + CHECK(d.root().dump() == expected.dump()); + CHECK(d.root().materialize() == expected); +}