From f3ae7f618eddde304b9dca21b7680b7897b3951c Mon Sep 17 00:00:00 2001 From: Niels Lohmann Date: Fri, 16 Oct 2026 14:58:16 +0200 Subject: [PATCH] Flatten into a std::map by inserting next to the previous entry 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 --- include/nlohmann/detail/json_pointer.hpp | 46 +++++++++++++++++++----- single_include/nlohmann/json.hpp | 46 +++++++++++++++++++----- 2 files changed, 74 insertions(+), 18 deletions(-) diff --git a/include/nlohmann/detail/json_pointer.hpp b/include/nlohmann/detail/json_pointer.hpp index 63ab49f7d..0608e023c 100644 --- a/include/nlohmann/detail/json_pointer.hpp +++ b/include/nlohmann/detail/json_pointer.hpp @@ -1135,20 +1135,45 @@ class json_pointer // Add the entry (key, v) to the object `obj`. Flattened paths are pairwise // distinct, so objects that can append a key known to be absent // (ordered_map) skip the lookup, which is linear and would make flatten - // quadratic; all other object types use operator[]. + // quadratic. Objects with a positional insert (std::map) insert next to + // the entry added last: paths come out in nearly sorted order, so the + // position is usually right and no search from the root is needed. + // All other object types use operator[]. template - static auto add_flattened(ObjectType& obj, const string_t& key, ValueType&& v, detail::priority_tag<1> /*unused*/) + static auto add_flattened(ObjectType& obj, typename ObjectType::iterator& /*hint*/, const string_t& key, ValueType&& v, detail::priority_tag<2> /*unused*/) -> decltype(obj.emplace_new_key(key, std::forward(v)), void()) { obj.emplace_new_key(key, std::forward(v)); } template - static void add_flattened(ObjectType& obj, const string_t& key, ValueType&& v, detail::priority_tag<0> /*unused*/) + static auto add_flattened(ObjectType& obj, typename ObjectType::iterator& hint, const string_t& key, ValueType&& v, detail::priority_tag<1> /*unused*/) + -> decltype(hint = obj.emplace_hint(hint, key, std::forward(v)), void()) + { + hint = obj.emplace_hint(hint, key, std::forward(v)); + ++hint; + } + + template + static void add_flattened(ObjectType& obj, typename ObjectType::iterator& /*hint*/, const string_t& key, ValueType&& v, detail::priority_tag<0> /*unused*/) { obj[key] = std::forward(v); } + /// 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) + std::size_t pos = sizeof(buf); + do + { + buf[--pos] = static_cast('0' + i % 10); + i /= 10; + } + while (i != 0); + out.append(buf + pos, sizeof(buf) - pos); + } + /*! @param[in] reference_string the reference string to the current value @param[in] value the value to consider @@ -1194,7 +1219,8 @@ class json_pointer // flatten `v`, whose path is `path`: primitives and empty containers // are added to the result right away; other containers get a frame auto& object = *result.m_data.m_value.object; - const auto enter = [&stack, &path, &object](const BasicJsonType & v) + typename BasicJsonType::object_t::iterator hint = object.end(); + const auto enter = [&stack, &path, &object, &hint](const BasicJsonType & v) { switch (v.type()) { @@ -1203,7 +1229,7 @@ class json_pointer if (v.m_data.m_value.array->empty()) { // flatten empty array as null - add_flattened(object, path, nullptr, detail::priority_tag<1> {}); + add_flattened(object, hint, path, nullptr, detail::priority_tag<2> {}); } else { @@ -1217,7 +1243,7 @@ class json_pointer if (v.m_data.m_value.object->empty()) { // flatten empty object as null - add_flattened(object, path, nullptr, detail::priority_tag<1> {}); + add_flattened(object, hint, path, nullptr, detail::priority_tag<2> {}); } else { @@ -1237,7 +1263,7 @@ class json_pointer default: { // add a primitive value with its reference string - add_flattened(object, path, v, detail::priority_tag<1> {}); + add_flattened(object, hint, path, v, detail::priority_tag<2> {}); return; } } @@ -1265,7 +1291,8 @@ class json_pointer // iterate array and use index as a reference string ++stack.back().index; - detail::concat_into(path, '/', detail::to_string(i)); + path.push_back('/'); + append_index(path, i); enter(array[i]); } else @@ -1279,7 +1306,8 @@ class json_pointer // iterate object and use keys as reference string ++stack.back().member; - detail::concat_into(path, '/', detail::escape(it->first)); + path.push_back('/'); + detail::escape_into(path, it->first); enter(it->second); } } diff --git a/single_include/nlohmann/json.hpp b/single_include/nlohmann/json.hpp index 4debe551f..6fdf062cc 100644 --- a/single_include/nlohmann/json.hpp +++ b/single_include/nlohmann/json.hpp @@ -22107,20 +22107,45 @@ class json_pointer // Add the entry (key, v) to the object `obj`. Flattened paths are pairwise // distinct, so objects that can append a key known to be absent // (ordered_map) skip the lookup, which is linear and would make flatten - // quadratic; all other object types use operator[]. + // quadratic. Objects with a positional insert (std::map) insert next to + // the entry added last: paths come out in nearly sorted order, so the + // position is usually right and no search from the root is needed. + // All other object types use operator[]. template - static auto add_flattened(ObjectType& obj, const string_t& key, ValueType&& v, detail::priority_tag<1> /*unused*/) + static auto add_flattened(ObjectType& obj, typename ObjectType::iterator& /*hint*/, const string_t& key, ValueType&& v, detail::priority_tag<2> /*unused*/) -> decltype(obj.emplace_new_key(key, std::forward(v)), void()) { obj.emplace_new_key(key, std::forward(v)); } template - static void add_flattened(ObjectType& obj, const string_t& key, ValueType&& v, detail::priority_tag<0> /*unused*/) + static auto add_flattened(ObjectType& obj, typename ObjectType::iterator& hint, const string_t& key, ValueType&& v, detail::priority_tag<1> /*unused*/) + -> decltype(hint = obj.emplace_hint(hint, key, std::forward(v)), void()) + { + hint = obj.emplace_hint(hint, key, std::forward(v)); + ++hint; + } + + template + static void add_flattened(ObjectType& obj, typename ObjectType::iterator& /*hint*/, const string_t& key, ValueType&& v, detail::priority_tag<0> /*unused*/) { obj[key] = std::forward(v); } + /// 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) + std::size_t pos = sizeof(buf); + do + { + buf[--pos] = static_cast('0' + i % 10); + i /= 10; + } + while (i != 0); + out.append(buf + pos, sizeof(buf) - pos); + } + /*! @param[in] reference_string the reference string to the current value @param[in] value the value to consider @@ -22166,7 +22191,8 @@ class json_pointer // flatten `v`, whose path is `path`: primitives and empty containers // are added to the result right away; other containers get a frame auto& object = *result.m_data.m_value.object; - const auto enter = [&stack, &path, &object](const BasicJsonType & v) + typename BasicJsonType::object_t::iterator hint = object.end(); + const auto enter = [&stack, &path, &object, &hint](const BasicJsonType & v) { switch (v.type()) { @@ -22175,7 +22201,7 @@ class json_pointer if (v.m_data.m_value.array->empty()) { // flatten empty array as null - add_flattened(object, path, nullptr, detail::priority_tag<1> {}); + add_flattened(object, hint, path, nullptr, detail::priority_tag<2> {}); } else { @@ -22189,7 +22215,7 @@ class json_pointer if (v.m_data.m_value.object->empty()) { // flatten empty object as null - add_flattened(object, path, nullptr, detail::priority_tag<1> {}); + add_flattened(object, hint, path, nullptr, detail::priority_tag<2> {}); } else { @@ -22209,7 +22235,7 @@ class json_pointer default: { // add a primitive value with its reference string - add_flattened(object, path, v, detail::priority_tag<1> {}); + add_flattened(object, hint, path, v, detail::priority_tag<2> {}); return; } } @@ -22237,7 +22263,8 @@ class json_pointer // iterate array and use index as a reference string ++stack.back().index; - detail::concat_into(path, '/', detail::to_string(i)); + path.push_back('/'); + append_index(path, i); enter(array[i]); } else @@ -22251,7 +22278,8 @@ class json_pointer // iterate object and use keys as reference string ++stack.back().member; - detail::concat_into(path, '/', detail::escape(it->first)); + path.push_back('/'); + detail::escape_into(path, it->first); enter(it->second); } }