Repository navigation
Unflatten in time and memory linear in the pointer depth - #5793
Merged
Merged
Conversation
nlohmann
added this pull request to stack #5794
October 9, 2026 07:50
nlohmann
force-pushed
the
fix-unflatten-quadratic
branch
from
October 9, 2026 15:06
04dff2f to
4ffcbb5
Compare
#5443 made unflatten() decide between arrays and objects independently of the iteration order by collecting the pointer prefixes that have a reference token 0 below them in a std::set<std::vector<string_t>>. Every such prefix was stored as a copy of all its reference tokens, and get_and_create() compared whole prefix vectors at every step, so unflattening a pointer of depth d took time and memory quadratic in d: a 10,000-level array pointer took 18 s and 1.3 GB, a 100,000-level one did not finish. The prefixes are now numbered nodes of a tree, so each is stored once and get_and_create() follows the tree token by token. The result is unchanged, including its independence of the iteration order. Signed-off-by: Niels Lohmann <mail@nlohmann.me>
Signed-off-by: Niels Lohmann <mail@nlohmann.me>
nlohmann
force-pushed
the
fix-unflatten-quadratic
branch
from
October 9, 2026 15:06
4ffcbb5 to
b98dea4
Compare
gregmarr
reviewed
Oct 9, 2026
The constructor now creates the root node, add_child() inserts a reference token below a prefix and returns the child's number, and find_child() looks one up for get_and_create(). Signed-off-by: Niels Lohmann <mail@nlohmann.me>
gregmarr
approved these changes
Oct 9, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Problem
Since #5443 (c72f37a, not in any release yet),
unflatten()takes time and memory quadratic in the depth of a pointer. To decide between arrays and objects independently of iteration order, it collects the prefixes that have a reference token0below them in astd::set<std::vector<string_t>>:0token inserts a copy of the whole prefix so far, so an array chain of depth d stores about d²/2 stringsget_and_create()pushes onto a prefix vector and looks it up in the set at every step, comparing whole vectorsOn
develop, unflattening{"/0/0/…/0": 1}:#5393 had confirmed that
unflatten()handled a 500,000-level pointer before #5443.Change
The prefixes are now numbered nodes of a tree (
prefix_tree):children[id]maps a token to the id of the extended prefix, andis_array[id]records whether a0token appears directly below it. The first pass ofunflatten()builds the tree, storing each prefix once.get_and_create()then follows it token by token instead of building and comparing prefix vectors. The rule from #5443 is unchanged: a value becomes an array exactly when some key has a0directly below it, regardless of iteration order.Benchmark
Deep pointer,
unflatten()only,-O2, Apple clang 21, arm64:Object chains weren't quadratic before (no
0tokens), so the tree adds a small constant cost there.Ordinary documents, best-of-7, median of 3 alternating runs (
dump()output identical in every case):jsonordered_jsonjsonordered_jsonThe wide cases are within the noise of this benchmark (runs varied by up to ±25%). Wide
ordered_jsonmay be slower by about 5%.Tests
unflatten()at full depth; it was limited to 500 levels before.ordered_json, where a0key comes after its sibling ({"/a/1": 2, "/a/0": 1}→{"a": [1, 2]}) and a prefix without a0key ({"/b/1": 2}→{"b": {"1": 2}}). The existing tests usedjson, whose sorted keys never put0second.Locally:
test-json_pointer(C++11, C++20),test-json_patch,test-alt-string,test-custom-array-type,test-custom-object-type,test-diagnosticsandtest-ordered_json2pass, and the build shows no warnings.Public API
No breaking changes. Only private members of
json_pointerchange (array_parents_t→prefix_tree, and the parameter ofget_and_create), and the output ofunflatten()is identical.🤖 Generated with Claude Code