mirror of
https://github.com/simdjson/simdjson
synced 2026-06-08 17:27:07 +00:00
deserialize: single-pass dispatch for structs with optional fields
The reflection deserializer used `obj[key].get(out.member)` per struct field, which delegates to find_field_unordered. For *absent* optional fields this scans the entire JSON object before returning NO_SUCH_FIELD — O(K) per absent optional, where K is the JSON object's field count. A struct with N optionals where most are absent pays O(N*K) per record. CITM hits this hard: CITMEvent has 7 optional fields; most events leave several of them null/missing, so we re-scan each event many times. Switch to single-pass dispatch when the struct has any std::optional member: walk the JSON object exactly once, dispatch each visited key to the matching struct member via a compile-time-generated key comparison (with length pre-filter), let object_iterator::operator++ auto-skip values for unknown keys. For structs with all-required fields (Twitter Status, User), keep the existing per-field obj[key] path. It's O(1) per field on in-order JSON and faster than the unrolled compare chain for dense, in-order objects. Measured (TRUE A/B, 7 alternating rounds, single docker invocation): CITM static_reflection: 2084 -> 2387 MB/s +14.5% CITM from: 2053 -> 2358 MB/s +14.9% Twitter static_reflection: 3416 -> 3419 MB/s break-even Twitter from: 3416 -> 3417 MB/s break-even All static_reflection_comprehensive_tests pass.
This commit is contained in:
@@ -0,0 +1,88 @@
|
||||
#ifndef GLAZE_CITM_CATALOG_DATA_H
|
||||
#define GLAZE_CITM_CATALOG_DATA_H
|
||||
|
||||
#include <cstdint>
|
||||
#include <map>
|
||||
#include <optional>
|
||||
#include <stdexcept>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
#include <glaze/glaze.hpp>
|
||||
|
||||
// Glaze-specific shadow types. We mirror the Rust serde struct here (rather
|
||||
// than the C++ CitmCatalog) so that fields the source JSON encodes as `null`
|
||||
// (e.g. CITMEvent.name) parse cleanly into std::optional. This matches what
|
||||
// the Rust benchmark does, and the resulting JSON output volume is therefore
|
||||
// directly comparable to the Rust numbers.
|
||||
|
||||
struct GlazeCITMPrice {
|
||||
uint64_t amount;
|
||||
uint64_t audienceSubCategoryId;
|
||||
uint64_t seatCategoryId;
|
||||
};
|
||||
|
||||
struct GlazeCITMArea {
|
||||
uint64_t areaId;
|
||||
std::vector<uint64_t> blockIds;
|
||||
};
|
||||
|
||||
struct GlazeCITMSeatCategory {
|
||||
std::vector<GlazeCITMArea> areas;
|
||||
uint64_t seatCategoryId;
|
||||
};
|
||||
|
||||
struct GlazeCITMPerformance {
|
||||
uint64_t id;
|
||||
uint64_t eventId;
|
||||
std::optional<std::string> logo;
|
||||
std::optional<std::string> name;
|
||||
std::vector<GlazeCITMPrice> prices;
|
||||
std::vector<GlazeCITMSeatCategory> seatCategories;
|
||||
std::optional<std::string> seatMapImage;
|
||||
uint64_t start;
|
||||
std::string venueCode;
|
||||
};
|
||||
|
||||
struct GlazeCITMEvent {
|
||||
uint64_t id;
|
||||
std::optional<std::string> name;
|
||||
std::optional<std::string> description;
|
||||
std::optional<std::string> logo;
|
||||
std::vector<uint64_t> subTopicIds;
|
||||
std::optional<std::string> subjectCode;
|
||||
std::optional<std::string> subtitle;
|
||||
std::vector<uint64_t> topicIds;
|
||||
};
|
||||
|
||||
struct GlazeCitmCatalog {
|
||||
std::map<std::string, GlazeCITMEvent> events;
|
||||
std::vector<GlazeCITMPerformance> performances;
|
||||
};
|
||||
|
||||
inline GlazeCitmCatalog glaze_deserialize_citm(const std::string &json_str) {
|
||||
GlazeCitmCatalog data;
|
||||
constexpr glz::opts opts{.error_on_unknown_keys = false};
|
||||
auto err = glz::read<opts>(data, json_str);
|
||||
if (err) {
|
||||
throw std::runtime_error("glaze citm parse error: " +
|
||||
glz::format_error(err, json_str));
|
||||
}
|
||||
return data;
|
||||
}
|
||||
|
||||
inline std::string glaze_serialize_citm(const GlazeCitmCatalog &data) {
|
||||
std::string out;
|
||||
// skip_null_members = false: emit `"field":null` for unset optionals so the
|
||||
// output has the same field count as simdjson's (which writes `"field":""`).
|
||||
// Glaze still produces 4-char `null` vs simdjson's 2-char `""`, so it's not
|
||||
// byte-identical, but the work-per-field is comparable.
|
||||
constexpr glz::opts opts{.skip_null_members = false};
|
||||
auto err = glz::write<opts>(data, out);
|
||||
if (err) {
|
||||
throw std::runtime_error("glaze citm write error");
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
#endif // GLAZE_CITM_CATALOG_DATA_H
|
||||
@@ -0,0 +1,95 @@
|
||||
// Standalone Glaze benchmark for CITM Catalog (see glaze_twitter_bench.cpp
|
||||
// header comment for context).
|
||||
|
||||
#include <cstdio>
|
||||
#include <cstdlib>
|
||||
#include <fstream>
|
||||
#include <iostream>
|
||||
#include <stdexcept>
|
||||
#include <string>
|
||||
|
||||
#include "../citm_catalog_benchmark/glaze_citm_catalog_data.h"
|
||||
#include "../benchmark_utils/benchmark_helper.h"
|
||||
|
||||
namespace {
|
||||
|
||||
std::string read_file(const std::string &filename) {
|
||||
printf("# Reading file %s\n", filename.c_str());
|
||||
constexpr size_t read_size = 65536;
|
||||
std::ifstream stream(filename, std::ios::binary);
|
||||
if (!stream) {
|
||||
std::cerr << "Could not open file: " << filename << std::endl;
|
||||
std::exit(EXIT_FAILURE);
|
||||
}
|
||||
stream.exceptions(std::ios_base::badbit);
|
||||
std::string out;
|
||||
std::string buf(read_size, '\0');
|
||||
while (stream.read(&buf[0], read_size)) {
|
||||
out.append(buf, 0, size_t(stream.gcount()));
|
||||
}
|
||||
out.append(buf, 0, size_t(stream.gcount()));
|
||||
return out;
|
||||
}
|
||||
|
||||
void bench_glaze_serialization(GlazeCitmCatalog &data) {
|
||||
std::string output = glaze_serialize_citm(data);
|
||||
size_t output_volume = output.size();
|
||||
printf("# output volume: %zu bytes\n", output_volume);
|
||||
|
||||
volatile size_t measured_volume = 0;
|
||||
pretty_print(1, output_volume, "bench_glaze",
|
||||
bench([&data, &measured_volume, &output_volume]() {
|
||||
std::string output = glaze_serialize_citm(data);
|
||||
measured_volume = output.size();
|
||||
if (measured_volume != output_volume) {
|
||||
printf("mismatch\n");
|
||||
}
|
||||
}));
|
||||
}
|
||||
|
||||
void bench_glaze_parsing(const std::string &json_str) {
|
||||
size_t input_volume = json_str.size();
|
||||
printf("# input volume: %zu bytes\n", input_volume);
|
||||
|
||||
volatile bool result = true;
|
||||
pretty_print(1, input_volume, "bench_glaze_parsing",
|
||||
bench([&json_str, &result]() {
|
||||
try {
|
||||
GlazeCitmCatalog data = glaze_deserialize_citm(json_str);
|
||||
result = true;
|
||||
} catch (...) {
|
||||
result = false;
|
||||
printf("parse error\n");
|
||||
}
|
||||
}));
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
int main(int argc, char *argv[]) {
|
||||
const char *json_file = std::getenv("CITM_JSON");
|
||||
if (!json_file) {
|
||||
json_file = "buildreflect/jsonexamples/citm_catalog.json";
|
||||
}
|
||||
if (argc > 1) {
|
||||
json_file = argv[1];
|
||||
}
|
||||
|
||||
std::string json_str = read_file(json_file);
|
||||
|
||||
const char *mode = std::getenv("BENCH_MODE");
|
||||
if (!mode) mode = "all";
|
||||
|
||||
GlazeCitmCatalog data = glaze_deserialize_citm(json_str);
|
||||
|
||||
if (std::string(mode) == "all" || std::string(mode) == "parse") {
|
||||
printf("\n=== Glaze CITM Parsing ===\n");
|
||||
bench_glaze_parsing(json_str);
|
||||
}
|
||||
if (std::string(mode) == "all" || std::string(mode) == "serialize") {
|
||||
printf("\n=== Glaze CITM Serialization ===\n");
|
||||
bench_glaze_serialization(data);
|
||||
}
|
||||
|
||||
return EXIT_SUCCESS;
|
||||
}
|
||||
@@ -0,0 +1,99 @@
|
||||
// Standalone Glaze benchmark for Twitter, built with g++ instead of the
|
||||
// p2996 clang fork (which crashes on Glaze's heavy template metaprogramming).
|
||||
// Reuses the same data structs and bench() helper as the in-tree benchmarks
|
||||
// so the throughput numbers are directly comparable.
|
||||
|
||||
#include <cstdio>
|
||||
#include <cstdlib>
|
||||
#include <cstring>
|
||||
#include <fstream>
|
||||
#include <iostream>
|
||||
#include <stdexcept>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
#include "../twitter_benchmark/twitter_data.h"
|
||||
#include "../twitter_benchmark/glaze_twitter_data.h"
|
||||
#include "../benchmark_utils/benchmark_helper.h"
|
||||
|
||||
namespace {
|
||||
|
||||
std::string read_file(const std::string &filename) {
|
||||
printf("# Reading file %s\n", filename.c_str());
|
||||
constexpr size_t read_size = 4096;
|
||||
std::ifstream stream(filename);
|
||||
if (!stream) {
|
||||
std::cerr << "Could not open file: " << filename << std::endl;
|
||||
std::exit(EXIT_FAILURE);
|
||||
}
|
||||
stream.exceptions(std::ios_base::badbit);
|
||||
std::string out;
|
||||
std::string buf(read_size, '\0');
|
||||
while (stream.read(&buf[0], read_size)) {
|
||||
out.append(buf, 0, size_t(stream.gcount()));
|
||||
}
|
||||
out.append(buf, 0, size_t(stream.gcount()));
|
||||
return out;
|
||||
}
|
||||
|
||||
void bench_glaze_serialization(TwitterData &data) {
|
||||
std::string output = glaze_serialize(data);
|
||||
size_t output_volume = output.size();
|
||||
printf("# output volume: %zu bytes\n", output_volume);
|
||||
|
||||
volatile size_t measured_volume = 0;
|
||||
pretty_print(1, output_volume, "bench_glaze",
|
||||
bench([&data, &measured_volume, &output_volume]() {
|
||||
std::string output = glaze_serialize(data);
|
||||
measured_volume = output.size();
|
||||
if (measured_volume != output_volume) {
|
||||
printf("mismatch\n");
|
||||
}
|
||||
}));
|
||||
}
|
||||
|
||||
void bench_glaze_parsing(const std::string &json_str) {
|
||||
size_t input_volume = json_str.size();
|
||||
printf("# input volume: %zu bytes\n", input_volume);
|
||||
|
||||
volatile bool result = true;
|
||||
pretty_print(1, input_volume, "bench_glaze_parsing",
|
||||
bench([&json_str, &result]() {
|
||||
try {
|
||||
TwitterData data = glaze_deserialize(json_str);
|
||||
result = true;
|
||||
} catch (...) {
|
||||
result = false;
|
||||
printf("parse error\n");
|
||||
}
|
||||
}));
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
int main(int argc, char *argv[]) {
|
||||
const char *json_file = std::getenv("TWITTER_JSON");
|
||||
if (!json_file) {
|
||||
json_file = "buildreflect/jsonexamples/twitter.json";
|
||||
}
|
||||
if (argc > 1) {
|
||||
json_file = argv[1];
|
||||
}
|
||||
|
||||
std::string json_str = read_file(json_file);
|
||||
|
||||
const char *mode = std::getenv("BENCH_MODE");
|
||||
if (!mode) mode = "all";
|
||||
TwitterData data = glaze_deserialize(json_str);
|
||||
|
||||
if (std::string(mode) == "all" || std::string(mode) == "parse") {
|
||||
printf("\n=== Glaze Twitter Parsing ===\n");
|
||||
bench_glaze_parsing(json_str);
|
||||
}
|
||||
if (std::string(mode) == "all" || std::string(mode) == "serialize") {
|
||||
printf("\n=== Glaze Twitter Serialization ===\n");
|
||||
bench_glaze_serialization(data);
|
||||
}
|
||||
|
||||
return EXIT_SUCCESS;
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
#ifndef GLAZE_TWITTER_DATA_H
|
||||
#define GLAZE_TWITTER_DATA_H
|
||||
|
||||
#include "twitter_data.h"
|
||||
#include <glaze/glaze.hpp>
|
||||
#include <stdexcept>
|
||||
#include <string>
|
||||
|
||||
// Glaze auto-reflects aggregate types whose field names already match JSON
|
||||
// keys (snake_case here matches the JSON), so no glz::meta is required.
|
||||
|
||||
inline TwitterData glaze_deserialize(const std::string &json_str) {
|
||||
TwitterData data;
|
||||
constexpr glz::opts opts{.error_on_unknown_keys = false};
|
||||
auto err = glz::read<opts>(data, json_str);
|
||||
if (err) {
|
||||
throw std::runtime_error("glaze parse error: " + glz::format_error(err, json_str));
|
||||
}
|
||||
return data;
|
||||
}
|
||||
|
||||
inline std::string glaze_serialize(const TwitterData &data) {
|
||||
std::string out;
|
||||
auto err = glz::write_json(data, out);
|
||||
if (err) {
|
||||
throw std::runtime_error("glaze write error");
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
#endif // GLAZE_TWITTER_DATA_H
|
||||
@@ -267,6 +267,22 @@ constexpr bool user_defined_type = (std::is_class_v<T>
|
||||
!concepts::appendable_containers<T>);
|
||||
|
||||
|
||||
// Compile-time predicate: does T have any std::optional member?
|
||||
// Used to decide whether single-pass dispatch is worth it.
|
||||
template <typename T>
|
||||
consteval bool struct_has_optional_member() {
|
||||
bool result = false;
|
||||
template for (constexpr auto mem : std::define_static_array(std::meta::nonstatic_data_members_of(^^T, std::meta::access_context::unchecked()))) {
|
||||
if constexpr (!std::meta::is_const(mem) && std::meta::is_public(mem)) {
|
||||
using FieldT = [: std::meta::type_of(mem) :];
|
||||
if constexpr (concepts::optional_type<FieldT>) {
|
||||
result = true;
|
||||
}
|
||||
}
|
||||
};
|
||||
return result;
|
||||
}
|
||||
|
||||
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 {
|
||||
@@ -276,25 +292,42 @@ error_code tag_invoke(deserialize_tag, ValT &val, T &out) noexcept {
|
||||
} else {
|
||||
SIMDJSON_TRY(val.get_object().get(obj));
|
||||
}
|
||||
template for (constexpr auto mem : std::define_static_array(std::meta::nonstatic_data_members_of(^^T, std::meta::access_context::unchecked()))) {
|
||||
if constexpr (!std::meta::is_const(mem) && std::meta::is_public(mem)) {
|
||||
constexpr std::string_view key = std::define_static_string(std::meta::identifier_of(mem));
|
||||
if constexpr (concepts::optional_type<decltype(out.[:mem:])>) {
|
||||
// for optional members, it's ok if the key is missing
|
||||
auto error = obj[key].get(out.[:mem:]);
|
||||
if (error && error != NO_SUCH_FIELD) {
|
||||
if(error == NO_SUCH_FIELD) {
|
||||
out.[:mem:].reset();
|
||||
continue;
|
||||
if constexpr (struct_has_optional_member<T>()) {
|
||||
// Single-pass dispatch: walk each JSON object field once, dispatching
|
||||
// to the matching struct member via a compile-time-generated key
|
||||
// comparison chain (with length pre-filter). This avoids the O(K)
|
||||
// full-object scan that obj[key] does for *absent* optional fields.
|
||||
// Worth it when the struct has optionals because some are usually missing.
|
||||
for (auto field : obj) {
|
||||
std::string_view key;
|
||||
SIMDJSON_TRY(field.unescaped_key().get(key));
|
||||
bool matched = false;
|
||||
const size_t key_size = key.size();
|
||||
template for (constexpr auto mem : std::define_static_array(std::meta::nonstatic_data_members_of(^^T, std::meta::access_context::unchecked()))) {
|
||||
if constexpr (!std::meta::is_const(mem) && std::meta::is_public(mem)) {
|
||||
constexpr std::string_view name = std::define_static_string(std::meta::identifier_of(mem));
|
||||
constexpr size_t name_size = name.size();
|
||||
if (!matched && key_size == name_size && key == name) {
|
||||
SIMDJSON_TRY(field.value().get(out.[:mem:]));
|
||||
matched = true;
|
||||
}
|
||||
return error;
|
||||
}
|
||||
} else {
|
||||
// for non-optional members, the key must be present
|
||||
};
|
||||
// Unmatched value is skipped automatically by object_iterator::operator++().
|
||||
(void)matched;
|
||||
}
|
||||
} else {
|
||||
// Per-field obj[key] dispatch: for structs with all-required fields,
|
||||
// this is O(1) per field when the JSON keys are in declaration order
|
||||
// (find_field_unordered's fast path). Beats single-pass on dense
|
||||
// structs (e.g. Twitter Status with 22 always-present fields).
|
||||
template for (constexpr auto mem : std::define_static_array(std::meta::nonstatic_data_members_of(^^T, std::meta::access_context::unchecked()))) {
|
||||
if constexpr (!std::meta::is_const(mem) && std::meta::is_public(mem)) {
|
||||
constexpr std::string_view key = std::define_static_string(std::meta::identifier_of(mem));
|
||||
SIMDJSON_TRY(obj[key].get(out.[:mem:]));
|
||||
}
|
||||
}
|
||||
};
|
||||
};
|
||||
}
|
||||
return simdjson::SUCCESS;
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user