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 <mail@nlohmann.me>
This commit is contained in:
7 files changed
+107
-3
No files matched your search
@@ -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.).
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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.).
|
||||
|
||||
|
||||
@@ -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<std::uint8_t>(value_t::object) ? 1 : 0;
|
||||
const node* const end = navigation<Editable>::end(d, container);
|
||||
const node* last = navigation<Editable>::first(d, container);
|
||||
if (value_offset == 0 && container->next == container->len + 1u && (!navigation<Editable>::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;
|
||||
|
||||
Generated
+8
@@ -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<std::uint8_t>(value_t::object) ? 1 : 0;
|
||||
const node* const end = navigation<Editable>::end(d, container);
|
||||
const node* last = navigation<Editable>::first(d, container);
|
||||
if (value_offset == 0 && container->next == container->len + 1u && (!navigation<Editable>::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;
|
||||
|
||||
@@ -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<std::string> 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<int>() == 999);
|
||||
CHECK(scalars.root()[500].get<int>() == 500);
|
||||
CHECK(scalars.root().at(1).get<int>() == 1);
|
||||
CHECK(scalars.root().back().get<int>() == 999);
|
||||
const json_document container_last = json_document::parse(text + ",[1000]]");
|
||||
CHECK(container_last.root()[999].get<int>() == 999);
|
||||
CHECK(container_last.root()[1000][0].get<int>() == 1000);
|
||||
CHECK(container_last.root().back()[0].get<int>() == 1000);
|
||||
}
|
||||
|
||||
SECTION("iterators")
|
||||
{
|
||||
const json_document d = json_document::parse(R"({"x":[1,{"y":2}],"z":null})");
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in new issue
Block a user