Compare commits

...

5 Commits

Author SHA1 Message Date
Daniel Lemire 263e58d9d7 guarding against zero memory case 2026-06-07 14:28:13 -04:00
Daniel Lemire ed345772cd we don't need this 2026-06-06 15:35:36 -04:00
Daniel Lemire 92123faa7d making perfect hash the default 2026-06-06 14:47:09 -04:00
Daniel Lemire 12518a278a minor fixes 2026-06-06 14:36:32 -04:00
Daniel Lemire dc9a8e2890 adding a new method... 2026-06-06 00:51:09 -04:00
8 changed files with 148 additions and 79 deletions
+20 -32
View File
@@ -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];
+28 -16
View File
@@ -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.
*/
+3 -3
View File
@@ -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>