From c8b380769cc60b3128cf92e77ba97026e76b9cdb Mon Sep 17 00:00:00 2001 From: Niels Lohmann Date: Fri, 16 Oct 2026 21:46:05 +0200 Subject: [PATCH] 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 --- 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/lookup.hpp | 8 ++++ single_include/nlohmann/json_view.hpp | 8 ++++ tests/src/unit-json_view.cpp | 43 +++++++++++++++++++ tests/src/unit-json_view_edit.cpp | 42 ++++++++++++++++++ 7 files changed, 107 insertions(+), 3 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..424e2cfa0 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 for an array none of whose elements 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/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/single_include/nlohmann/json_view.hpp b/single_include/nlohmann/json_view.hpp index 666ba7f84..f5f2fe9db 100644 --- a/single_include/nlohmann/json_view.hpp +++ b/single_include/nlohmann/json_view.hpp @@ -3421,6 +3421,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 +3440,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/tests/src/unit-json_view.cpp b/tests/src/unit-json_view.cpp index 4c0983174..b7e3ee374 100644 --- a/tests/src/unit-json_view.cpp +++ b/tests/src/unit-json_view.cpp @@ -1031,6 +1031,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]", "[\"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})"); diff --git a/tests/src/unit-json_view_edit.cpp b/tests/src/unit-json_view_edit.cpp index 5c3b4396a..da3f8a29b 100644 --- a/tests/src/unit-json_view_edit.cpp +++ b/tests/src/unit-json_view_edit.cpp @@ -661,6 +661,48 @@ 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(); +} + namespace { // reads of "a" in an object that repeats it: the first member wins, however the