json_view: fewer allocations when parsing small documents
- Size the buffer of decoded strings on its first growth from the input (a quarter of it plus 16 bytes, at most 4 KiB) instead of 272 bytes, so that a document with escapes does not grow it twice, with a zero-filled copy each time. - Allocate the node index of documents with an estimate of up to 1024 nodes (was 256) together with the header, which saves one allocation. When the estimate is too low (dense numbers), that block stays unused: at most 16 KiB instead of 4 KiB. shrink_to_fit() cannot release it separately; the documentation says so. Fresh parse of a 2.5 KB minified tweet: 993 to 958 ns (-3.5 %), traverse and select -3 %, with no row consistently slower (Apple M1 Max, 30 rounds, three runs). Dense numeric documents parse about 9 % faster and keep up to 16 KiB more memory. Signed-off-by: Niels Lohmann <mail@nlohmann.me>
This commit is contained in:
5 files changed
+89
-8
No files matched your search
@@ -23,6 +23,10 @@ Linear in [`node_count()`](node_count.md) plus the length of the decoded strings
|
||||
|
||||
## Notes
|
||||
|
||||
The node index of a small document (an estimate of up to 1024 nodes, which is up to about 4 KiB of minified text) is
|
||||
allocated together with the document's header, so that parsing needs one allocation. That part cannot be released
|
||||
separately: `shrink_to_fit()` only trims a node index that has its own allocation, and the decoded strings.
|
||||
|
||||
!!! warning "Invalidates views"
|
||||
|
||||
Unlike moving the document, `shrink_to_fit()` **invalidates every view taken from this document before the
|
||||
|
||||
@@ -543,7 +543,15 @@ run_found:
|
||||
NLOHMANN_VIEW_NOINLINE void arena_grow(std::size_t n)
|
||||
{
|
||||
const std::size_t used = arena_used();
|
||||
m_doc.arena.resize((std::max)(m_doc.arena.size() * 2, used + n + 256));
|
||||
std::size_t wanted = used + n + 256;
|
||||
if (m_doc.arena.empty())
|
||||
{
|
||||
// the first growth: a decoded string is never longer than its source,
|
||||
// so a fraction of the input (capped, as the buffer is zero-filled)
|
||||
// covers the escapes of a typical document without regrowing it
|
||||
wanted = (std::max)(wanted, (std::min)(static_cast<std::size_t>(m_end - m_begin) / 4 + 16, std::size_t{4096}));
|
||||
}
|
||||
m_doc.arena.resize((std::max)(m_doc.arena.size() * 2, wanted));
|
||||
m_arena_write = m_doc.arena.data() + used;
|
||||
m_arena_end = m_doc.arena.data() + m_doc.arena.size();
|
||||
}
|
||||
|
||||
@@ -82,14 +82,17 @@ struct document_data
|
||||
};
|
||||
std::unique_ptr<edit_state> edits; ///< created by the first edit
|
||||
|
||||
/// the most nodes that are allocated together with the header (16 KiB)
|
||||
static constexpr std::size_t max_inline_nodes = 1024;
|
||||
|
||||
/// One allocation for the header and room for `count` nodes, so that a
|
||||
/// small document needs one allocation instead of two. Large documents get
|
||||
/// a separate node array instead (so it can be trimmed). When a document
|
||||
/// outgrows the inline array, that array stays unused: at most 256 nodes
|
||||
/// (4 KiB), and only for documents whose estimate was too small.
|
||||
/// outgrows the inline array, that array stays unused: at most 1024 nodes
|
||||
/// (16 KiB), and only for documents whose estimate was too small.
|
||||
static document_data* create(std::size_t count)
|
||||
{
|
||||
count = count <= 256 ? count : 0;
|
||||
count = count <= max_inline_nodes ? count : 0;
|
||||
void* mem = ::operator new (sizeof(document_data) + (count * sizeof(node)));
|
||||
auto* d = new (mem) document_data(); // NOLINT(cppcoreguidelines-owning-memory): owned by the returned pointer, freed by deleter
|
||||
// (aligned: sizeof is a multiple of the alignment; through void*, as GCC's -Wcast-align wants)
|
||||
|
||||
Generated
+15
-4
@@ -397,14 +397,17 @@ struct document_data
|
||||
};
|
||||
std::unique_ptr<edit_state> edits; ///< created by the first edit
|
||||
|
||||
/// the most nodes that are allocated together with the header (16 KiB)
|
||||
static constexpr std::size_t max_inline_nodes = 1024;
|
||||
|
||||
/// One allocation for the header and room for `count` nodes, so that a
|
||||
/// small document needs one allocation instead of two. Large documents get
|
||||
/// a separate node array instead (so it can be trimmed). When a document
|
||||
/// outgrows the inline array, that array stays unused: at most 256 nodes
|
||||
/// (4 KiB), and only for documents whose estimate was too small.
|
||||
/// outgrows the inline array, that array stays unused: at most 1024 nodes
|
||||
/// (16 KiB), and only for documents whose estimate was too small.
|
||||
static document_data* create(std::size_t count)
|
||||
{
|
||||
count = count <= 256 ? count : 0;
|
||||
count = count <= max_inline_nodes ? count : 0;
|
||||
void* mem = ::operator new (sizeof(document_data) + (count * sizeof(node)));
|
||||
auto* d = new (mem) document_data(); // NOLINT(cppcoreguidelines-owning-memory): owned by the returned pointer, freed by deleter
|
||||
// (aligned: sizeof is a multiple of the alignment; through void*, as GCC's -Wcast-align wants)
|
||||
@@ -1734,7 +1737,15 @@ run_found:
|
||||
NLOHMANN_VIEW_NOINLINE void arena_grow(std::size_t n)
|
||||
{
|
||||
const std::size_t used = arena_used();
|
||||
m_doc.arena.resize((std::max)(m_doc.arena.size() * 2, used + n + 256));
|
||||
std::size_t wanted = used + n + 256;
|
||||
if (m_doc.arena.empty())
|
||||
{
|
||||
// the first growth: a decoded string is never longer than its source,
|
||||
// so a fraction of the input (capped, as the buffer is zero-filled)
|
||||
// covers the escapes of a typical document without regrowing it
|
||||
wanted = (std::max)(wanted, (std::min)(static_cast<std::size_t>(m_end - m_begin) / 4 + 16, std::size_t{4096}));
|
||||
}
|
||||
m_doc.arena.resize((std::max)(m_doc.arena.size() * 2, wanted));
|
||||
m_arena_write = m_doc.arena.data() + used;
|
||||
m_arena_end = m_doc.arena.data() + m_doc.arena.size();
|
||||
}
|
||||
|
||||
@@ -666,6 +666,61 @@ TEST_CASE("json_view")
|
||||
CHECK(small.root().materialize() == json::parse("[1,[2,3],{\"a\":\"b\\n\"}]"));
|
||||
}
|
||||
|
||||
SECTION("block sizes")
|
||||
{
|
||||
// documents whose node estimate lies around the size that is allocated
|
||||
// with the header, and whose decoded strings need the first growth of
|
||||
// the buffer, in all sizes around the points where the policies change
|
||||
for (const int count :
|
||||
{
|
||||
1, 2, 10, 50, 100, 255, 256, 257, 500, 1000, 1023, 1024, 1025, 2000, 5000
|
||||
})
|
||||
{
|
||||
// strings with escapes (decoded text, minified)
|
||||
std::string escaped = "[";
|
||||
for (int i = 0; i < count; ++i)
|
||||
{
|
||||
escaped += (i != 0 ? ",\"a\\n" : "\"a\\n") + std::to_string(i) + "\\u00e4\"";
|
||||
}
|
||||
escaped += ']';
|
||||
json_document e = json_document::parse(escaped);
|
||||
CHECK(e.root().materialize() == json::parse(escaped));
|
||||
e.shrink_to_fit();
|
||||
CHECK(e.root().materialize() == json::parse(escaped));
|
||||
|
||||
// dense numbers (more nodes per byte than the estimate expects)
|
||||
std::string dense = "[";
|
||||
for (int i = 0; i < count; ++i)
|
||||
{
|
||||
dense += (i != 0 ? ",1" : "1");
|
||||
}
|
||||
dense += ']';
|
||||
json_document n = json_document::parse(dense);
|
||||
CHECK(n.node_count() == static_cast<std::size_t>(count) + 1);
|
||||
CHECK(n.root().materialize() == json::parse(dense));
|
||||
n.shrink_to_fit();
|
||||
CHECK(n.root().size() == static_cast<std::size_t>(count));
|
||||
|
||||
// the same documents, pretty-printed
|
||||
const std::string pretty = json::parse(escaped).dump(2);
|
||||
json_document p = json_document::parse(pretty);
|
||||
CHECK(p.root().materialize() == json::parse(escaped));
|
||||
}
|
||||
|
||||
// a large document with a single escape does not need a buffer of the
|
||||
// size of its input
|
||||
std::string large = "[\"a\\n\"";
|
||||
for (int i = 0; i < 100000; ++i)
|
||||
{
|
||||
large += ",1";
|
||||
}
|
||||
large += ']';
|
||||
const json_document l = json_document::parse(large);
|
||||
CHECK(l.root().size() == 100001);
|
||||
CHECK(l.root()[0].get_string() == "a\n");
|
||||
CHECK(l.root()[100000].get<int>() == 1);
|
||||
}
|
||||
|
||||
SECTION("source offsets")
|
||||
{
|
||||
const std::string text = R"( {"key": "value", "escaped": "a\nb", "n": 42})";
|
||||
|
||||
Reference in new issue
Block a user