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 <mail@nlohmann.me>
This commit is contained in:
Niels Lohmann committed 2026-10-09 16:30:59 +02:00
1 parent 79a51feb44
commit 21c8fb4318
5 files changed
+62 -8

No files matched your search

@@ -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`.
+10 -4
View File
@@ -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<std::uint8_t>(k);
n->flags = node_flags::is_new;
n->flags = static_cast<std::uint8_t>(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<std::uint8_t>(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<std::uint8_t>(node_flags::moved | node_flags::is_new) : std::uint8_t{0};
slot->flags = static_cast<std::uint8_t>((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;
}
@@ -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<std::uint8_t>(value_t::object);
const std::size_t used = 1 + (static_cast<std::size_t>(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<node*>(document_data::deref(c))); // NOLINT(cppcoreguidelines-pro-type-const-cast): the nodes belong to the document
++o;
c = document_data::after(c);
}
+4 -1
View File
@@ -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<std::uint8_t>(target->flags | node_flags::linked);
n = node{};
n.kind = kind_link;
std::memcpy(reinterpret_cast<unsigned char*>(&n) + 8, static_cast<const void*>(&target), sizeof(const node*)); // NOLINT(cppcoreguidelines-pro-type-reinterpret-cast)
+42
View File
@@ -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<std::size_t>(i)], i);
expected[static_cast<std::size_t>(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<std::size_t>(i)][1], "x"); // replaces an object
expected[static_cast<std::size_t>(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);
}