Diff objects with the same keys in lockstep and keep one path buffer

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>
This commit is contained in:
Niels Lohmann committed 2026-10-16 19:38:24 +02:00
1 parent 80407e8316
commit 994099ca28
2 files changed
+92 -12

No files matched your search

+46 -6
View File
@@ -7341,7 +7341,8 @@ public:
{
// the patch
basic_json result(value_t::array);
diff_recursively(result, source, target, path, 0);
string_t buffer = path;
diff_recursively(result, source, target, buffer, 0);
return result;
}
@@ -7501,10 +7502,26 @@ public:
std::vector<const_iterator>& added)
{
// first pass: one lookup per source key
// Objects of the same size usually have the same keys, in the same
// order: walk them together as long as that holds, which takes no lookups.
target_values.reserve(source.size());
std::size_t common = 0;
bool together = source.size() == target.size();
auto partner = target.cbegin();
for (auto it = source.cbegin(); it != source.cend(); ++it)
{
if (together)
{
if (it.key() == partner.key())
{
target_values.push_back(&partner.value());
++common;
++partner;
continue;
}
together = false;
}
const auto found = target.find(it.key());
if (found != target.cend())
{
@@ -7517,6 +7534,12 @@ public:
}
}
// the same keys in the same order
if (together)
{
return true;
}
// second pass: find the keys only target has. An object type that
// keeps its members in insertion order, such as the one backing
// ordered_json, also needs the common keys to be in the same order
@@ -7582,6 +7605,13 @@ public:
return true;
}
// equal objects, whose members are in a different order, need no
// operations
if (source == target)
{
return false;
}
// slow path: the common keys are in a different relative
// order in source and target (only possible for a
// reorderable object_t like ordered_map). Building a
@@ -7616,6 +7646,9 @@ public:
@brief @ref diff, for values at nesting level @a depth, appending the
operations to @a result
@a path is the path of the values; it is a buffer that is extended and
shortened again below, and is the same when the function returns.
Diffing two arrays or objects calls this function again, once per nesting
level, so values nested deeply enough used to exhaust the call stack and
terminate the process. The descent is bounded here: once @ref
@@ -7623,7 +7656,7 @@ public:
diff_iteratively diffs what is left without the call stack.
*/
static void diff_recursively(basic_json& result, const basic_json& source, const basic_json& target,
const string_t& path, const std::size_t depth)
string_t& path, const std::size_t depth)
{
// if the values are the same, there is nothing to do
if (source == target)
@@ -7635,9 +7668,9 @@ public:
}
/// @brief @ref diff_recursively for values known to be unequal; callers
/// compare first so they build no path for equal ones
/// compare first so they extend the path only for unequal ones
static void diff_unequal(basic_json& result, const basic_json& source, const basic_json& target,
const string_t& path, const std::size_t depth)
string_t& path, const std::size_t depth)
{
if (JSON_HEDLEY_UNLIKELY(depth >= detail::recursion_depth_limit()))
{
@@ -7656,6 +7689,8 @@ public:
{
case value_t::array:
{
const std::size_t path_length = path.size();
// first pass: traverse common elements
std::size_t i = 0;
while (i < source.size() && i < target.size())
@@ -7663,7 +7698,9 @@ public:
// recursive call to compare array values at index i
if (!(source[i] == target[i]))
{
diff_unequal(result, source[i], target[i], detail::concat<string_t>(path, '/', detail::to_string<string_t>(i)), depth + 1);
diff_append_index(path, i);
diff_unequal(result, source[i], target[i], path, depth + 1);
path.resize(path_length);
}
++i;
}
@@ -7685,6 +7722,7 @@ public:
// lookup is needed. Deleted keys are interleaved here
// too, in source's original order, to match the
// historical (pre-reordering-aware) output order.
const std::size_t path_length = path.size();
std::size_t i = 0;
for (auto it = source.cbegin(); it != source.cend(); ++it, ++i)
{
@@ -7692,7 +7730,9 @@ public:
{
if (!(it.value() == *target_values[i]))
{
diff_unequal(result, it.value(), *target_values[i], detail::concat<string_t>(path, '/', detail::escape(it.key())), depth + 1);
diff_append_key(path, it.key());
diff_unequal(result, it.value(), *target_values[i], path, depth + 1);
path.resize(path_length);
}
}
else
+46 -6
View File
@@ -37597,7 +37597,8 @@ public:
{
// the patch
basic_json result(value_t::array);
diff_recursively(result, source, target, path, 0);
string_t buffer = path;
diff_recursively(result, source, target, buffer, 0);
return result;
}
@@ -37757,10 +37758,26 @@ public:
std::vector<const_iterator>& added)
{
// first pass: one lookup per source key
// Objects of the same size usually have the same keys, in the same
// order: walk them together as long as that holds, which takes no lookups.
target_values.reserve(source.size());
std::size_t common = 0;
bool together = source.size() == target.size();
auto partner = target.cbegin();
for (auto it = source.cbegin(); it != source.cend(); ++it)
{
if (together)
{
if (it.key() == partner.key())
{
target_values.push_back(&partner.value());
++common;
++partner;
continue;
}
together = false;
}
const auto found = target.find(it.key());
if (found != target.cend())
{
@@ -37773,6 +37790,12 @@ public:
}
}
// the same keys in the same order
if (together)
{
return true;
}
// second pass: find the keys only target has. An object type that
// keeps its members in insertion order, such as the one backing
// ordered_json, also needs the common keys to be in the same order
@@ -37838,6 +37861,13 @@ public:
return true;
}
// equal objects, whose members are in a different order, need no
// operations
if (source == target)
{
return false;
}
// slow path: the common keys are in a different relative
// order in source and target (only possible for a
// reorderable object_t like ordered_map). Building a
@@ -37872,6 +37902,9 @@ public:
@brief @ref diff, for values at nesting level @a depth, appending the
operations to @a result
@a path is the path of the values; it is a buffer that is extended and
shortened again below, and is the same when the function returns.
Diffing two arrays or objects calls this function again, once per nesting
level, so values nested deeply enough used to exhaust the call stack and
terminate the process. The descent is bounded here: once @ref
@@ -37879,7 +37912,7 @@ public:
diff_iteratively diffs what is left without the call stack.
*/
static void diff_recursively(basic_json& result, const basic_json& source, const basic_json& target,
const string_t& path, const std::size_t depth)
string_t& path, const std::size_t depth)
{
// if the values are the same, there is nothing to do
if (source == target)
@@ -37891,9 +37924,9 @@ public:
}
/// @brief @ref diff_recursively for values known to be unequal; callers
/// compare first so they build no path for equal ones
/// compare first so they extend the path only for unequal ones
static void diff_unequal(basic_json& result, const basic_json& source, const basic_json& target,
const string_t& path, const std::size_t depth)
string_t& path, const std::size_t depth)
{
if (JSON_HEDLEY_UNLIKELY(depth >= detail::recursion_depth_limit()))
{
@@ -37912,6 +37945,8 @@ public:
{
case value_t::array:
{
const std::size_t path_length = path.size();
// first pass: traverse common elements
std::size_t i = 0;
while (i < source.size() && i < target.size())
@@ -37919,7 +37954,9 @@ public:
// recursive call to compare array values at index i
if (!(source[i] == target[i]))
{
diff_unequal(result, source[i], target[i], detail::concat<string_t>(path, '/', detail::to_string<string_t>(i)), depth + 1);
diff_append_index(path, i);
diff_unequal(result, source[i], target[i], path, depth + 1);
path.resize(path_length);
}
++i;
}
@@ -37941,6 +37978,7 @@ public:
// lookup is needed. Deleted keys are interleaved here
// too, in source's original order, to match the
// historical (pre-reordering-aware) output order.
const std::size_t path_length = path.size();
std::size_t i = 0;
for (auto it = source.cbegin(); it != source.cend(); ++it, ++i)
{
@@ -37948,7 +37986,9 @@ public:
{
if (!(it.value() == *target_values[i]))
{
diff_unequal(result, it.value(), *target_values[i], detail::concat<string_t>(path, '/', detail::escape(it.key())), depth + 1);
diff_append_key(path, it.key());
diff_unequal(result, it.value(), *target_values[i], path, depth + 1);
path.resize(path_length);
}
}
else