Trim the work the DOM parser does per container and per member

Opening and closing an array or object cost the DOM parser more than before
the exact-size array change, which showed on documents of many small
objects (jeopardy.json: +1.4 % Ir with GCC). This takes it back:

- the stack of open containers keeps its first eight entries in the parser
  object instead of in a vector, so it needs no capacity check beyond the
  push itself and no allocation for typical documents;
- a container is created in the place it ends up in, instead of as a
  temporary that is then moved there;
- a new object member, which is null, is constructed over the null instead
  of being swapped with it (only with the default base class of basic_json);
- the key index that big ordered_json objects use is only a part of the
  stack entries of objects that can have one;
- an array that outgrows the stack reserves twice what it has so far, as a
  vector that grew by itself would have.

DOM parse Ir of the series up to here against develop, Linux GCC / Clang:
canada -6.6 % / -7.3 %, citm +1.2 % / +0.2 %, jeopardy +0.4 % / -0.1 %,
124-byte object -6.5 % / -6.2 %. (twitter measures +6.8 % / +6.6 % at this
point in steady state, almost all of it in glibc's malloc/consolidate for the
new allocation sizes; with the last commit of the series it is -0.1 % / 0.0 %.)

Signed-off-by: Niels Lohmann <mail@nlohmann.me>
This commit is contained in:
Niels Lohmann committed 2026-10-22 09:28:00 +02:00
1 parent 5b08041308
commit ffa9babc8d
2 files changed
+401 -116

No files matched your search

+174 -32
View File
@@ -8,11 +8,12 @@
#pragma once
#include <algorithm> // find_if, min
#include <algorithm> // copy, find_if, min
#include <cstddef>
#include <functional> // hash
#include <limits> // numeric_limits
#include <memory> // unique_ptr
#include <new> // operator new
#include <string> // string
#include <type_traits> // enable_if_t
#include <utility> // move, pair
@@ -20,6 +21,7 @@
#include <nlohmann/detail/exceptions.hpp>
#include <nlohmann/detail/input/lexer.hpp>
#include <nlohmann/detail/json_custom_base_class.hpp>
#include <nlohmann/detail/macro_scope.hpp>
#include <nlohmann/detail/meta/cpp_future.hpp>
#include <nlohmann/detail/meta/detected.hpp>
@@ -494,9 +496,8 @@ class json_sax_dom_parser
bool start_object(std::size_t len)
{
BasicJsonType* const ref = store(BasicJsonType(BasicJsonType::value_t::object));
reserve_frames();
frames.push_back(frame{ref->m_data.m_value.object, nullptr, ref, 0, nullptr});
BasicJsonType* const ref = store(BasicJsonType::value_t::object);
frames.push_back(frame{ref->m_data.m_value.object, nullptr, ref, 0, key_index_pointer()});
in_object = true;
stored_array = nullptr;
@@ -535,10 +536,7 @@ class json_sax_dom_parser
ref->set_parents();
#endif
if (JSON_HEDLEY_UNLIKELY(frames.back().index != nullptr))
{
key_indexes.pop_back();
}
release_key_index(frames.back(), std::integral_constant<bool, indexed_keys> {});
frames.pop_back();
enter_enclosing_container();
return true;
@@ -579,9 +577,8 @@ class json_sax_dom_parser
{
slot = object_element;
}
reserve_frames();
reserve_elements();
frames.push_back(frame{nullptr, nullptr, slot, elements.size(), nullptr});
frames.push_back(frame{nullptr, nullptr, slot, elements.size(), key_index_pointer()});
in_object = false;
stored_array = nullptr;
}
@@ -649,6 +646,19 @@ class json_sax_dom_parser
using object_t = typename BasicJsonType::object_t;
using array_t = typename BasicJsonType::array_t;
/// whether objects keep their elements in a vector and a key can be hashed,
/// so that a big object can find the keys it has in a hash table instead of
/// searching all of them
template<typename ObjectType, typename StringType>
using detect_indexable_keys = decltype(std::declval<ObjectType&>().emplace_new_key(std::declval<const StringType&>(), std::declval<typename ObjectType::mapped_type>()),
std::hash<StringType>()(std::declval<const StringType&>()));
static constexpr bool indexed_keys = is_detected<detect_indexable_keys, object_t, string_t>::value;
/// what a frame holds of the key index of its object: a pointer for objects
/// that have such an index, nothing for the others
struct no_key_index {};
using key_index_pointer = typename std::conditional<indexed_keys, object_key_index*, no_key_index>::type;
/// an array or object that is being read
struct frame
{
@@ -664,16 +674,131 @@ class json_sax_dom_parser
/// array without `array`: the index of its first element in `elements`
std::size_t start;
/// object with many keys in a vector: its key_indexes entry, else nullptr
object_key_index* index;
key_index_pointer index;
};
/// whether objects keep their elements in a vector and a key can be hashed,
/// so that a big object can find the keys it has in a hash table instead of
/// searching all of them
template<typename ObjectType, typename StringType>
using detect_indexable_keys = decltype(std::declval<ObjectType&>().emplace_new_key(std::declval<const StringType&>(), std::declval<typename ObjectType::mapped_type>()),
std::hash<StringType>()(std::declval<const StringType&>()));
static constexpr bool indexed_keys = is_detected<detect_indexable_keys, object_t, string_t>::value;
/// the open arrays and objects; the first few live in the object itself, so
/// that the common documents do not allocate for them
class frame_stack
{
public:
frame_stack() = default;
frame_stack(const frame_stack&) = delete;
frame_stack& operator=(const frame_stack&) = delete;
frame_stack(frame_stack&& other) noexcept
{
take(other);
}
frame_stack& operator=(frame_stack&& other) noexcept
{
if (this != &other)
{
release();
take(other);
}
return *this;
}
~frame_stack()
{
release();
}
bool empty() const noexcept
{
return count == 0;
}
std::size_t size() const noexcept
{
return count;
}
frame& back() noexcept
{
return data[count - 1];
}
frame& operator[](const std::size_t i) noexcept
{
return data[i];
}
void push_back(const frame& f)
{
if (JSON_HEDLEY_UNLIKELY(count == capacity))
{
grow();
}
data[count++] = f;
}
void pop_back() noexcept
{
JSON_ASSERT(count > 0);
--count;
}
private:
JSON_HEDLEY_NEVER_INLINE
void grow()
{
const std::size_t new_capacity = capacity * 2;
frame* const new_data = new frame[new_capacity];
std::copy(data, data + count, new_data);
release();
data = new_data;
capacity = new_capacity;
}
void release() noexcept
{
if (data != local)
{
delete[] data;
data = local;
capacity = local_capacity;
}
}
/// make the contents of @a other our own
void take(frame_stack& other) noexcept
{
if (other.data == other.local)
{
std::copy(other.local, other.local + other.count, local);
data = local;
capacity = local_capacity;
}
else
{
data = other.data;
capacity = other.capacity;
other.data = other.local;
other.capacity = local_capacity;
}
count = other.count;
other.count = 0;
}
static constexpr std::size_t local_capacity = 8;
frame local[local_capacity];
frame* data = local;
std::size_t count = 0;
std::size_t capacity = local_capacity;
};
void release_key_index(const frame& f, std::true_type /*indexed*/)
{
if (JSON_HEDLEY_UNLIKELY(f.index != nullptr))
{
key_indexes.pop_back();
}
}
static void release_key_index(const frame& /*f*/, std::false_type /*indexed*/) noexcept {}
/// the number of keys from which an object looks them up in a hash table
static constexpr std::size_t key_index_threshold = 64;
@@ -733,14 +858,6 @@ class json_sax_dom_parser
/// stack with its elements (and move all of them in the end)
static constexpr std::size_t buffer_limit = 16;
void reserve_frames()
{
if (JSON_HEDLEY_UNLIKELY(frames.capacity() == 0))
{
frames.reserve(16);
}
}
void reserve_elements()
{
if (JSON_HEDLEY_UNLIKELY(elements.capacity() == 0))
@@ -752,9 +869,8 @@ class json_sax_dom_parser
/// store the elements of a new array directly in it
void start_stored_array(const std::size_t len)
{
BasicJsonType* const ref = store(BasicJsonType(BasicJsonType::value_t::array));
reserve_frames();
frames.push_back(frame{nullptr, ref->m_data.m_value.array, ref, 0, nullptr});
BasicJsonType* const ref = store(BasicJsonType::value_t::array);
frames.push_back(frame{nullptr, ref->m_data.m_value.array, ref, 0, key_index_pointer()});
in_object = false;
stored_array = ref->m_data.m_value.array;
@@ -781,7 +897,7 @@ class json_sax_dom_parser
if (in_object)
{
JSON_ASSERT(object_element);
*object_element = BasicJsonType(std::forward<Value>(v));
set_member(*object_element, std::forward<Value>(v), std::is_same<typename BasicJsonType::json_base_class_t, json_default_base> {});
return object_element;
}
@@ -818,7 +934,8 @@ class json_sax_dom_parser
const auto first = elements.begin() + static_cast<std::ptrdiff_t>(f.start);
BasicJsonType value(BasicJsonType::value_t::array);
array_t& arr = *value.m_data.m_value.array;
reserve_array(arr, 4 * buffer_limit, priority_tag<1> {});
// as much room as the array would have after one more doubling
reserve_array(arr, 2 * (elements.size() - f.start), priority_tag<1> {});
arr.insert(arr.end(), std::make_move_iterator(first), std::make_move_iterator(elements.end()));
elements.erase(first, elements.end());
f.array = &arr;
@@ -845,6 +962,31 @@ class json_sax_dom_parser
return &arr.back();
}
/// set the value of the member key() just added
template<typename Value>
JSON_HEDLEY_ALWAYS_INLINE
static void set_member(BasicJsonType& member, Value&& v, std::true_type /*default base*/)
{
if (JSON_HEDLEY_LIKELY(member.is_null()))
{
// a new member is null and holds nothing: construct the value
// over it instead of destroying and swapping
::new (static_cast<void*>(&member)) BasicJsonType(std::forward<Value>(v));
}
else
{
// the key occurred before
member = BasicJsonType(std::forward<Value>(v));
}
}
/// a custom base class may care about the lifetime of the value
template<typename Value>
static void set_member(BasicJsonType& member, Value&& v, std::false_type /*custom base*/)
{
member = BasicJsonType(std::forward<Value>(v));
}
/// after a container was closed: the one it was in is the one that gets the next value
void enter_enclosing_container()
{
@@ -876,7 +1018,7 @@ class json_sax_dom_parser
/// the parsed JSON value
BasicJsonType& root;
/// the open arrays and objects
std::vector<frame> frames {};
frame_stack frames {};
/// the elements of the open arrays that are not stored in them yet
std::vector<BasicJsonType> elements {};
/// the key indexes of the open objects that have one, innermost last