Repository navigation
Speed up flatten(), unflatten(), diff() and patch() - #5861
Merged
Merged
Conversation
gregmarr
reviewed
Oct 16, 2026
| /// append the decimal representation of @a i to @a out | ||
| static void append_index(string_t& out, std::size_t i) | ||
| { | ||
| char buf[24]; // NOLINT(cppcoreguidelines-avoid-c-arrays,hicpp-avoid-c-arrays,modernize-avoid-c-arrays) |
Owner
Author
gregmarr
reviewed
Oct 16, 2026
| target_values.reserve(source.size()); | ||
| std::size_t common = 0; | ||
| bool together = source.size() == target.size(); | ||
| auto partner = target.cbegin(); |
Contributor
There was a problem hiding this comment.
This partner should be able to go in the for loop initializer as well.
Owner
Author
nlohmann
force-pushed
the
perf/value-operations-speedup
branch
2 times, most recently
from
October 17, 2026 06:29
2241af3 to
65fbbc5
Compare
| std::uint64_t w = 0; | ||
| if (i < size) | ||
| { | ||
| std::memcpy(&w, data + i, size - i); |
| std::uint64_t w = 0; | ||
| if (i < size) | ||
| { | ||
| std::memcpy(&w, data + i, size - i); |
flatten() added every path with operator[], so each insertion into the std::map of the result searched the tree from the root and compared long shared path prefixes, which took half the time on canada.json. Paths come out in nearly sorted order, so they are now inserted with emplace_hint next to the entry added last, for object types that have it. ordered_map keeps its append without lookup, and other object types keep operator[]. Path segments are also appended in place, without a temporary string for each escaped key or array index. flatten() of canada.json, citm_catalog.json and twitter.json is 1.4x to 1.9x as fast; ordered_json is unchanged (1.05x). The output is unchanged. Signed-off-by: Niels Lohmann <mail@nlohmann.me>
unflatten() split every key into a vector of reference tokens, stored the pointer prefixes in a tree to decide which values become arrays, and then looked up every token of every key from the root again. Keys are now checked by a scan that raises the same parse_error.107/108 as split(), skipping the part a key shares with the previous one, which was checked already. The prefixes that denote arrays are kept in a small hash set of key slices; the array/object decision is unchanged. The walk keeps the values along the previous key's path, so only the part of a key that differs from the previous one is looked up. Keys of a flattened object are usually sorted, so that part is short. unflatten() of canada.json, citm_catalog.json and twitter.json is 1.9x to 2.3x as fast; the output and the exceptions are unchanged. Signed-off-by: Niels Lohmann <mail@nlohmann.me>
patch() built a json_pointer for every operation, which splits the path into a vector of strings, and for "add" and "remove" a second one for the parent. That took 20% to 45% of the time not spent copying the document. "add", "remove", "replace" and "test" with a path that has no escapes and denotes existing values (the parent, for "add"), as diff() creates them, are now applied by resolving the path in place, reusing one string for the reference tokens. Every other operation, and every operation that would fail, takes the existing code, so the results, the exceptions and the state left after a failing operation are unchanged. patch() of the diffs of canada.json, citm_catalog.json and twitter.json is 1.02x to 1.10x as fast; the rest is the copy of the document. Signed-off-by: Niels Lohmann <mail@nlohmann.me>
diff() created every operation from an initializer list, so each
{"op", "replace"} pair became a temporary array with two heap strings
before it was turned into an object member; an operation took about 25
allocations. The operations are now built as objects whose members are
assigned one by one. Paths of members and elements are also built in
place, without a temporary string for each escaped key or array index.
diff() of canada.json, citm_catalog.json and twitter.json, with every
eighth scalar changed, is 1.3x to 1.6x as fast; the output is unchanged.
Signed-off-by: Niels Lohmann <mail@nlohmann.me>
diff() looked up every key of the source object in the target object, and built a new string for the path of every value it descended into. Objects of the same size usually have the same keys in the same order, so they are now walked together as long as that holds, without a lookup; objects that differ fall back to the lookup from the first key that does not match. The path is one buffer that is extended before descending and shortened again afterwards. Objects whose members are equal but in a different order (possible with ordered_json) still get no operations. Together with the previous commit, diff() of canada.json, citm_catalog.json and twitter.json is 1.5x to 2.0x as fast; ordered_json 1.65x to 1.85x. The output is unchanged. Signed-off-by: Niels Lohmann <mail@nlohmann.me>
Signed-off-by: Niels Lohmann <mail@nlohmann.me>
…eys() Signed-off-by: Niels Lohmann <mail@nlohmann.me>
Signed-off-by: Niels Lohmann <mail@nlohmann.me>
nlohmann
force-pushed
the
perf/value-operations-speedup
branch
from
October 20, 2026 05:51
65fbbc5 to
b3fd84d
Compare
gregmarr
approved these changes
Oct 21, 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.
flatten(),unflatten()anddiff()spent most of their time on work that can be avoided: searching astd::mapfrom the root for every flattened path, splitting every key into a vector of strings and walking it from the root again, and building every diff operation from initializer lists. This makes them 1.4× to 2.3× as fast, andpatch()a little faster. Output, exceptions and the state left after a failing patch are unchanged.One commit per change:
emplace_hint(std::map), each path is inserted next to the entry added last, since paths come out in nearly sorted order.ordered_mapkeeps its append without lookup; other object types keepoperator[]. Path segments are appended in place.parse_error.107/108assplit(), skipping the part shared with the previous key. The prefixes that become arrays are kept in a small hash set of key slices instead of a prefix tree, and the walk keeps the values along the previous key's path, so only the part of a key that differs from the previous one is looked up. The array/object decision is unchanged.add,remove,replaceandteston a path without escapes that denotes existing values (asdiff()creates them) are applied without building ajson_pointer. Everything else, and every operation that would fail, takes the existing code.Performance
tests/benchmarksof #5845, Apple M1 Max, Apple Clang 21,-O3 -flto, C++11, medians of 4 interleaved rounds againstdevelop(5683f5e):Flatten/canadaFlatten/citm_catalogFlatten/twitterFlattenOrdered/citm_catalogFlattenOrdered/twitterUnflatten/canadaUnflatten/citm_catalogUnflatten/twitterDiff/canadaDiff/citm_catalogDiff/twitterPatch/canadaPatch/citm_catalogPatch/twitterpatch()is bound by the copy of the document it makes (3.4 of 5.5 ms on canada). Forordered_json,unflatten()is 2.05× to 2.2× anddiff()1.65× to 1.85× as fast;patch()1.03× to 1.09×.Breaking changes
None. Only private and
detailcode changes; the public API, the output and the exceptions are unchanged. The private helpersjson_pointer::prefix_treeandjson_pointer::get_and_createare removed, andbasic_json::diff_recursively/diff_unequaltake the path as a mutable buffer; none of them is part of the public API.Testing
developon canada, citm_catalog and twitter, forjsonandordered_json:flatten(),unflatten(),diff()at three change densities andpatch(), plus about 90,000 random flattened objects (valid and invalid keys), 200,000 random patches (including the partially patched state after a failing one) and random diff/patch and flatten/unflatten round trips. 137 MB of output, byte-identical, including exception ids and messages.JSON_DIAGNOSTICS=1.-Wall -Wextra -Wpedantic -Wconversion -Wsign-conversion -Wshadow -Wold-style-cast -Werror), and itssingle_includematchesmake amalgamate.This PR was written by Claude Code on behalf of @nlohmann.
🤖 Generated with Claude Code