diff --git a/benchmark/static_reflect/citm_catalog_benchmark/glaze_citm_catalog_data.h b/benchmark/static_reflect/citm_catalog_benchmark/glaze_citm_catalog_data.h new file mode 100644 index 000000000..3c6dbe38c --- /dev/null +++ b/benchmark/static_reflect/citm_catalog_benchmark/glaze_citm_catalog_data.h @@ -0,0 +1,88 @@ +#ifndef GLAZE_CITM_CATALOG_DATA_H +#define GLAZE_CITM_CATALOG_DATA_H + +#include +#include +#include +#include +#include +#include + +#include + +// 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 blockIds; +}; + +struct GlazeCITMSeatCategory { + std::vector areas; + uint64_t seatCategoryId; +}; + +struct GlazeCITMPerformance { + uint64_t id; + uint64_t eventId; + std::optional logo; + std::optional name; + std::vector prices; + std::vector seatCategories; + std::optional seatMapImage; + uint64_t start; + std::string venueCode; +}; + +struct GlazeCITMEvent { + uint64_t id; + std::optional name; + std::optional description; + std::optional logo; + std::vector subTopicIds; + std::optional subjectCode; + std::optional subtitle; + std::vector topicIds; +}; + +struct GlazeCitmCatalog { + std::map events; + std::vector 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(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(data, out); + if (err) { + throw std::runtime_error("glaze citm write error"); + } + return out; +} + +#endif // GLAZE_CITM_CATALOG_DATA_H diff --git a/benchmark/static_reflect/glaze_benchmark/glaze_citm_bench.cpp b/benchmark/static_reflect/glaze_benchmark/glaze_citm_bench.cpp new file mode 100644 index 000000000..ff53b1fcf --- /dev/null +++ b/benchmark/static_reflect/glaze_benchmark/glaze_citm_bench.cpp @@ -0,0 +1,95 @@ +// Standalone Glaze benchmark for CITM Catalog (see glaze_twitter_bench.cpp +// header comment for context). + +#include +#include +#include +#include +#include +#include + +#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; +} diff --git a/benchmark/static_reflect/glaze_benchmark/glaze_twitter_bench.cpp b/benchmark/static_reflect/glaze_benchmark/glaze_twitter_bench.cpp new file mode 100644 index 000000000..4634b58eb --- /dev/null +++ b/benchmark/static_reflect/glaze_benchmark/glaze_twitter_bench.cpp @@ -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 +#include +#include +#include +#include +#include +#include +#include + +#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; +} diff --git a/benchmark/static_reflect/twitter_benchmark/glaze_twitter_data.h b/benchmark/static_reflect/twitter_benchmark/glaze_twitter_data.h new file mode 100644 index 000000000..abe96bd32 --- /dev/null +++ b/benchmark/static_reflect/twitter_benchmark/glaze_twitter_data.h @@ -0,0 +1,31 @@ +#ifndef GLAZE_TWITTER_DATA_H +#define GLAZE_TWITTER_DATA_H + +#include "twitter_data.h" +#include +#include +#include + +// 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(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 diff --git a/include/simdjson/generic/ondemand/std_deserialize.h b/include/simdjson/generic/ondemand/std_deserialize.h index 36350ce85..409cf3582 100644 --- a/include/simdjson/generic/ondemand/std_deserialize.h +++ b/include/simdjson/generic/ondemand/std_deserialize.h @@ -267,6 +267,22 @@ constexpr bool user_defined_type = (std::is_class_v !concepts::appendable_containers); +// Compile-time predicate: does T have any std::optional member? +// Used to decide whether single-pass dispatch is worth it. +template +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) { + result = true; + } + } + }; + return result; +} + template requires(user_defined_type && std::is_class_v) 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) { - // 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()) { + // 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; }