mirror of
https://github.com/nlohmann/json.git
synced 2026-10-22 14:08:13 +00:00
Look up the keys of big ordered_json objects in a hash table
ordered_map keeps its elements in a vector and searches it linearly for every key it adds, so parsing an object with n keys into ordered_json took O(n^2) key comparisons: 33 billion instructions for one object with 30 000 keys, where json needs 245 million. From 64 keys on, the DOM parser now keeps a hash table of the keys of the open object (hashes and positions only; the elements stay in the vector) and finds duplicate keys there. The result is the same, including the position of a key that occurs twice. Smaller objects, std::map based objects and key types without std::hash are untouched. ordered_json parse, macOS instructions: object with 1 000 keys 64.8 M -> 14.6 M, 10 000 keys 5 104 M -> 38.7 M, 30 000 keys 33 358 M -> 89 M. Linux Ir of an object with 2 000 keys: 43.0 M -> 1.9 M (GCC), 37.9 M -> 1.8 M (Clang). Signed-off-by: Niels Lohmann <mail@nlohmann.me>
This commit is contained in:
2 files changed
+321
-8
No files matched your search
@@ -10,7 +10,9 @@
|
||||
|
||||
#include <algorithm> // find_if, min
|
||||
#include <cstddef>
|
||||
#include <functional> // hash
|
||||
#include <limits> // numeric_limits
|
||||
#include <memory> // unique_ptr
|
||||
#include <string> // string
|
||||
#include <type_traits> // enable_if_t
|
||||
#include <utility> // move, pair
|
||||
@@ -20,6 +22,7 @@
|
||||
#include <nlohmann/detail/input/lexer.hpp>
|
||||
#include <nlohmann/detail/macro_scope.hpp>
|
||||
#include <nlohmann/detail/meta/cpp_future.hpp>
|
||||
#include <nlohmann/detail/meta/detected.hpp>
|
||||
#include <nlohmann/detail/string_concat.hpp>
|
||||
NLOHMANN_JSON_NAMESPACE_BEGIN
|
||||
|
||||
@@ -315,6 +318,90 @@ struct diagnostic_positions
|
||||
};
|
||||
#endif
|
||||
|
||||
/*!
|
||||
@brief a hash table that finds the position of a key in an object whose
|
||||
elements are kept in a vector (ordered_map)
|
||||
|
||||
The elements stay where they are: the table only holds their hashes and
|
||||
positions, and the caller compares the keys.
|
||||
*/
|
||||
class object_key_index
|
||||
{
|
||||
public:
|
||||
static constexpr std::size_t npos = static_cast<std::size_t>(-1);
|
||||
|
||||
/// @param[in] element_hashes the hashes of the keys of the elements that are there already
|
||||
explicit object_key_index(std::vector<std::size_t>&& element_hashes)
|
||||
: hashes(std::move(element_hashes))
|
||||
{
|
||||
std::size_t size = 256;
|
||||
while (size < 4 * hashes.size())
|
||||
{
|
||||
size *= 2;
|
||||
}
|
||||
rebuild(size);
|
||||
}
|
||||
|
||||
/// @return the position of the element with @a hash for which @a equal(position) holds, or npos
|
||||
template<typename Equal>
|
||||
std::size_t find(const std::size_t hash, Equal equal) const
|
||||
{
|
||||
for (std::size_t slot = hash & mask;; slot = (slot + 1) & mask)
|
||||
{
|
||||
const std::size_t entry = table[slot];
|
||||
if (entry == 0)
|
||||
{
|
||||
return npos;
|
||||
}
|
||||
if (hashes[entry - 1] == hash && equal(entry - 1))
|
||||
{
|
||||
return entry - 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// add the next element, whose position is the number of elements so far
|
||||
void add(const std::size_t hash)
|
||||
{
|
||||
hashes.push_back(hash);
|
||||
if (hashes.size() * 2 > table.size())
|
||||
{
|
||||
rebuild(table.size() * 2);
|
||||
}
|
||||
else
|
||||
{
|
||||
place(hash, hashes.size());
|
||||
}
|
||||
}
|
||||
|
||||
private:
|
||||
void rebuild(const std::size_t size)
|
||||
{
|
||||
table.assign(size, 0);
|
||||
mask = size - 1;
|
||||
for (std::size_t i = 0; i < hashes.size(); ++i)
|
||||
{
|
||||
place(hashes[i], i + 1);
|
||||
}
|
||||
}
|
||||
|
||||
void place(const std::size_t hash, const std::size_t entry)
|
||||
{
|
||||
std::size_t slot = hash & mask;
|
||||
while (table[slot] != 0)
|
||||
{
|
||||
slot = (slot + 1) & mask;
|
||||
}
|
||||
table[slot] = entry;
|
||||
}
|
||||
|
||||
/// the hash of each element, by position
|
||||
std::vector<std::size_t> hashes;
|
||||
/// open addressing, a power of two in size: position + 1 of an element, or 0 for a free slot
|
||||
std::vector<std::size_t> table {};
|
||||
std::size_t mask = 0;
|
||||
};
|
||||
|
||||
/*!
|
||||
@brief SAX implementation to create a JSON value from SAX events
|
||||
|
||||
@@ -409,7 +496,7 @@ class json_sax_dom_parser
|
||||
{
|
||||
BasicJsonType* const ref = store(BasicJsonType(BasicJsonType::value_t::object));
|
||||
reserve_frames();
|
||||
frames.push_back(frame{ref->m_data.m_value.object, nullptr, ref, 0});
|
||||
frames.push_back(frame{ref->m_data.m_value.object, nullptr, ref, 0, nullptr});
|
||||
in_object = true;
|
||||
stored_array = nullptr;
|
||||
|
||||
@@ -428,7 +515,7 @@ class json_sax_dom_parser
|
||||
|
||||
// add null at the given key and store the reference for later; the
|
||||
// key is moved, as the lexer starts the next token with reset()
|
||||
object_element = &(frames.back().object->operator[](std::move(val)));
|
||||
object_element = insert_key(frames.back(), std::move(val), std::integral_constant<bool, indexed_keys> {});
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -448,6 +535,10 @@ class json_sax_dom_parser
|
||||
ref->set_parents();
|
||||
#endif
|
||||
|
||||
if (JSON_HEDLEY_UNLIKELY(frames.back().index != nullptr))
|
||||
{
|
||||
key_indexes.pop_back();
|
||||
}
|
||||
frames.pop_back();
|
||||
enter_enclosing_container();
|
||||
return true;
|
||||
@@ -490,7 +581,7 @@ class json_sax_dom_parser
|
||||
}
|
||||
reserve_frames();
|
||||
reserve_elements();
|
||||
frames.push_back(frame{nullptr, nullptr, slot, elements.size()});
|
||||
frames.push_back(frame{nullptr, nullptr, slot, elements.size(), nullptr});
|
||||
in_object = false;
|
||||
stored_array = nullptr;
|
||||
}
|
||||
@@ -572,8 +663,71 @@ class json_sax_dom_parser
|
||||
BasicJsonType* slot;
|
||||
/// 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;
|
||||
};
|
||||
|
||||
/// 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 number of keys from which an object looks them up in a hash table
|
||||
static constexpr std::size_t key_index_threshold = 64;
|
||||
|
||||
/// add @a key to the object of @a f (or find it, if it is there), searching it in std::map
|
||||
static BasicJsonType* insert_key(frame& f, string_t&& key, std::false_type /*indexed*/)
|
||||
{
|
||||
return &f.object->operator[](std::move(key));
|
||||
}
|
||||
|
||||
/// add @a key to the object of @a f (or find it, if it is there), searching it in a vector
|
||||
BasicJsonType* insert_key(frame& f, string_t&& key, std::true_type /*indexed*/)
|
||||
{
|
||||
object_t& obj = *f.object;
|
||||
if (JSON_HEDLEY_LIKELY(obj.size() < key_index_threshold))
|
||||
{
|
||||
return &obj[std::move(key)];
|
||||
}
|
||||
return insert_key_with_index(f, std::move(key));
|
||||
}
|
||||
|
||||
/// add @a key to the big object of @a f (or find it, if it is there), using its key index
|
||||
JSON_HEDLEY_NEVER_INLINE
|
||||
BasicJsonType* insert_key_with_index(frame& f, string_t&& key)
|
||||
{
|
||||
object_t& obj = *f.object;
|
||||
const std::hash<string_t> hasher {};
|
||||
const std::size_t hash = hasher(key);
|
||||
if (JSON_HEDLEY_UNLIKELY(f.index == nullptr))
|
||||
{
|
||||
std::vector<std::size_t> hashes;
|
||||
hashes.reserve(obj.size());
|
||||
for (const auto& element : obj)
|
||||
{
|
||||
hashes.push_back(hasher(element.first));
|
||||
}
|
||||
key_indexes.emplace_back(new object_key_index(std::move(hashes)));
|
||||
f.index = key_indexes.back().get();
|
||||
}
|
||||
|
||||
const std::size_t position = f.index->find(hash, [&obj, &key](std::size_t i)
|
||||
{
|
||||
return (obj.begin() + static_cast<std::ptrdiff_t>(i))->first == key;
|
||||
});
|
||||
if (position != object_key_index::npos)
|
||||
{
|
||||
return &(obj.begin() + static_cast<std::ptrdiff_t>(position))->second;
|
||||
}
|
||||
|
||||
obj.emplace_new_key(key, BasicJsonType());
|
||||
f.index->add(hash);
|
||||
return &obj.back().second;
|
||||
}
|
||||
|
||||
/// the number of elements an array collects on the stack before it
|
||||
/// switches to storing them directly, so that it does not hold up the
|
||||
/// stack with its elements (and move all of them in the end)
|
||||
@@ -600,7 +754,7 @@ class json_sax_dom_parser
|
||||
{
|
||||
BasicJsonType* const ref = store(BasicJsonType(BasicJsonType::value_t::array));
|
||||
reserve_frames();
|
||||
frames.push_back(frame{nullptr, ref->m_data.m_value.array, ref, 0});
|
||||
frames.push_back(frame{nullptr, ref->m_data.m_value.array, ref, 0, nullptr});
|
||||
in_object = false;
|
||||
stored_array = ref->m_data.m_value.array;
|
||||
|
||||
@@ -725,6 +879,8 @@ class json_sax_dom_parser
|
||||
std::vector<frame> 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
|
||||
std::vector<std::unique_ptr<object_key_index>> key_indexes {};
|
||||
/// helper to hold the reference for the next object element
|
||||
BasicJsonType* object_element = nullptr;
|
||||
/// whether the innermost container is an object (frames.back().object)
|
||||
|
||||
Reference in new issue
Block a user