mirror of
https://github.com/simdjson/simdjson
synced 2026-06-08 17:27:07 +00:00
Compare commits
5 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 263e58d9d7 | |||
| ed345772cd | |||
| 92123faa7d | |||
| 12518a278a | |||
| dc9a8e2890 |
+20
-32
@@ -1528,10 +1528,6 @@ void f() {
|
||||
```
|
||||
|
||||
|
||||
**Performance tip**: You will get better performance if you order the attributes (make, model)
|
||||
in the order they appear in the JSON document. Alternatively, [key selectors](#key-selectors)
|
||||
let you extract a fixed, known set of fields in a single pass, independently of the
|
||||
order in which they appear in the document.
|
||||
|
||||
#### Special cases
|
||||
|
||||
@@ -1684,29 +1680,23 @@ std::map<std::string, std::string> obj =
|
||||
|
||||
The `simdjson::from` construction is EXPERIMENTAL and subject to changes.
|
||||
|
||||
### Order-independent reflective deserialization (experimental)
|
||||
### Order-independent reflective deserialization
|
||||
|
||||
> **Experimental and opt-in.** This feature is disabled by default. Enable it by
|
||||
> defining the macro `SIMDJSON_USE_KEY_SELECTOR_REFLECTION=1` before including
|
||||
> simdjson (e.g. as a compiler flag `-DSIMDJSON_USE_KEY_SELECTOR_REFLECTION=1`).
|
||||
> It requires C++26 static reflection (`SIMDJSON_STATIC_REFLECTION`).
|
||||
> **Default.** Reflective deserialization uses an order-independent, single-pass
|
||||
> key-selector strategy by default. You can opt out — falling back to the
|
||||
> per-member ordered-lookup path — by defining the macro
|
||||
> `SIMDJSON_DISABLE_KEY_SELECTOR_REFLECTION=1` before including simdjson (e.g. as
|
||||
> a compiler flag `-DSIMDJSON_DISABLE_KEY_SELECTOR_REFLECTION=1`). It requires
|
||||
> C++26 static reflection (`SIMDJSON_STATIC_REFLECTION`).
|
||||
|
||||
By default, reflective deserialization (`doc.get<T>()` / `simdjson::from`) reads
|
||||
each struct member with an ordered field lookup. This is fastest when the JSON
|
||||
keys appear in the same order as the struct's members, which is the common case.
|
||||
|
||||
When you genuinely cannot rely on the JSON key order — for example when the data
|
||||
comes from a producer that emits members in an arbitrary or varying order — the
|
||||
ordered lookups degrade, because each out-of-order key forces a rescan of the
|
||||
object. For that situation simdjson offers an optional, order-independent
|
||||
strategy: when `SIMDJSON_USE_KEY_SELECTOR_REFLECTION=1` is defined, the
|
||||
reflective deserializer builds a compile-time [key selector](#key-selectors)
|
||||
from the struct's members and walks each object **once** with
|
||||
`object::for_each`, classifying every key through a perfect hash regardless of
|
||||
its position.
|
||||
By default, reflective deserialization (`doc.get<T>()` / `simdjson::from`) builds
|
||||
a compile-time [key selector](#key-selectors) from the struct's members and walks
|
||||
each object **once** with `object::for_each`, classifying every key through a
|
||||
perfect hash regardless of its position. This deserializes structs correctly even
|
||||
when the JSON keys are not in declaration order, without forcing a rescan of the
|
||||
object per out-of-order key.
|
||||
|
||||
```cpp
|
||||
// Compile this translation unit with -DSIMDJSON_USE_KEY_SELECTOR_REFLECTION=1
|
||||
#include "simdjson.h"
|
||||
using namespace simdjson;
|
||||
|
||||
@@ -1721,17 +1711,15 @@ auto json = R"({ "retweet_count": 7, "id": 12345, "text": "hello" })"_padded;
|
||||
ondemand::parser parser;
|
||||
ondemand::document doc = parser.iterate(json);
|
||||
Tweet t;
|
||||
auto error = doc.get(t); // uses the key-selector path under the macro
|
||||
auto error = doc.get(t); // uses the key-selector path by default
|
||||
```
|
||||
|
||||
**This is not a universal speedup — run your own benchmarks before adopting it.**
|
||||
In our measurements the key-selector path is roughly on par with the default on
|
||||
small structs (e.g. the Twitter user/tweet objects) but **slower** on documents
|
||||
dominated by many small nested objects (e.g. the CITM catalog), because walking
|
||||
every field of every object and hashing each key costs more than ordered,
|
||||
short-circuiting lookups when the keys *are* in order. Only consider enabling the
|
||||
macro when (a) your inputs really do present keys out of order, and (b) your own
|
||||
benchmarks on your own data show a win. Otherwise, leave it off.
|
||||
The alternative, opt-out path reads each struct member with an ordered field
|
||||
lookup. This can edge ahead when the JSON keys reliably match declaration order,
|
||||
which is a common case. **Neither path is a universal speedup — run your own
|
||||
benchmarks before switching.** Throughput numbers carry some run-to-run noise, so
|
||||
measure on your own data before defining
|
||||
`-DSIMDJSON_DISABLE_KEY_SELECTOR_REFLECTION=1`.
|
||||
|
||||
|
||||
Minifying JSON strings without parsing
|
||||
|
||||
@@ -327,11 +327,9 @@ consteval bool try_generate_gperf(
|
||||
std::array<std::size_t, N> phash{};
|
||||
for (std::size_t k = 0; k < N; ++k) { phash[k] = keys[k].size(); }
|
||||
|
||||
// Equivalence-class signatures: sig[k] = XOR of a per-symbol salt over the
|
||||
// key's undetermined symbols. Updated incrementally as symbols are fixed.
|
||||
std::array<std::array<std::size_t, 256>, MAX_POSITIONS> salt{};
|
||||
std::array<std::array<uint64_t, 256>, MAX_POSITIONS> salt{};
|
||||
{
|
||||
std::size_t s = 0x9e3779b97f4a7c15ULL;
|
||||
uint64_t s = 0x9e3779b97f4a7c15ULL;
|
||||
for (std::size_t p = 0; p < num_positions; ++p) {
|
||||
for (std::size_t c = 0; c < 256; ++c) {
|
||||
s = s * 6364136223846793005ULL + 1442695040888963407ULL;
|
||||
@@ -339,9 +337,9 @@ consteval bool try_generate_gperf(
|
||||
}
|
||||
}
|
||||
}
|
||||
std::array<std::size_t, N> sig{};
|
||||
std::array<uint64_t, N> sig{};
|
||||
for (std::size_t k = 0; k < N; ++k) {
|
||||
std::size_t s = 0;
|
||||
uint64_t s = 0;
|
||||
for (std::size_t p = 0; p < num_positions; ++p) {
|
||||
std::size_t c = kchars[k][p];
|
||||
if (c < 256) { s ^= salt[p][c]; }
|
||||
@@ -361,14 +359,14 @@ consteval bool try_generate_gperf(
|
||||
std::size_t sp = syms[si].pos;
|
||||
std::size_t sc = syms[si].ch;
|
||||
|
||||
std::size_t sp_salt = salt[sp][sc];
|
||||
uint64_t sp_salt = salt[sp][sc];
|
||||
for (std::size_t k = 0; k < N; ++k) {
|
||||
if (kchars[k][sp] == sc) { sig[k] ^= sp_salt; }
|
||||
}
|
||||
|
||||
for (std::size_t i = 1; i < N; ++i) {
|
||||
std::size_t x = order[i];
|
||||
std::size_t xs = sig[x];
|
||||
uint64_t xs = sig[x];
|
||||
std::size_t j = i;
|
||||
while (j > 0 && sig[order[j - 1]] > xs) {
|
||||
order[j] = order[j - 1];
|
||||
@@ -382,7 +380,7 @@ consteval bool try_generate_gperf(
|
||||
bool collision = false;
|
||||
std::size_t ci = 0;
|
||||
while (ci < N && !collision) {
|
||||
std::size_t class_sig = sig[order[ci]];
|
||||
uint64_t class_sig = sig[order[ci]];
|
||||
std::size_t cj = ci;
|
||||
while (cj < N && sig[order[cj]] == class_sig) { ++cj; }
|
||||
if (cj - ci > 1) {
|
||||
@@ -758,7 +756,9 @@ simdjson_really_inline std::size_t scan_key_length(const char* p) noexcept {
|
||||
template <std::size_t MaxKeyLen>
|
||||
simdjson_really_inline bool compare_key_bytes(
|
||||
const char* p, const char* stored, std::size_t len) noexcept {
|
||||
alignas(16) static constexpr uint8_t idx16[16] =
|
||||
// [[maybe_unused]]: only the NEON/SSE2 branches read these; the scalar
|
||||
// fallback build (no SIMD) leaves them unused, which is an error under -Werror.
|
||||
[[maybe_unused]] alignas(16) static constexpr uint8_t idx16[16] =
|
||||
{0,1,2,3,4,5,6,7,8,9,10,11,12,13,14,15};
|
||||
if constexpr (MaxKeyLen <= 16) {
|
||||
#if SIMDJSON_KEY_SELECTOR_HAS_NEON
|
||||
@@ -783,7 +783,7 @@ simdjson_really_inline bool compare_key_bytes(
|
||||
return true;
|
||||
#endif
|
||||
} else if constexpr (MaxKeyLen <= 32) {
|
||||
alignas(16) static constexpr uint8_t idx32_hi[16] =
|
||||
[[maybe_unused]] alignas(16) static constexpr uint8_t idx32_hi[16] =
|
||||
{16,17,18,19,20,21,22,23,24,25,26,27,28,29,30,31};
|
||||
#if SIMDJSON_KEY_SELECTOR_HAS_NEON
|
||||
uint8x16_t vp_lo = vld1q_u8(reinterpret_cast<const uint8_t*>(p));
|
||||
@@ -867,12 +867,16 @@ struct key_selector {
|
||||
static constexpr std::size_t size() noexcept { return N; }
|
||||
|
||||
/**
|
||||
* Look up a JSON key. rjs must point just after an opening quote in a padded
|
||||
* simdjson buffer. Returns the selector index in [0, N) on match, or N on miss.
|
||||
* Look up a JSON key whose length is already known. p must point at the first
|
||||
* key byte (just after the opening quote) in a padded simdjson buffer, and len
|
||||
* must be the number of raw key bytes (the distance to the closing quote).
|
||||
* Returns the selector index in [0, N) on match, or N on miss.
|
||||
*
|
||||
* Prefer this overload when the caller can obtain the key length cheaply (for
|
||||
* example, object::for_each derives it from the structural index rather than
|
||||
* re-scanning for the closing quote).
|
||||
*/
|
||||
static simdjson_really_inline std::size_t match_raw(raw_json_string rjs) noexcept {
|
||||
const char* p = rjs.raw();
|
||||
std::size_t len = key_selector_detail::scan_key_length<max_key_len>(p);
|
||||
static simdjson_really_inline std::size_t match_raw(const char* p, std::size_t len) noexcept {
|
||||
if (len == 0 || len > max_key_len) { return N; }
|
||||
|
||||
std::size_t slot;
|
||||
@@ -911,6 +915,17 @@ struct key_selector {
|
||||
return ki;
|
||||
}
|
||||
|
||||
/**
|
||||
* Look up a JSON key. rjs must point just after an opening quote in a padded
|
||||
* simdjson buffer. Returns the selector index in [0, N) on match, or N on miss.
|
||||
* The key length is recovered with a SIMD scan for the closing quote; callers
|
||||
* that already know the length should use the (p, len) overload above.
|
||||
*/
|
||||
static simdjson_really_inline std::size_t match_raw(raw_json_string rjs) noexcept {
|
||||
const char* p = rjs.raw();
|
||||
return match_raw(p, key_selector_detail::scan_key_length<max_key_len>(p));
|
||||
}
|
||||
|
||||
/** Return the key text at selector index i (i in [0, N)). */
|
||||
static constexpr std::string_view key_at(std::size_t i) noexcept {
|
||||
return keys[i];
|
||||
|
||||
@@ -73,29 +73,41 @@ simdjson_flatten simdjson_inline for_each_result object::for_each(Func&& on_matc
|
||||
// in object_iterator -- building a value only for the fields that actually match.
|
||||
// We operate on a copy of the iterator, as object::begin() would.
|
||||
value_iterator it = iter;
|
||||
std::array<bool, Selector::size()> seen{};
|
||||
// Track which selector indices have already matched, as a compile-time bitset:
|
||||
// a single 64-bit word for up to 64 keys (the common case), more words as
|
||||
// needed. Initializing one (or a few) registers to zero is cheaper than zeroing
|
||||
// a per-key byte array on every call, and the test/set become register bit ops.
|
||||
constexpr std::size_t seen_words = (Selector::size() + 63) / 64;
|
||||
std::array<std::uint64_t, seen_words> seen{};
|
||||
std::size_t matched = 0;
|
||||
while (it.is_open()) {
|
||||
raw_json_string key;
|
||||
error_code error = it.field_key().get(key);
|
||||
std::size_t key_len;
|
||||
// field_key_with_length derives the key length from the structural index (the
|
||||
// following ':' token), avoiding a forward SIMD scan for the closing quote.
|
||||
error_code error = it.field_key_with_length(key, key_len);
|
||||
if (error) { it.abandon(); return {error, matched}; }
|
||||
// Advance past the ':' and descend onto the value.
|
||||
if ((error = it.field_value())) { it.abandon(); return {error, matched}; }
|
||||
std::size_t idx = Selector::match_raw(key);
|
||||
if (idx < Selector::size() && !seen[idx]) {
|
||||
seen[idx] = true;
|
||||
value matched_value(it.child());
|
||||
// The callback may return either void or an error_code. When it returns an
|
||||
// error_code, we stop at the first non-SUCCESS result and propagate it so
|
||||
// the caller can surface value-parse errors (for example, a type mismatch
|
||||
// on a matched field). A void-returning callback is responsible for
|
||||
// handling its own errors.
|
||||
if constexpr (std::is_same_v<decltype(on_match(idx, matched_value)), error_code>) {
|
||||
if ((error = on_match(idx, matched_value))) { return {error, matched}; }
|
||||
} else {
|
||||
on_match(idx, matched_value);
|
||||
std::size_t idx = Selector::match_raw(key.raw(), key_len);
|
||||
if (idx < Selector::size()) {
|
||||
const std::uint64_t seen_bit = std::uint64_t{1} << (idx & 63);
|
||||
std::uint64_t &seen_word = seen[idx >> 6];
|
||||
if (!(seen_word & seen_bit)) {
|
||||
seen_word |= seen_bit;
|
||||
value matched_value(it.child());
|
||||
// The callback may return either void or an error_code. When it returns an
|
||||
// error_code, we stop at the first non-SUCCESS result and propagate it so
|
||||
// the caller can surface value-parse errors (for example, a type mismatch
|
||||
// on a matched field). A void-returning callback is responsible for
|
||||
// handling its own errors.
|
||||
if constexpr (std::is_same_v<decltype(on_match(idx, matched_value)), error_code>) {
|
||||
if ((error = on_match(idx, matched_value))) { return {error, matched}; }
|
||||
} else {
|
||||
on_match(idx, matched_value);
|
||||
}
|
||||
if (++matched >= Selector::size()) { break; }
|
||||
}
|
||||
if (++matched >= Selector::size()) { break; }
|
||||
}
|
||||
// Skip the value (a no-op if the callback consumed it) and step to the next
|
||||
// field; has_next_field() ends the container on '}', which closes the loop.
|
||||
|
||||
@@ -269,11 +269,12 @@ constexpr bool user_defined_type = (std::is_class_v<T>
|
||||
!concepts::appendable_containers<T>);
|
||||
|
||||
|
||||
#if defined(SIMDJSON_USE_KEY_SELECTOR_REFLECTION) && SIMDJSON_USE_KEY_SELECTOR_REFLECTION
|
||||
#if !(defined(SIMDJSON_DISABLE_KEY_SELECTOR_REFLECTION) && SIMDJSON_DISABLE_KEY_SELECTOR_REFLECTION)
|
||||
|
||||
// Experimental: deserialize a reflected struct using a compile-time key_selector
|
||||
// Default: deserialize a reflected struct using a compile-time key_selector
|
||||
// and a single object::for_each pass (perfect-hash key matching) instead of one
|
||||
// obj[key] lookup per member. Enable with -DSIMDJSON_USE_KEY_SELECTOR_REFLECTION=1.
|
||||
// obj[key] lookup per member. Disable (falling back to the per-member obj[key]
|
||||
// path below) with -DSIMDJSON_DISABLE_KEY_SELECTOR_REFLECTION=1.
|
||||
namespace key_selector_reflection_detail {
|
||||
|
||||
// A member participates if it is public, non-const, and not annotated to skip.
|
||||
@@ -309,6 +310,21 @@ template <typename T>
|
||||
using selector_for = typename [: std::meta::substitute(
|
||||
^^SIMDJSON_IMPLEMENTATION::ondemand::key_selector, selector_key_args<T>()) :];
|
||||
|
||||
// Number of members that participate in deserialization. A class can have zero
|
||||
// eligible members (e.g. std::chrono::time_point, whose only data member is
|
||||
// private): an empty key_selector cannot be built, so the tag_invoke below
|
||||
// special-cases this count.
|
||||
template <typename T>
|
||||
consteval std::size_t eligible_member_count() {
|
||||
std::size_t count = 0;
|
||||
template for (constexpr auto mem : std::define_static_array(std::meta::nonstatic_data_members_of(^^T, std::meta::access_context::unchecked()))) {
|
||||
if constexpr (is_eligible_member(mem)) {
|
||||
++count;
|
||||
}
|
||||
}
|
||||
return count;
|
||||
}
|
||||
|
||||
// True when none of T's eligible members is an optional type, i.e. every member
|
||||
// is required. In that case presence can be checked with a single match count
|
||||
// instead of a per-member "seen" array.
|
||||
@@ -336,6 +352,14 @@ error_code tag_invoke(deserialize_tag, ValT &val, T &out) noexcept {
|
||||
} else {
|
||||
SIMDJSON_TRY(val.get_object().get(obj));
|
||||
}
|
||||
if constexpr (key_selector_reflection_detail::eligible_member_count<T>() == 0) {
|
||||
// No members to deserialize: an empty key_selector cannot be built, so just
|
||||
// validate that the input is an object (done above) and succeed. Mirrors the
|
||||
// opt-out per-member path, which iterates over zero members.
|
||||
(void)out;
|
||||
(void)obj;
|
||||
return SUCCESS;
|
||||
} else {
|
||||
using selector = key_selector_reflection_detail::selector_for<T>;
|
||||
if constexpr (key_selector_reflection_detail::all_eligible_members_required<T>()) {
|
||||
// Fast path: every member is required. A single for_each pass parses each
|
||||
@@ -394,10 +418,14 @@ error_code tag_invoke(deserialize_tag, ValT &val, T &out) noexcept {
|
||||
}
|
||||
return SUCCESS;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#else
|
||||
|
||||
// Opt-out path (-DSIMDJSON_DISABLE_KEY_SELECTOR_REFLECTION=1): deserialize a
|
||||
// reflected struct with one obj[key] lookup per member, instead of the default
|
||||
// single-pass object::for_each path above.
|
||||
template <typename T, typename ValT>
|
||||
requires(user_defined_type<T> && std::is_class_v<T>)
|
||||
error_code tag_invoke(deserialize_tag, ValT &val, T &out) noexcept {
|
||||
@@ -430,7 +458,7 @@ error_code tag_invoke(deserialize_tag, ValT &val, T &out) noexcept {
|
||||
return simdjson::SUCCESS;
|
||||
}
|
||||
|
||||
#endif // SIMDJSON_USE_KEY_SELECTOR_REFLECTION
|
||||
#endif // SIMDJSON_DISABLE_KEY_SELECTOR_REFLECTION
|
||||
|
||||
// Support for enum deserialization - deserialize from string representation using expand approach from P2996R12
|
||||
template <typename T, typename ValT>
|
||||
|
||||
@@ -423,6 +423,22 @@ simdjson_warn_unused simdjson_inline simdjson_result<raw_json_string> value_iter
|
||||
return raw_json_string(key);
|
||||
}
|
||||
|
||||
simdjson_warn_unused simdjson_inline error_code value_iterator::field_key_with_length(raw_json_string &key, std::size_t &len) noexcept {
|
||||
assert_at_next();
|
||||
|
||||
const uint8_t *k = _json_iter->return_current_and_advance();
|
||||
if (*(k++) != '"') { return report_error(TAPE_ERROR, "Object key is not a string"); }
|
||||
// After return_current_and_advance(), the current token is the ':' that follows
|
||||
// the key. The closing quote sits just before it (only JSON whitespace may
|
||||
// intervene), so step back from the ':' to the closing quote to get the length.
|
||||
// In minified JSON this is a single back-step.
|
||||
const char *q = reinterpret_cast<const char *>(_json_iter->peek());
|
||||
do { --q; } while (*q != '"');
|
||||
key = raw_json_string(k);
|
||||
len = static_cast<std::size_t>(q - reinterpret_cast<const char *>(k));
|
||||
return SUCCESS;
|
||||
}
|
||||
|
||||
simdjson_warn_unused simdjson_inline error_code value_iterator::field_value() noexcept {
|
||||
assert_at_next();
|
||||
|
||||
|
||||
@@ -158,6 +158,17 @@ public:
|
||||
*/
|
||||
simdjson_warn_unused simdjson_inline simdjson_result<raw_json_string> field_key() noexcept;
|
||||
|
||||
/**
|
||||
* Get the current field's key together with its raw byte length.
|
||||
*
|
||||
* Like field_key(), but also returns the number of raw key bytes (the distance
|
||||
* from the first key byte to the closing quote). The length is recovered from
|
||||
* the structural index -- the next structural token is the ':' -- by stepping
|
||||
* back over any whitespace to the closing quote, avoiding a forward SIMD scan
|
||||
* for the closing quote. Leaves the iterator positioned exactly as field_key().
|
||||
*/
|
||||
simdjson_warn_unused simdjson_inline error_code field_key_with_length(raw_json_string &key, std::size_t &len) noexcept;
|
||||
|
||||
/**
|
||||
* Pass the : in the field and move to its value.
|
||||
*/
|
||||
|
||||
@@ -38,9 +38,9 @@ add_cpp_test(ondemand_wrong_type_error_tests LABELS ondemand acceptance
|
||||
add_cpp_test(ondemand_iterate_many_csv LABELS ondemand acceptance per_implementation)
|
||||
add_cpp_test(ondemand_custom_types_tests LABELS ondemand acceptance per_implementation)
|
||||
add_cpp_test(ondemand_custom_types_document_tests LABELS ondemand acceptance per_implementation)
|
||||
# Reflective deserialization through the experimental key-selector path. The
|
||||
# SIMDJSON_USE_KEY_SELECTOR_REFLECTION macro is defined at the top of the test
|
||||
# source itself, so this is an ordinary test target.
|
||||
# Reflective deserialization through the (default) key-selector path. No special
|
||||
# macro is needed: the key-selector + object::for_each path is the default, so
|
||||
# this is an ordinary test target.
|
||||
add_cpp_test(ondemand_key_selector_reflection_tests LABELS ondemand acceptance per_implementation)
|
||||
add_cpp_test(ondemand_stl_types_tests LABELS ondemand acceptance per_implementation)
|
||||
add_cpp_test(ondemand_convert_tests LABELS ondemand acceptance per_implementation)
|
||||
|
||||
@@ -1,11 +1,10 @@
|
||||
// Exercises the experimental key-selector-based reflective deserializer.
|
||||
//
|
||||
// We enable the option here (before including simdjson) so the reflective
|
||||
// tag_invoke uses a compile-time key_selector + object::for_each single pass
|
||||
// instead of one obj[key] lookup per member. The point of the option is to
|
||||
// deserialize structs correctly even when the JSON keys are NOT in declaration
|
||||
// order, so the tests below deliberately scramble the key order.
|
||||
#define SIMDJSON_USE_KEY_SELECTOR_REFLECTION 1
|
||||
// Exercises the key-selector-based reflective deserializer, which is the
|
||||
// default for reflective tag_invoke: a compile-time key_selector +
|
||||
// object::for_each single pass instead of one obj[key] lookup per member.
|
||||
// (It can be disabled with -DSIMDJSON_DISABLE_KEY_SELECTOR_REFLECTION=1.) The
|
||||
// point of this path is to deserialize structs correctly even when the JSON
|
||||
// keys are NOT in declaration order, so the tests below deliberately scramble
|
||||
// the key order.
|
||||
#include "simdjson.h"
|
||||
#include <optional>
|
||||
#include <string>
|
||||
|
||||
Reference in New Issue
Block a user