Bound diff()'s descent with a depth count instead of scanning the source

Now that operator== no longer recurses (#5390), diff() can keep its per-level
equality shortcut all the way down. It diffs recursively for the first
detail::recursion_depth_limit() levels, as merge_patch() does, and hands
anything deeper to diff_iteratively(). The nesting_exceeds() scan, which
cost about 30% on equal documents, is gone, and diff() is on par with
develop again.

Signed-off-by: Niels Lohmann <mail@nlohmann.me>
This commit is contained in:
Niels Lohmann
2026-09-25 22:23:57 +02:00
parent 958e0a906b
commit b41e43fffc
2 changed files with 48 additions and 142 deletions
+24 -71
View File
@@ -5994,62 +5994,10 @@ class basic_json // NOLINT(cppcoreguidelines-special-member-functions,hicpp-spec
static basic_json diff(const basic_json& source, const basic_json& target,
const string_t& path = "")
{
// Diffing descends into both values once per nesting level and
// compares them with operator== on the way, which recurses as well,
// so values nested deeply enough used to exhaust the call stack.
// Both only descend as far as the source is nested, so a source
// nested no more than detail::recursion_depth_limit() levels deep -
// all but a vanishing minority - is diffed recursively as before;
// deeper ones are diffed without the call stack.
if (JSON_HEDLEY_LIKELY(!nesting_exceeds(source, detail::recursion_depth_limit())))
{
return diff_recursively(source, target, path);
}
return diff_iteratively(source, target, path);
return diff_recursively(source, target, path, 0);
}
private:
/*!
@brief whether @a j is nested more than @a limit levels deep
A primitive value is not nested at all, an array or object one level more
than its most deeply nested element. Recurses at most @a limit levels.
*/
static bool nesting_exceeds(const basic_json& j, const std::size_t limit)
{
switch (j.m_data.m_type)
{
case value_t::array:
{
return limit == 0 || std::any_of(j.m_data.m_value.array->cbegin(), j.m_data.m_value.array->cend(),
[limit](const basic_json & element)
{
return element.is_structured() && nesting_exceeds(element, limit - 1);
});
}
case value_t::object:
{
return limit == 0 || std::any_of(j.m_data.m_value.object->cbegin(), j.m_data.m_value.object->cend(),
[limit](const typename object_t::value_type & element)
{
return element.second.is_structured() && nesting_exceeds(element.second, limit - 1);
});
}
case value_t::null:
case value_t::string:
case value_t::boolean:
case value_t::number_integer:
case value_t::number_unsigned:
case value_t::number_float:
case value_t::binary:
case value_t::discarded:
default:
return false;
}
}
/// @brief two arrays or two objects @ref diff_iteratively is diffing
struct diff_frame
{
@@ -6084,9 +6032,17 @@ class basic_json // NOLINT(cppcoreguidelines-special-member-functions,hicpp-spec
basic_json added_ops{}; // NOLINT(readability-redundant-member-init)
};
/// @ref diff for a @a source nested no more than @ref detail::recursion_depth_limit levels deep
/*!
@brief @ref diff, for values at nesting level @a depth
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
detail::recursion_depth_limit levels have been entered, @ref
diff_iteratively diffs what is left without the call stack.
*/
static basic_json diff_recursively(const basic_json& source, const basic_json& target,
const string_t& path)
const string_t& path, const std::size_t depth)
{
// the patch
basic_json result(value_t::array);
@@ -6097,6 +6053,11 @@ class basic_json // NOLINT(cppcoreguidelines-special-member-functions,hicpp-spec
return result;
}
if (JSON_HEDLEY_UNLIKELY(depth >= detail::recursion_depth_limit()))
{
return diff_iteratively(source, target, path);
}
if (source.type() != target.type())
{
// different types: replace value
@@ -6116,7 +6077,7 @@ class basic_json // NOLINT(cppcoreguidelines-special-member-functions,hicpp-spec
while (i < source.size() && i < target.size())
{
// recursive call to compare array values at index i
auto temp_diff = diff_recursively(source[i], target[i], detail::concat<string_t>(path, '/', detail::to_string<string_t>(i)));
auto temp_diff = diff_recursively(source[i], target[i], detail::concat<string_t>(path, '/', detail::to_string<string_t>(i)), depth + 1);
result.insert(result.end(), temp_diff.begin(), temp_diff.end());
++i;
}
@@ -6235,7 +6196,7 @@ class basic_json // NOLINT(cppcoreguidelines-special-member-functions,hicpp-spec
if (common_it != common_keys_source_order.cend() && it.key() == *common_it)
{
const auto path_key = detail::concat<string_t>(path, '/', detail::escape(it.key()));
auto temp_diff = diff_recursively(it.value(), target[it.key()], path_key);
auto temp_diff = diff_recursively(it.value(), target[it.key()], path_key, depth + 1);
result.insert(result.end(), temp_diff.begin(), temp_diff.end());
++common_it;
}
@@ -6323,9 +6284,8 @@ class basic_json // NOLINT(cppcoreguidelines-special-member-functions,hicpp-spec
/*!
@brief @ref diff without the call stack
Produces the same patch as @ref diff_recursively. Only used for a source
nested more deeply than @ref detail::recursion_depth_limit; any arrays and objects
below it that are not nested that deeply are diffed recursively.
Produces the same patch as @ref diff_recursively. Only reached for values
nested more deeply than @ref detail::recursion_depth_limit.
*/
static basic_json diff_iteratively(const basic_json& source, const basic_json& target,
const string_t& path)
@@ -6348,8 +6308,10 @@ class basic_json // NOLINT(cppcoreguidelines-special-member-functions,hicpp-spec
const auto enter = [&result, &stack, &current_path](const basic_json & s, const basic_json & t)
{
// if the values are the same, there is nothing to do. Arrays and
// objects are not compared up front: comparing them recurses into
// everything below them - equal ones yield no operations anyway.
// objects are not compared up front: comparing them visits
// everything below them, so doing that at every level would take
// quadratic time in the nesting depth - equal ones yield no
// operations anyway.
if ((!s.is_structured() || !t.is_structured()) && s == t)
{
return;
@@ -6365,15 +6327,6 @@ class basic_json // NOLINT(cppcoreguidelines-special-member-functions,hicpp-spec
return;
}
// arrays and objects that are not nested too deeply for the call
// stack are diffed recursively, which can skip equal parts
if (!nesting_exceeds(s, detail::recursion_depth_limit()))
{
const basic_json partial = diff_recursively(s, t, current_path);
result.insert(result.end(), partial.begin(), partial.end());
return;
}
switch (s.type())
{
case value_t::array:
+24 -71
View File
@@ -30952,62 +30952,10 @@ class basic_json // NOLINT(cppcoreguidelines-special-member-functions,hicpp-spec
static basic_json diff(const basic_json& source, const basic_json& target,
const string_t& path = "")
{
// Diffing descends into both values once per nesting level and
// compares them with operator== on the way, which recurses as well,
// so values nested deeply enough used to exhaust the call stack.
// Both only descend as far as the source is nested, so a source
// nested no more than detail::recursion_depth_limit() levels deep -
// all but a vanishing minority - is diffed recursively as before;
// deeper ones are diffed without the call stack.
if (JSON_HEDLEY_LIKELY(!nesting_exceeds(source, detail::recursion_depth_limit())))
{
return diff_recursively(source, target, path);
}
return diff_iteratively(source, target, path);
return diff_recursively(source, target, path, 0);
}
private:
/*!
@brief whether @a j is nested more than @a limit levels deep
A primitive value is not nested at all, an array or object one level more
than its most deeply nested element. Recurses at most @a limit levels.
*/
static bool nesting_exceeds(const basic_json& j, const std::size_t limit)
{
switch (j.m_data.m_type)
{
case value_t::array:
{
return limit == 0 || std::any_of(j.m_data.m_value.array->cbegin(), j.m_data.m_value.array->cend(),
[limit](const basic_json & element)
{
return element.is_structured() && nesting_exceeds(element, limit - 1);
});
}
case value_t::object:
{
return limit == 0 || std::any_of(j.m_data.m_value.object->cbegin(), j.m_data.m_value.object->cend(),
[limit](const typename object_t::value_type & element)
{
return element.second.is_structured() && nesting_exceeds(element.second, limit - 1);
});
}
case value_t::null:
case value_t::string:
case value_t::boolean:
case value_t::number_integer:
case value_t::number_unsigned:
case value_t::number_float:
case value_t::binary:
case value_t::discarded:
default:
return false;
}
}
/// @brief two arrays or two objects @ref diff_iteratively is diffing
struct diff_frame
{
@@ -31042,9 +30990,17 @@ class basic_json // NOLINT(cppcoreguidelines-special-member-functions,hicpp-spec
basic_json added_ops{}; // NOLINT(readability-redundant-member-init)
};
/// @ref diff for a @a source nested no more than @ref detail::recursion_depth_limit levels deep
/*!
@brief @ref diff, for values at nesting level @a depth
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
detail::recursion_depth_limit levels have been entered, @ref
diff_iteratively diffs what is left without the call stack.
*/
static basic_json diff_recursively(const basic_json& source, const basic_json& target,
const string_t& path)
const string_t& path, const std::size_t depth)
{
// the patch
basic_json result(value_t::array);
@@ -31055,6 +31011,11 @@ class basic_json // NOLINT(cppcoreguidelines-special-member-functions,hicpp-spec
return result;
}
if (JSON_HEDLEY_UNLIKELY(depth >= detail::recursion_depth_limit()))
{
return diff_iteratively(source, target, path);
}
if (source.type() != target.type())
{
// different types: replace value
@@ -31074,7 +31035,7 @@ class basic_json // NOLINT(cppcoreguidelines-special-member-functions,hicpp-spec
while (i < source.size() && i < target.size())
{
// recursive call to compare array values at index i
auto temp_diff = diff_recursively(source[i], target[i], detail::concat<string_t>(path, '/', detail::to_string<string_t>(i)));
auto temp_diff = diff_recursively(source[i], target[i], detail::concat<string_t>(path, '/', detail::to_string<string_t>(i)), depth + 1);
result.insert(result.end(), temp_diff.begin(), temp_diff.end());
++i;
}
@@ -31193,7 +31154,7 @@ class basic_json // NOLINT(cppcoreguidelines-special-member-functions,hicpp-spec
if (common_it != common_keys_source_order.cend() && it.key() == *common_it)
{
const auto path_key = detail::concat<string_t>(path, '/', detail::escape(it.key()));
auto temp_diff = diff_recursively(it.value(), target[it.key()], path_key);
auto temp_diff = diff_recursively(it.value(), target[it.key()], path_key, depth + 1);
result.insert(result.end(), temp_diff.begin(), temp_diff.end());
++common_it;
}
@@ -31281,9 +31242,8 @@ class basic_json // NOLINT(cppcoreguidelines-special-member-functions,hicpp-spec
/*!
@brief @ref diff without the call stack
Produces the same patch as @ref diff_recursively. Only used for a source
nested more deeply than @ref detail::recursion_depth_limit; any arrays and objects
below it that are not nested that deeply are diffed recursively.
Produces the same patch as @ref diff_recursively. Only reached for values
nested more deeply than @ref detail::recursion_depth_limit.
*/
static basic_json diff_iteratively(const basic_json& source, const basic_json& target,
const string_t& path)
@@ -31306,8 +31266,10 @@ class basic_json // NOLINT(cppcoreguidelines-special-member-functions,hicpp-spec
const auto enter = [&result, &stack, &current_path](const basic_json & s, const basic_json & t)
{
// if the values are the same, there is nothing to do. Arrays and
// objects are not compared up front: comparing them recurses into
// everything below them - equal ones yield no operations anyway.
// objects are not compared up front: comparing them visits
// everything below them, so doing that at every level would take
// quadratic time in the nesting depth - equal ones yield no
// operations anyway.
if ((!s.is_structured() || !t.is_structured()) && s == t)
{
return;
@@ -31323,15 +31285,6 @@ class basic_json // NOLINT(cppcoreguidelines-special-member-functions,hicpp-spec
return;
}
// arrays and objects that are not nested too deeply for the call
// stack are diffed recursively, which can skip equal parts
if (!nesting_exceeds(s, detail::recursion_depth_limit()))
{
const basic_json partial = diff_recursively(s, t, current_path);
result.insert(result.end(), partial.begin(), partial.end());
return;
}
switch (s.type())
{
case value_t::array: