From 82250cbe02c527db301f55e7f70240c566a50132 Mon Sep 17 00:00:00 2001 From: Niels Lohmann Date: Thu, 15 Oct 2026 11:47:26 +0200 Subject: [PATCH] Destroy the first levels of a value by recursion The pointer-reversing walk in destroy_container() avoids the call stack but moves a value around for every container it visits. Recurse for the first recursion_depth_limit() levels instead - no allocation, no thread-local, a bounded frame - and hand anything deeper to the walk. Destroy twitter 0.49 -> 0.36 ms, citm 0.80 -> 0.74 ms (3.12.0: 0.47 / 1.00). Signed-off-by: Niels Lohmann --- include/nlohmann/json.hpp | 59 +++++++++++++++++-- single_include/nlohmann/json.hpp | 99 ++++++++++++++++++++++++-------- 2 files changed, 130 insertions(+), 28 deletions(-) diff --git a/include/nlohmann/json.hpp b/include/nlohmann/json.hpp index 63ce8acb1..2e9553f94 100644 --- a/include/nlohmann/json.hpp +++ b/include/nlohmann/json.hpp @@ -878,7 +878,58 @@ public: return; } - // Destroy the tree without recursing per nesting level and + // This json_value is not itself a basic_json, so the top-level + // container is first moved into a local stand-in ("cur"); a + // default-constructed basic_json has a null pointer in its + // m_value (see data::m_value's initializer), so swapping it + // with *this leaves this union's own pointer null, and it is + // never looked at or freed a second time. + basic_json cur; + cur.m_data.m_type = t; + using std::swap; + swap(cur.m_data.m_value, *this); + + destroy_bounded(cur, 0); + } + + // Destroy the array/object v, whose children are destroyed first. + // The first recursion_depth_limit() levels are destroyed by plain + // recursion, which is the fastest way and cannot throw; anything + // nested deeper is handed to destroy_walk(), which uses no call stack. + static void destroy_bounded(basic_json& v, std::size_t depth) noexcept + { + if (JSON_HEDLEY_UNLIKELY(depth >= detail::recursion_depth_limit())) + { + destroy_walk(v); + return; + } + + if (v.m_data.m_type == value_t::array) + { + for (auto& child : *v.m_data.m_value.array) + { + if (child.m_data.m_type == value_t::array || child.m_data.m_type == value_t::object) + { + destroy_bounded(child, depth + 1); + } + } + } + else + { + for (auto& child : *v.m_data.m_value.object) + { + if (child.second.m_data.m_type == value_t::array || child.second.m_data.m_type == value_t::object) + { + destroy_bounded(child.second, depth + 1); + } + } + } + + // only leaves and already destroyed (null) children remain + free_container(v); + } + + // Destroy the tree below top without recursing per nesting level and // without any heap allocation: a heap-allocated flattening // stack (the previous implementation) can itself throw // bad_alloc, which would escape this noexcept destructor and @@ -902,10 +953,10 @@ public: // pointer in its m_value (see data::m_value's initializer), // so swapping it with *this leaves this union's own pointer // null, and it is never looked at or freed a second time. + static void destroy_walk(basic_json& top) noexcept + { basic_json cur; - cur.m_data.m_type = t; - using std::swap; - swap(cur.m_data.m_value, *this); + take(cur, top); basic_json prev; // value_t::null: no parent diff --git a/single_include/nlohmann/json.hpp b/single_include/nlohmann/json.hpp index a45b29ce9..7f53b1aa9 100644 --- a/single_include/nlohmann/json.hpp +++ b/single_include/nlohmann/json.hpp @@ -30571,35 +30571,86 @@ public: return; } - // Destroy the tree without recursing per nesting level and - // without any heap allocation: a heap-allocated flattening - // stack (the previous implementation) can itself throw - // bad_alloc, which would escape this noexcept destructor and - // terminate the program (#5135). - // - // Instead, walk down a chain of children (always the one - // walk_child() picks), reversing links as we go: cur is the - // container currently being emptied, and prev is its parent - // (value_t::null when there is none). Each parent's - // walk_child() slot doubles as storage for that parent's own - // parent link while we are below it, so no extra memory is - // needed; the parent is not modified meanwhile, so - // walk_child() finds that same slot again on the way up. We - // only ever remove a child once it is a scalar or an empty - // array/object, which neither allocates nor recurses more - // than one level deep. - // - // This json_value is not itself a basic_json, so the - // top-level container is first moved into a local stand-in - // ("cur"); a default-constructed basic_json has a null - // pointer in its m_value (see data::m_value's initializer), - // so swapping it with *this leaves this union's own pointer - // null, and it is never looked at or freed a second time. + // This json_value is not itself a basic_json, so the top-level + // container is first moved into a local stand-in ("cur"); a + // default-constructed basic_json has a null pointer in its + // m_value (see data::m_value's initializer), so swapping it + // with *this leaves this union's own pointer null, and it is + // never looked at or freed a second time. basic_json cur; cur.m_data.m_type = t; using std::swap; swap(cur.m_data.m_value, *this); + destroy_bounded(cur, 0); + } + + // Destroy the array/object v, whose children are destroyed first. + // The first recursion_depth_limit() levels are destroyed by plain + // recursion, which is the fastest way and cannot throw; anything + // nested deeper is handed to destroy_walk(), which uses no call stack. + static void destroy_bounded(basic_json& v, std::size_t depth) noexcept + { + if (JSON_HEDLEY_UNLIKELY(depth >= detail::recursion_depth_limit())) + { + destroy_walk(v); + return; + } + + if (v.m_data.m_type == value_t::array) + { + for (auto& child : *v.m_data.m_value.array) + { + if (child.m_data.m_type == value_t::array || child.m_data.m_type == value_t::object) + { + destroy_bounded(child, depth + 1); + } + } + } + else + { + for (auto& child : *v.m_data.m_value.object) + { + if (child.second.m_data.m_type == value_t::array || child.second.m_data.m_type == value_t::object) + { + destroy_bounded(child.second, depth + 1); + } + } + } + + // only leaves and already destroyed (null) children remain + free_container(v); + } + + // Destroy the tree below top without recursing per nesting level and + // without any heap allocation: a heap-allocated flattening + // stack (the previous implementation) can itself throw + // bad_alloc, which would escape this noexcept destructor and + // terminate the program (#5135). + // + // Instead, walk down a chain of children (always the one + // walk_child() picks), reversing links as we go: cur is the + // container currently being emptied, and prev is its parent + // (value_t::null when there is none). Each parent's + // walk_child() slot doubles as storage for that parent's own + // parent link while we are below it, so no extra memory is + // needed; the parent is not modified meanwhile, so + // walk_child() finds that same slot again on the way up. We + // only ever remove a child once it is a scalar or an empty + // array/object, which neither allocates nor recurses more + // than one level deep. + // + // This json_value is not itself a basic_json, so the + // top-level container is first moved into a local stand-in + // ("cur"); a default-constructed basic_json has a null + // pointer in its m_value (see data::m_value's initializer), + // so swapping it with *this leaves this union's own pointer + // null, and it is never looked at or freed a second time. + static void destroy_walk(basic_json& top) noexcept + { + basic_json cur; + take(cur, top); + basic_json prev; // value_t::null: no parent // true right after cur was entered: its children have not been