mirror of
https://github.com/nlohmann/json.git
synced 2026-11-04 03:39:22 +00:00
Three reviews of "How it compares" (a maintainer audit of the harness, a
methodology review, a reader's red team) led to a full rerun on the Mac and
x86-64 with simdjson 5.1.1 and new ways in the harness:
- in-place parses with the copy made before the timer ("(copy untimed)",
never chosen as a library's best) and json_document::parse_copy();
- RapidJSON through StringStream, and with default precision in validate,
which reads no number;
- BENCH_ONLY takes several prefixes.
The post now gives the copy's real cost (11-14 us for twitter.json on the
Mac, not 30), what yyjson gains without it, the run-to-run noise, the LTO
check, the single-file losses that were missing, and qualifies the headline
claim. Every number in the section and Appendix E is recomputed; the
NUL-byte remark moved out of the body.
Signed-off-by: Niels Lohmann <mail@nlohmann.me>
3556 lines
128 KiB
C++
3556 lines
128 KiB
C++
// __ _____ _____ _____
|
|
// __| | __| | | | JSON for Modern C++ (supporting code)
|
|
// | | |__ | | | | | | version 3.12.0
|
|
// |_____|_____|_____|_|___| https://github.com/nlohmann/json
|
|
//
|
|
// SPDX-FileCopyrightText: 2013-2026 Niels Lohmann <https://nlohmann.me>
|
|
// SPDX-License-Identifier: MIT
|
|
|
|
// Use cases of the nativejson-benchmark files, jeopardy and one twitter status with every C++ library in one binary:
|
|
// validate, sum, select, edit, minify, write (see README.md). A library may implement a use case in every reasonable
|
|
// way ("variants": other APIs, SAX, in-situ parsing, reused parsers). Before timing, every variant must produce the
|
|
// right result on the file (else it is printed as "excluded" and not timed), and it is probed with malformed input
|
|
// derived from the file, which gives its validation level: "full", "no UTF-8" or "partial" (printed in its cell).
|
|
// All variants of a use case run interleaved in every round, in a new random order each round (fixed seed), each timed
|
|
// call after an untimed one, and the best round is reported (same method as tests/benchmarks/json_view/bench_view.cpp).
|
|
//
|
|
// Input contract: the input is a const std::string holding the file with at least 64 bytes of spare capacity, so
|
|
// simdjson can use it without a copy. A variant that modifies its input copies it inside the timed call. Each timed
|
|
// call starts from the text and frees everything it built, except variants named "(reused)".
|
|
//
|
|
// Usage: bench_all <json_test_data dir> [rounds] [document] [use case]
|
|
// bench_all --files <dir> [rounds] [document] [use case] (every *.json file of dir: validate, sum, minify, write)
|
|
// bench_all --conformance <dir> (every validate variant on every *.json file of dir)
|
|
// BENCH_ONLY=<prefix>[,<prefix>...] times only the variants whose name starts with one of them (e.g. "nlohmann ")
|
|
#include <nlohmann/json_view.hpp>
|
|
|
|
#include <boost/describe.hpp>
|
|
#include <boost/json.hpp>
|
|
#include <boost/json/src.hpp>
|
|
#include <boost/json/basic_parser_impl.hpp>
|
|
#include <boost/json/parse_into.hpp>
|
|
#if !BENCH_NO_GLAZE
|
|
#include <glaze/glaze.hpp>
|
|
#include <glaze/json/lazy.hpp>
|
|
#endif
|
|
#include <rapidjson/document.h>
|
|
#include <rapidjson/encodedstream.h>
|
|
#include <rapidjson/memorystream.h>
|
|
#include <rapidjson/reader.h>
|
|
#include <rapidjson/stringbuffer.h>
|
|
#include <rapidjson/writer.h>
|
|
#include <simdjson.h>
|
|
#include <sonic/sonic.h>
|
|
#include <yyjson.h>
|
|
|
|
#include <algorithm>
|
|
#include <charconv>
|
|
#include <chrono>
|
|
#include <cmath>
|
|
#include <cstdint>
|
|
#include <cstdio>
|
|
#include <cstdlib>
|
|
#include <cstring>
|
|
#include <filesystem>
|
|
#include <fstream>
|
|
#include <functional>
|
|
#include <map>
|
|
#include <numeric>
|
|
#include <random>
|
|
#include <memory>
|
|
#include <new>
|
|
#include <sstream>
|
|
#include <string>
|
|
#include <string_view>
|
|
#include <utility>
|
|
#include <vector>
|
|
|
|
#include <sys/wait.h>
|
|
#include <unistd.h>
|
|
|
|
using nlohmann::json;
|
|
using nlohmann::json_document;
|
|
using nlohmann::json_editable_document;
|
|
using nlohmann::json_editable_view;
|
|
using nlohmann::json_view;
|
|
using nlohmann::ordered_json;
|
|
|
|
static volatile double g_sink;
|
|
|
|
struct stats
|
|
{
|
|
double num = 0;
|
|
std::size_t str = 0, nodes = 0;
|
|
};
|
|
|
|
// what a variant returns besides success: the stats (sum), the selected value (select), or the output text
|
|
// (edit, minify, write; only copied when out is set, i.e., when checking)
|
|
struct result
|
|
{
|
|
stats st;
|
|
double acc = 0;
|
|
std::string* out = nullptr;
|
|
};
|
|
|
|
static bool emit(result& r, const char* p, std::size_t n)
|
|
{
|
|
r.acc = static_cast<double>(n);
|
|
if (r.out != nullptr)
|
|
{
|
|
r.out->assign(p, n);
|
|
}
|
|
return true;
|
|
}
|
|
|
|
static bool emit(result& r, std::string&& o)
|
|
{
|
|
r.acc = static_cast<double>(o.size());
|
|
if (r.out != nullptr)
|
|
{
|
|
*r.out = std::move(o);
|
|
}
|
|
return true;
|
|
}
|
|
|
|
// RapidJSON: correctly rounded floats, as every other library, and UTF-8 validation
|
|
constexpr unsigned rj_flags = rapidjson::kParseFullPrecisionFlag | rapidjson::kParseValidateEncodingFlag;
|
|
|
|
// Boost.JSON: correctly rounded floats (the default is number_precision::imprecise)
|
|
static boost::json::parse_options bj_opts()
|
|
{
|
|
boost::json::parse_options o;
|
|
o.numbers = boost::json::number_precision::precise;
|
|
return o;
|
|
}
|
|
static const boost::json::parse_options bj_precise = bj_opts();
|
|
|
|
// Boost.JSON number_precision::none: numbers are validated but not converted; every number reads as 0 (int64 or
|
|
// double), so only use cases that read no number give the right result
|
|
static boost::json::parse_options bj_opts_none()
|
|
{
|
|
boost::json::parse_options o;
|
|
o.numbers = boost::json::number_precision::none;
|
|
return o;
|
|
}
|
|
static const boost::json::parse_options bj_none = bj_opts_none();
|
|
|
|
// a number that a library kept as text (yyjson YYJSON_READ_NUMBER_AS_RAW, RapidJSON kParseNumbersAsStringsFlag),
|
|
// converted exactly when it is read
|
|
template<class T>
|
|
static T from_text(const char* p, std::size_t n)
|
|
{
|
|
T v{};
|
|
std::from_chars(p, p + n, v);
|
|
return v;
|
|
}
|
|
|
|
///////////////////////////////////////////////////////////////////////////////
|
|
// sum: walk a DOM, visit every value, convert every number, touch every string and key
|
|
///////////////////////////////////////////////////////////////////////////////
|
|
|
|
// ---- json::parse
|
|
static void walk(const json& j, stats& st)
|
|
{
|
|
++st.nodes;
|
|
switch (j.type())
|
|
{
|
|
case json::value_t::object:
|
|
for (const auto& kv : j.get_ref<const json::object_t&>())
|
|
{
|
|
st.str += kv.first.size();
|
|
walk(kv.second, st);
|
|
}
|
|
break;
|
|
case json::value_t::array:
|
|
for (const auto& e : j.get_ref<const json::array_t&>())
|
|
{
|
|
walk(e, st);
|
|
}
|
|
break;
|
|
case json::value_t::string:
|
|
st.str += j.get_ref<const std::string&>().size();
|
|
break;
|
|
case json::value_t::number_integer:
|
|
st.num += static_cast<double>(*j.get_ptr<const json::number_integer_t*>());
|
|
break;
|
|
case json::value_t::number_unsigned:
|
|
st.num += static_cast<double>(*j.get_ptr<const json::number_unsigned_t*>());
|
|
break;
|
|
case json::value_t::number_float:
|
|
st.num += *j.get_ptr<const json::number_float_t*>();
|
|
break;
|
|
default:
|
|
break;
|
|
}
|
|
}
|
|
|
|
// ---- json_view
|
|
static void walk(json_view v, stats& st)
|
|
{
|
|
++st.nodes;
|
|
switch (v.type())
|
|
{
|
|
case json::value_t::object:
|
|
for (auto it = v.begin(); it != v.end(); ++it)
|
|
{
|
|
st.str += it.key().size();
|
|
walk(*it, st);
|
|
}
|
|
break;
|
|
case json::value_t::array:
|
|
for (const json_view e : v)
|
|
{
|
|
walk(e, st);
|
|
}
|
|
break;
|
|
case json::value_t::string:
|
|
st.str += v.get_string().size();
|
|
break;
|
|
case json::value_t::number_integer:
|
|
st.num += static_cast<double>(v.get<std::int64_t>());
|
|
break;
|
|
case json::value_t::number_unsigned:
|
|
st.num += static_cast<double>(v.get<std::uint64_t>());
|
|
break;
|
|
case json::value_t::number_float:
|
|
st.num += v.get<double>();
|
|
break;
|
|
default:
|
|
break;
|
|
}
|
|
}
|
|
|
|
// ---- json::sax_parse: the same visit without a DOM, in document order
|
|
struct json_sum_sax
|
|
{
|
|
stats* st;
|
|
bool null()
|
|
{
|
|
++st->nodes;
|
|
return true;
|
|
}
|
|
bool boolean(bool /*b*/)
|
|
{
|
|
++st->nodes;
|
|
return true;
|
|
}
|
|
bool number_integer(json::number_integer_t i)
|
|
{
|
|
++st->nodes;
|
|
st->num += static_cast<double>(i);
|
|
return true;
|
|
}
|
|
bool number_unsigned(json::number_unsigned_t u)
|
|
{
|
|
++st->nodes;
|
|
st->num += static_cast<double>(u);
|
|
return true;
|
|
}
|
|
bool number_float(json::number_float_t f, const json::string_t& /*s*/)
|
|
{
|
|
++st->nodes;
|
|
st->num += f;
|
|
return true;
|
|
}
|
|
bool string(json::string_t& s)
|
|
{
|
|
++st->nodes;
|
|
st->str += s.size();
|
|
return true;
|
|
}
|
|
bool binary(json::binary_t& /*b*/)
|
|
{
|
|
return true;
|
|
}
|
|
bool start_object(std::size_t /*n*/)
|
|
{
|
|
++st->nodes;
|
|
return true;
|
|
}
|
|
bool key(json::string_t& k)
|
|
{
|
|
st->str += k.size();
|
|
return true;
|
|
}
|
|
bool end_object()
|
|
{
|
|
return true;
|
|
}
|
|
bool start_array(std::size_t /*n*/)
|
|
{
|
|
++st->nodes;
|
|
return true;
|
|
}
|
|
bool end_array()
|
|
{
|
|
return true;
|
|
}
|
|
bool parse_error(std::size_t /*pos*/, const std::string& /*tok*/, const json::exception& /*ex*/)
|
|
{
|
|
return false;
|
|
}
|
|
};
|
|
|
|
// ---- yyjson
|
|
static void walk(yyjson_val* v, stats& st)
|
|
{
|
|
++st.nodes;
|
|
switch (yyjson_get_type(v))
|
|
{
|
|
case YYJSON_TYPE_OBJ:
|
|
{
|
|
std::size_t idx, max;
|
|
yyjson_val* k, * val;
|
|
yyjson_obj_foreach(v, idx, max, k, val)
|
|
{
|
|
st.str += yyjson_get_len(k);
|
|
walk(val, st);
|
|
}
|
|
break;
|
|
}
|
|
case YYJSON_TYPE_ARR:
|
|
{
|
|
std::size_t idx, max;
|
|
yyjson_val* val;
|
|
yyjson_arr_foreach(v, idx, max, val) walk(val, st);
|
|
break;
|
|
}
|
|
case YYJSON_TYPE_STR:
|
|
st.str += yyjson_get_len(v);
|
|
break;
|
|
case YYJSON_TYPE_NUM:
|
|
if (yyjson_is_sint(v))
|
|
{
|
|
st.num += static_cast<double>(yyjson_get_sint(v));
|
|
}
|
|
else if (yyjson_is_uint(v))
|
|
{
|
|
st.num += static_cast<double>(yyjson_get_uint(v));
|
|
}
|
|
else
|
|
{
|
|
st.num += yyjson_get_real(v);
|
|
}
|
|
break;
|
|
default:
|
|
break;
|
|
}
|
|
}
|
|
|
|
// yyjson in situ: the input is copied into a padded, mutable buffer
|
|
struct yy_insitu
|
|
{
|
|
char* buf = nullptr;
|
|
yyjson_doc* doc = nullptr;
|
|
explicit yy_insitu(const std::string& s)
|
|
: buf(static_cast<char*>(std::malloc(s.size() + YYJSON_PADDING_SIZE)))
|
|
{
|
|
std::memcpy(buf, s.data(), s.size());
|
|
std::memset(buf + s.size(), 0, YYJSON_PADDING_SIZE);
|
|
doc = yyjson_read_opts(buf, s.size(), YYJSON_READ_INSITU, nullptr, nullptr);
|
|
}
|
|
yy_insitu(const yy_insitu&) = delete;
|
|
yy_insitu& operator=(const yy_insitu&) = delete;
|
|
~yy_insitu()
|
|
{
|
|
yyjson_doc_free(doc);
|
|
std::free(buf);
|
|
}
|
|
};
|
|
|
|
// "(copy untimed)": an in-situ way whose input copy is made before the timer starts and freed after it stops, as for
|
|
// a program that reads a file into a buffer it owns and no longer needs; never chosen as a library's best. Outside
|
|
// the timing loop (the checks and probes), take() copies the input itself.
|
|
struct input_copies
|
|
{
|
|
std::vector<char*> bufs;
|
|
std::size_t next = 0;
|
|
void make(const std::string& s, int n)
|
|
{
|
|
clear();
|
|
for (int i = 0; i < n; ++i)
|
|
{
|
|
char* b = static_cast<char*>(std::malloc(s.size() + YYJSON_PADDING_SIZE));
|
|
std::memcpy(b, s.data(), s.size());
|
|
std::memset(b + s.size(), 0, YYJSON_PADDING_SIZE);
|
|
bufs.push_back(b);
|
|
}
|
|
}
|
|
void clear()
|
|
{
|
|
for (char* b : bufs)
|
|
{
|
|
std::free(b);
|
|
}
|
|
bufs.clear();
|
|
next = 0;
|
|
}
|
|
};
|
|
static input_copies g_copies;
|
|
|
|
struct taken_copy
|
|
{
|
|
char* buf = nullptr;
|
|
bool owned = false;
|
|
explicit taken_copy(const std::string& s)
|
|
{
|
|
if (g_copies.next < g_copies.bufs.size())
|
|
{
|
|
buf = g_copies.bufs[g_copies.next++];
|
|
return;
|
|
}
|
|
owned = true;
|
|
buf = static_cast<char*>(std::malloc(s.size() + YYJSON_PADDING_SIZE));
|
|
std::memcpy(buf, s.data(), s.size());
|
|
std::memset(buf + s.size(), 0, YYJSON_PADDING_SIZE);
|
|
}
|
|
taken_copy(const taken_copy&) = delete;
|
|
taken_copy& operator=(const taken_copy&) = delete;
|
|
~taken_copy()
|
|
{
|
|
if (owned)
|
|
{
|
|
std::free(buf);
|
|
}
|
|
}
|
|
};
|
|
|
|
// yyjson in situ on a copy made before the timer
|
|
struct yy_insitu_untimed
|
|
{
|
|
taken_copy c;
|
|
yyjson_doc* doc = nullptr;
|
|
explicit yy_insitu_untimed(const std::string& s)
|
|
: c(s), doc(yyjson_read_opts(c.buf, s.size(), YYJSON_READ_INSITU, nullptr, nullptr))
|
|
{}
|
|
yy_insitu_untimed(const yy_insitu_untimed&) = delete;
|
|
yy_insitu_untimed& operator=(const yy_insitu_untimed&) = delete;
|
|
~yy_insitu_untimed()
|
|
{
|
|
yyjson_doc_free(doc);
|
|
}
|
|
};
|
|
|
|
// ---- simdjson DOM and On-Demand
|
|
// after the type switch, the unchecked accessors (value_unsafe()) instead of the throwing conversions (int64_t(e),
|
|
// as in the 3.13.0 post; measured the same)
|
|
static void walk(simdjson::dom::element e, stats& st)
|
|
{
|
|
++st.nodes;
|
|
switch (e.type())
|
|
{
|
|
case simdjson::dom::element_type::OBJECT:
|
|
{
|
|
// (a local, not a range-for over value_unsafe() of the temporary result, which dangles before C++23's
|
|
// P2718, e.g. with GCC 13)
|
|
const simdjson::dom::object o = e.get_object().value_unsafe();
|
|
for (auto f : o)
|
|
{
|
|
st.str += f.key.size();
|
|
walk(f.value, st);
|
|
}
|
|
break;
|
|
}
|
|
case simdjson::dom::element_type::ARRAY:
|
|
{
|
|
const simdjson::dom::array a = e.get_array().value_unsafe();
|
|
for (auto c : a)
|
|
{
|
|
walk(c, st);
|
|
}
|
|
break;
|
|
}
|
|
case simdjson::dom::element_type::STRING:
|
|
st.str += e.get_string().value_unsafe().size();
|
|
break;
|
|
case simdjson::dom::element_type::INT64:
|
|
st.num += static_cast<double>(e.get_int64().value_unsafe());
|
|
break;
|
|
case simdjson::dom::element_type::UINT64:
|
|
st.num += static_cast<double>(e.get_uint64().value_unsafe());
|
|
break;
|
|
case simdjson::dom::element_type::DOUBLE:
|
|
st.num += e.get_double().value_unsafe();
|
|
break;
|
|
default:
|
|
break;
|
|
}
|
|
}
|
|
|
|
static void walk_od(simdjson::ondemand::value v, stats& st)
|
|
{
|
|
++st.nodes;
|
|
switch (v.type())
|
|
{
|
|
case simdjson::ondemand::json_type::object:
|
|
for (auto f : v.get_object())
|
|
{
|
|
st.str += std::string_view(f.unescaped_key()).size();
|
|
walk_od(f.value(), st);
|
|
}
|
|
break;
|
|
case simdjson::ondemand::json_type::array:
|
|
for (auto c : v.get_array())
|
|
{
|
|
walk_od(c.value(), st);
|
|
}
|
|
break;
|
|
case simdjson::ondemand::json_type::string:
|
|
st.str += std::string_view(v.get_string()).size();
|
|
break;
|
|
case simdjson::ondemand::json_type::number:
|
|
{
|
|
simdjson::ondemand::number n = v.get_number();
|
|
switch (n.get_number_type())
|
|
{
|
|
case simdjson::ondemand::number_type::signed_integer:
|
|
st.num += static_cast<double>(n.get_int64());
|
|
break;
|
|
case simdjson::ondemand::number_type::unsigned_integer:
|
|
st.num += static_cast<double>(n.get_uint64());
|
|
break;
|
|
default:
|
|
st.num += n.get_double();
|
|
break;
|
|
}
|
|
break;
|
|
}
|
|
case simdjson::ondemand::json_type::boolean:
|
|
(void)bool(v.get_bool());
|
|
break;
|
|
default:
|
|
if (!v.is_null())
|
|
{
|
|
throw simdjson::simdjson_error(simdjson::INCORRECT_TYPE);
|
|
}
|
|
break;
|
|
}
|
|
}
|
|
|
|
// ---- RapidJSON DOM and SAX
|
|
static void walk(const rapidjson::Value& v, stats& st)
|
|
{
|
|
++st.nodes;
|
|
switch (v.GetType())
|
|
{
|
|
case rapidjson::kObjectType:
|
|
for (auto m = v.MemberBegin(); m != v.MemberEnd(); ++m)
|
|
{
|
|
st.str += m->name.GetStringLength();
|
|
walk(m->value, st);
|
|
}
|
|
break;
|
|
case rapidjson::kArrayType:
|
|
for (const auto& e : v.GetArray())
|
|
{
|
|
walk(e, st);
|
|
}
|
|
break;
|
|
case rapidjson::kStringType:
|
|
st.str += v.GetStringLength();
|
|
break;
|
|
case rapidjson::kNumberType:
|
|
if (v.IsDouble())
|
|
{
|
|
st.num += v.GetDouble();
|
|
}
|
|
else if (v.IsInt64())
|
|
{
|
|
st.num += static_cast<double>(v.GetInt64());
|
|
}
|
|
else
|
|
{
|
|
st.num += static_cast<double>(v.GetUint64());
|
|
}
|
|
break;
|
|
default:
|
|
break;
|
|
}
|
|
}
|
|
|
|
struct rj_sum : rapidjson::BaseReaderHandler<rapidjson::UTF8<>, rj_sum>
|
|
{
|
|
stats* st;
|
|
explicit rj_sum(stats* s) : st(s) {}
|
|
bool Null()
|
|
{
|
|
++st->nodes;
|
|
return true;
|
|
}
|
|
bool Bool(bool /*b*/)
|
|
{
|
|
++st->nodes;
|
|
return true;
|
|
}
|
|
bool Int(int i)
|
|
{
|
|
++st->nodes;
|
|
st->num += static_cast<double>(i);
|
|
return true;
|
|
}
|
|
bool Uint(unsigned u)
|
|
{
|
|
++st->nodes;
|
|
st->num += static_cast<double>(u);
|
|
return true;
|
|
}
|
|
bool Int64(std::int64_t i)
|
|
{
|
|
++st->nodes;
|
|
st->num += static_cast<double>(i);
|
|
return true;
|
|
}
|
|
bool Uint64(std::uint64_t u)
|
|
{
|
|
++st->nodes;
|
|
st->num += static_cast<double>(u);
|
|
return true;
|
|
}
|
|
bool Double(double d)
|
|
{
|
|
++st->nodes;
|
|
st->num += d;
|
|
return true;
|
|
}
|
|
bool String(const char* /*s*/, rapidjson::SizeType n, bool /*copy*/)
|
|
{
|
|
++st->nodes;
|
|
st->str += n;
|
|
return true;
|
|
}
|
|
bool Key(const char* /*s*/, rapidjson::SizeType n, bool /*copy*/)
|
|
{
|
|
st->str += n;
|
|
return true;
|
|
}
|
|
bool StartObject()
|
|
{
|
|
++st->nodes;
|
|
return true;
|
|
}
|
|
bool EndObject(rapidjson::SizeType /*n*/)
|
|
{
|
|
return true;
|
|
}
|
|
bool StartArray()
|
|
{
|
|
++st->nodes;
|
|
return true;
|
|
}
|
|
bool EndArray(rapidjson::SizeType /*n*/)
|
|
{
|
|
return true;
|
|
}
|
|
};
|
|
|
|
// RapidJSON's Reader on (data, size), as Document::Parse(data, size) does it
|
|
template<unsigned Flags, class Handler>
|
|
static bool rj_read(const std::string& s, Handler& h)
|
|
{
|
|
rapidjson::MemoryStream ms(s.data(), s.size());
|
|
rapidjson::EncodedInputStream<rapidjson::UTF8<>, rapidjson::MemoryStream> is(ms);
|
|
rapidjson::Reader reader;
|
|
return !reader.Parse<Flags>(is, h).IsError();
|
|
}
|
|
|
|
// RapidJSON's Reader on the NUL-terminated string, as Document::Parse(const char*) does it: StringStream has
|
|
// RapidJSON's SIMD string scanner, which skips the encoding check
|
|
template<unsigned Flags, class Handler>
|
|
static bool rj_read_cstr(const std::string& s, Handler& h)
|
|
{
|
|
rapidjson::StringStream is(s.c_str());
|
|
rapidjson::Reader reader;
|
|
return !reader.Parse<Flags>(is, h).IsError();
|
|
}
|
|
|
|
// RapidJSON's Writer writes a raw number (kParseNumbersAsStringsFlag) as a string: write its text instead
|
|
struct rj_raw_writer : rapidjson::Writer<rapidjson::StringBuffer>
|
|
{
|
|
using Writer::Writer;
|
|
bool RawNumber(const char* str, rapidjson::SizeType n, bool /*copy*/)
|
|
{
|
|
return RawValue(str, n, rapidjson::kNumberType);
|
|
}
|
|
};
|
|
|
|
// ---- sonic-cpp
|
|
template<class N>
|
|
static void walk_sonic(const N& v, stats& st)
|
|
{
|
|
++st.nodes;
|
|
if (v.IsObject())
|
|
{
|
|
for (auto m = v.MemberBegin(); m != v.MemberEnd(); ++m)
|
|
{
|
|
st.str += m->name.GetStringView().size();
|
|
walk_sonic(m->value, st);
|
|
}
|
|
}
|
|
else if (v.IsArray())
|
|
{
|
|
for (auto e = v.Begin(); e != v.End(); ++e)
|
|
{
|
|
walk_sonic(*e, st);
|
|
}
|
|
}
|
|
else if (v.IsString())
|
|
{
|
|
st.str += v.GetStringView().size();
|
|
}
|
|
else if (v.IsNumber())
|
|
{
|
|
if (v.IsDouble())
|
|
{
|
|
st.num += v.GetDouble();
|
|
}
|
|
else if (v.IsUint64())
|
|
{
|
|
st.num += static_cast<double>(v.GetUint64());
|
|
}
|
|
else
|
|
{
|
|
st.num += static_cast<double>(v.GetInt64());
|
|
}
|
|
}
|
|
}
|
|
|
|
#if !BENCH_NO_GLAZE
|
|
// ---- Glaze generic_u64 and generic (owning DOMs; generic keeps every number as a double) and lazy_json (on demand)
|
|
template<glz::num_mode M>
|
|
static void walk(const glz::generic_json<M>& j, stats& st)
|
|
{
|
|
using G = glz::generic_json<M>;
|
|
++st.nodes;
|
|
std::visit([&](const auto & x)
|
|
{
|
|
using T = std::decay_t<decltype(x)>;
|
|
if constexpr (std::is_same_v<T, typename G::object_t>)
|
|
{
|
|
for (const auto& [k, v] : x)
|
|
{
|
|
st.str += k.size();
|
|
walk(v, st);
|
|
}
|
|
}
|
|
else if constexpr (std::is_same_v<T, typename G::array_t>)
|
|
{
|
|
for (const auto& e : x)
|
|
{
|
|
walk(e, st);
|
|
}
|
|
}
|
|
else if constexpr (std::is_same_v<T, std::string>)
|
|
{
|
|
st.str += x.size();
|
|
}
|
|
else if constexpr (std::is_same_v<T, double> || std::is_same_v<T, std::int64_t> || std::is_same_v<T, std::uint64_t>)
|
|
{
|
|
st.num += static_cast<double>(x);
|
|
}
|
|
}, j.data);
|
|
}
|
|
|
|
// Str: how a string is read; std::string (unescaped, UTF-8 checked) or std::string_view (the raw bytes)
|
|
template<class Str = std::string, class V>
|
|
static void walk_lazy(const V& v, stats& st)
|
|
{
|
|
++st.nodes;
|
|
if (v.is_object())
|
|
{
|
|
for (auto item : v)
|
|
{
|
|
st.str += item.key().size();
|
|
walk_lazy<Str>(item, st);
|
|
}
|
|
}
|
|
else if (v.is_array())
|
|
{
|
|
for (auto item : v)
|
|
{
|
|
walk_lazy<Str>(item, st);
|
|
}
|
|
}
|
|
else if (v.is_string())
|
|
{
|
|
st.str += v.template get<Str>().value().size();
|
|
}
|
|
else if (v.is_number())
|
|
{
|
|
const std::string_view raw = v.raw_json();
|
|
if (raw.find_first_of(".eE") != std::string_view::npos)
|
|
{
|
|
st.num += v.template get<double>().value();
|
|
}
|
|
else if (raw[0] == '-')
|
|
{
|
|
st.num += static_cast<double>(v.template get<std::int64_t>().value());
|
|
}
|
|
else
|
|
{
|
|
st.num += static_cast<double>(v.template get<std::uint64_t>().value());
|
|
}
|
|
}
|
|
}
|
|
#endif
|
|
|
|
// ---- Boost.JSON DOM and basic_parser (SAX)
|
|
static void walk(const boost::json::value& v, stats& st)
|
|
{
|
|
++st.nodes;
|
|
switch (v.kind())
|
|
{
|
|
case boost::json::kind::object:
|
|
for (const auto& kv : v.get_object())
|
|
{
|
|
st.str += kv.key().size();
|
|
walk(kv.value(), st);
|
|
}
|
|
break;
|
|
case boost::json::kind::array:
|
|
for (const auto& c : v.get_array())
|
|
{
|
|
walk(c, st);
|
|
}
|
|
break;
|
|
case boost::json::kind::string:
|
|
st.str += v.get_string().size();
|
|
break;
|
|
case boost::json::kind::int64:
|
|
st.num += static_cast<double>(v.get_int64());
|
|
break;
|
|
case boost::json::kind::uint64:
|
|
st.num += static_cast<double>(v.get_uint64());
|
|
break;
|
|
case boost::json::kind::double_:
|
|
st.num += v.get_double();
|
|
break;
|
|
default:
|
|
break;
|
|
}
|
|
}
|
|
|
|
struct bj_null
|
|
{
|
|
using ec_t = boost::system::error_code;
|
|
using sv_t = boost::json::string_view;
|
|
static constexpr std::size_t max_array_size = static_cast<std::size_t>(-1);
|
|
static constexpr std::size_t max_object_size = static_cast<std::size_t>(-1);
|
|
static constexpr std::size_t max_string_size = static_cast<std::size_t>(-1);
|
|
static constexpr std::size_t max_key_size = static_cast<std::size_t>(-1);
|
|
bool on_document_begin(ec_t& /*ec*/)
|
|
{
|
|
return true;
|
|
}
|
|
bool on_document_end(ec_t& /*ec*/)
|
|
{
|
|
return true;
|
|
}
|
|
bool on_array_begin(ec_t& /*ec*/)
|
|
{
|
|
return true;
|
|
}
|
|
bool on_array_end(std::size_t /*n*/, ec_t& /*ec*/)
|
|
{
|
|
return true;
|
|
}
|
|
bool on_object_begin(ec_t& /*ec*/)
|
|
{
|
|
return true;
|
|
}
|
|
bool on_object_end(std::size_t /*n*/, ec_t& /*ec*/)
|
|
{
|
|
return true;
|
|
}
|
|
bool on_string_part(sv_t /*s*/, std::size_t /*n*/, ec_t& /*ec*/)
|
|
{
|
|
return true;
|
|
}
|
|
bool on_string(sv_t /*s*/, std::size_t /*n*/, ec_t& /*ec*/)
|
|
{
|
|
return true;
|
|
}
|
|
bool on_key_part(sv_t /*s*/, std::size_t /*n*/, ec_t& /*ec*/)
|
|
{
|
|
return true;
|
|
}
|
|
bool on_key(sv_t /*s*/, std::size_t /*n*/, ec_t& /*ec*/)
|
|
{
|
|
return true;
|
|
}
|
|
bool on_number_part(sv_t /*s*/, ec_t& /*ec*/)
|
|
{
|
|
return true;
|
|
}
|
|
bool on_int64(std::int64_t /*i*/, sv_t /*s*/, ec_t& /*ec*/)
|
|
{
|
|
return true;
|
|
}
|
|
bool on_uint64(std::uint64_t /*u*/, sv_t /*s*/, ec_t& /*ec*/)
|
|
{
|
|
return true;
|
|
}
|
|
bool on_double(double /*d*/, sv_t /*s*/, ec_t& /*ec*/)
|
|
{
|
|
return true;
|
|
}
|
|
bool on_bool(bool /*b*/, ec_t& /*ec*/)
|
|
{
|
|
return true;
|
|
}
|
|
bool on_null(ec_t& /*ec*/)
|
|
{
|
|
return true;
|
|
}
|
|
bool on_comment_part(sv_t /*s*/, ec_t& /*ec*/)
|
|
{
|
|
return true;
|
|
}
|
|
bool on_comment(sv_t /*s*/, ec_t& /*ec*/)
|
|
{
|
|
return true;
|
|
}
|
|
};
|
|
|
|
struct bj_sum : bj_null
|
|
{
|
|
stats* st;
|
|
explicit bj_sum(stats* s) : st(s) {}
|
|
bool on_array_begin(ec_t& /*ec*/)
|
|
{
|
|
++st->nodes;
|
|
return true;
|
|
}
|
|
bool on_object_begin(ec_t& /*ec*/)
|
|
{
|
|
++st->nodes;
|
|
return true;
|
|
}
|
|
bool on_string(sv_t /*s*/, std::size_t n, ec_t& /*ec*/)
|
|
{
|
|
++st->nodes;
|
|
st->str += n;
|
|
return true;
|
|
}
|
|
bool on_key(sv_t /*s*/, std::size_t n, ec_t& /*ec*/)
|
|
{
|
|
st->str += n;
|
|
return true;
|
|
}
|
|
bool on_int64(std::int64_t i, sv_t /*s*/, ec_t& /*ec*/)
|
|
{
|
|
++st->nodes;
|
|
st->num += static_cast<double>(i);
|
|
return true;
|
|
}
|
|
bool on_uint64(std::uint64_t u, sv_t /*s*/, ec_t& /*ec*/)
|
|
{
|
|
++st->nodes;
|
|
st->num += static_cast<double>(u);
|
|
return true;
|
|
}
|
|
bool on_double(double d, sv_t /*s*/, ec_t& /*ec*/)
|
|
{
|
|
++st->nodes;
|
|
st->num += d;
|
|
return true;
|
|
}
|
|
bool on_bool(bool /*b*/, ec_t& /*ec*/)
|
|
{
|
|
++st->nodes;
|
|
return true;
|
|
}
|
|
bool on_null(ec_t& /*ec*/)
|
|
{
|
|
++st->nodes;
|
|
return true;
|
|
}
|
|
};
|
|
|
|
template<class Handler, class... Args>
|
|
static bool bj_sax(const std::string& s, Args&& ... args)
|
|
{
|
|
boost::json::basic_parser<Handler> p(bj_precise, std::forward<Args>(args)...);
|
|
boost::system::error_code ec;
|
|
const std::size_t n = p.write_some(false, s.data(), s.size(), ec);
|
|
return !ec && n == s.size();
|
|
}
|
|
|
|
static bool bj_sax_none(const std::string& s)
|
|
{
|
|
boost::json::basic_parser<bj_null> p(bj_none);
|
|
boost::system::error_code ec;
|
|
const std::size_t n = p.write_some(false, s.data(), s.size(), ec);
|
|
return !ec && n == s.size();
|
|
}
|
|
|
|
// Boost.JSON with reuse, Boost's documented fast path: a basic_parser (SAX) that is reset per call, and a parser
|
|
// that is reset per call and builds its value in a monotonic_resource over a caller-owned buffer that is kept across
|
|
// calls (grown after a call that needed more, so the timed calls allocate nothing)
|
|
struct bj_counting final : boost::container::pmr::memory_resource
|
|
{
|
|
std::size_t bytes = 0;
|
|
void* do_allocate(std::size_t n, std::size_t align) override
|
|
{
|
|
bytes += n;
|
|
return ::operator new(n, std::align_val_t(align));
|
|
}
|
|
void do_deallocate(void* p, std::size_t n, std::size_t align) override
|
|
{
|
|
::operator delete(p, n, std::align_val_t(align));
|
|
}
|
|
bool do_is_equal(const boost::container::pmr::memory_resource& o) const noexcept override
|
|
{
|
|
return this == &o;
|
|
}
|
|
};
|
|
|
|
struct bj_reuse
|
|
{
|
|
boost::json::parser p{boost::json::storage_ptr(), bj_precise};
|
|
std::vector<unsigned char> buf = std::vector<unsigned char>(std::size_t{1} << 16);
|
|
bj_counting upstream;
|
|
boost::json::basic_parser<bj_null> sax_null{bj_precise};
|
|
boost::json::basic_parser<bj_sum> sax_sum{bj_precise, nullptr};
|
|
|
|
template<class Use>
|
|
bool parse(const std::string& s, Use&& use)
|
|
{
|
|
bool ok = false;
|
|
{
|
|
boost::json::monotonic_resource mr(buf.data(), buf.size(), &upstream);
|
|
p.reset(&mr);
|
|
boost::system::error_code ec;
|
|
p.write(s.data(), s.size(), ec);
|
|
if (!ec)
|
|
{
|
|
const boost::json::value v = p.release();
|
|
ok = use(v);
|
|
}
|
|
p.reset(); // drop the reference to mr
|
|
}
|
|
if (upstream.bytes != 0)
|
|
{
|
|
buf.resize(buf.size() + 2 * upstream.bytes);
|
|
upstream.bytes = 0;
|
|
}
|
|
return ok;
|
|
}
|
|
|
|
template<class Handler>
|
|
static bool sax(boost::json::basic_parser<Handler>& p, const std::string& s)
|
|
{
|
|
p.reset();
|
|
boost::system::error_code ec;
|
|
const std::size_t n = p.write_some(false, s.data(), s.size(), ec);
|
|
return !ec && n == s.size();
|
|
}
|
|
};
|
|
|
|
// yyjson with reuse: a dynamic allocator (yyjson_alc_dyn_new) that keeps the memory of freed documents for the next
|
|
// read
|
|
struct yy_alc
|
|
{
|
|
yyjson_alc* a = yyjson_alc_dyn_new();
|
|
yy_alc() = default;
|
|
yy_alc(const yy_alc&) = delete;
|
|
yy_alc& operator=(const yy_alc&) = delete;
|
|
~yy_alc()
|
|
{
|
|
yyjson_alc_dyn_free(a);
|
|
}
|
|
};
|
|
|
|
// RapidJSON with reuse: the "user buffer" setup of RapidJSON's documentation, a MemoryPoolAllocator for the values and
|
|
// one for the parse stack, each over a caller-owned buffer that is kept across calls (grown after a call that needed
|
|
// more chunks, so the timed calls allocate nothing)
|
|
using rj_pool_doc = rapidjson::GenericDocument<rapidjson::UTF8<>, rapidjson::MemoryPoolAllocator<>, rapidjson::MemoryPoolAllocator<>>;
|
|
struct rj_reuse
|
|
{
|
|
std::vector<char> vbuf = std::vector<char>(std::size_t{1} << 16);
|
|
std::vector<char> pbuf = std::vector<char>(std::size_t{1} << 16);
|
|
|
|
template<unsigned F, class Use>
|
|
bool parse(const std::string& s, Use&& use)
|
|
{
|
|
bool ok = false;
|
|
std::size_t vcap = 0, pcap = 0;
|
|
{
|
|
rapidjson::MemoryPoolAllocator<> va(vbuf.data(), vbuf.size());
|
|
rapidjson::MemoryPoolAllocator<> pa(pbuf.data(), pbuf.size());
|
|
{
|
|
rj_pool_doc d(&va, pbuf.size() / 4, &pa);
|
|
d.Parse<F>(s.data(), s.size());
|
|
ok = !d.HasParseError() && use(d);
|
|
}
|
|
// more than the user buffer: the allocator added chunks
|
|
vcap = va.Capacity();
|
|
pcap = pa.Capacity();
|
|
}
|
|
if (vcap > vbuf.size())
|
|
{
|
|
vbuf.resize(2 * vcap);
|
|
}
|
|
if (pcap > pbuf.size())
|
|
{
|
|
pbuf.resize(2 * pcap);
|
|
}
|
|
return ok;
|
|
}
|
|
};
|
|
|
|
///////////////////////////////////////////////////////////////////////////////
|
|
// select: read a few fields of every record (the fields of bench_view.cpp), one accessor per DOM
|
|
///////////////////////////////////////////////////////////////////////////////
|
|
|
|
struct acc_json
|
|
{
|
|
using V = const json*;
|
|
static V get(V v, const char* k)
|
|
{
|
|
return &(*v)[k];
|
|
}
|
|
static V at(V v, std::size_t i)
|
|
{
|
|
return &(*v)[i];
|
|
}
|
|
static std::uint64_t u64(V v)
|
|
{
|
|
return v->get<std::uint64_t>();
|
|
}
|
|
static std::int64_t i64(V v)
|
|
{
|
|
return v->get<std::int64_t>();
|
|
}
|
|
static std::string_view str(V v)
|
|
{
|
|
return v->get_ref<const std::string&>();
|
|
}
|
|
static std::size_t size(V v)
|
|
{
|
|
return v->size();
|
|
}
|
|
template<class F> static void each(V v, F&& f)
|
|
{
|
|
for (const json& e : *v)
|
|
{
|
|
f(&e);
|
|
}
|
|
}
|
|
};
|
|
|
|
struct acc_view
|
|
{
|
|
using V = json_view;
|
|
static V get(V v, const char* k)
|
|
{
|
|
return v[k];
|
|
}
|
|
static V at(V v, std::size_t i)
|
|
{
|
|
return v[i];
|
|
}
|
|
static std::uint64_t u64(V v)
|
|
{
|
|
return v.get<std::uint64_t>();
|
|
}
|
|
static std::int64_t i64(V v)
|
|
{
|
|
return v.get<std::int64_t>();
|
|
}
|
|
static std::string_view str(V v)
|
|
{
|
|
const auto s = v.get_string();
|
|
return {s.data(), s.size()};
|
|
}
|
|
static std::size_t size(V v)
|
|
{
|
|
return v.size();
|
|
}
|
|
template<class F> static void each(V v, F&& f)
|
|
{
|
|
for (const json_view e : v)
|
|
{
|
|
f(e);
|
|
}
|
|
}
|
|
};
|
|
|
|
struct acc_yyjson
|
|
{
|
|
using V = yyjson_val*;
|
|
static V get(V v, const char* k)
|
|
{
|
|
return yyjson_obj_get(v, k);
|
|
}
|
|
static V at(V v, std::size_t i)
|
|
{
|
|
return yyjson_arr_get(v, i);
|
|
}
|
|
static std::uint64_t u64(V v)
|
|
{
|
|
return yyjson_get_uint(v);
|
|
}
|
|
static std::int64_t i64(V v)
|
|
{
|
|
return yyjson_get_sint(v);
|
|
}
|
|
static std::string_view str(V v)
|
|
{
|
|
return {yyjson_get_str(v), yyjson_get_len(v)};
|
|
}
|
|
static std::size_t size(V v)
|
|
{
|
|
return yyjson_is_arr(v) ? yyjson_arr_size(v) : yyjson_obj_size(v);
|
|
}
|
|
template<class F> static void each(V v, F&& f)
|
|
{
|
|
std::size_t idx, max;
|
|
yyjson_val* e;
|
|
yyjson_arr_foreach(v, idx, max, e)
|
|
{
|
|
f(e);
|
|
}
|
|
}
|
|
};
|
|
|
|
// YYJSON_READ_NUMBER_AS_RAW: numbers are kept as text; the selected ones are converted exactly
|
|
struct acc_yyjson_raw : acc_yyjson
|
|
{
|
|
static std::uint64_t u64(V v)
|
|
{
|
|
return from_text<std::uint64_t>(yyjson_get_raw(v), yyjson_get_len(v));
|
|
}
|
|
static std::int64_t i64(V v)
|
|
{
|
|
return from_text<std::int64_t>(yyjson_get_raw(v), yyjson_get_len(v));
|
|
}
|
|
};
|
|
|
|
// the unchecked accessors (value_unsafe()); the selected fields exist and have the expected types
|
|
struct acc_simdjson
|
|
{
|
|
using V = simdjson::dom::element;
|
|
static V get(V v, const char* k)
|
|
{
|
|
return v[k].value_unsafe();
|
|
}
|
|
static V at(V v, std::size_t i)
|
|
{
|
|
return v.at(i).value_unsafe();
|
|
}
|
|
static std::uint64_t u64(V v)
|
|
{
|
|
return v.get_uint64().value_unsafe();
|
|
}
|
|
static std::int64_t i64(V v)
|
|
{
|
|
return v.get_int64().value_unsafe();
|
|
}
|
|
static std::string_view str(V v)
|
|
{
|
|
return v.get_string().value_unsafe();
|
|
}
|
|
static std::size_t size(V v)
|
|
{
|
|
return v.is_array() ? v.get_array().value_unsafe().size() : v.get_object().value_unsafe().size();
|
|
}
|
|
template<class F> static void each(V v, F&& f)
|
|
{
|
|
const simdjson::dom::array a = v.get_array().value_unsafe(); // (a local: see walk())
|
|
for (V e : a)
|
|
{
|
|
f(e);
|
|
}
|
|
}
|
|
};
|
|
|
|
struct acc_rapidjson
|
|
{
|
|
using V = const rapidjson::Value*;
|
|
static V get(V v, const char* k)
|
|
{
|
|
return &(*v)[k];
|
|
}
|
|
static V at(V v, std::size_t i)
|
|
{
|
|
return &(*v)[static_cast<rapidjson::SizeType>(i)];
|
|
}
|
|
static std::uint64_t u64(V v)
|
|
{
|
|
return v->GetUint64();
|
|
}
|
|
static std::int64_t i64(V v)
|
|
{
|
|
return v->GetInt64();
|
|
}
|
|
static std::string_view str(V v)
|
|
{
|
|
return {v->GetString(), v->GetStringLength()};
|
|
}
|
|
static std::size_t size(V v)
|
|
{
|
|
return v->IsArray() ? v->Size() : v->MemberCount();
|
|
}
|
|
template<class F> static void each(V v, F&& f)
|
|
{
|
|
for (const auto& e : v->GetArray())
|
|
{
|
|
f(&e);
|
|
}
|
|
}
|
|
};
|
|
|
|
// kParseNumbersAsStringsFlag: numbers are kept as strings; the selected ones are converted exactly
|
|
struct acc_rapidjson_raw : acc_rapidjson
|
|
{
|
|
static std::uint64_t u64(V v)
|
|
{
|
|
return from_text<std::uint64_t>(v->GetString(), v->GetStringLength());
|
|
}
|
|
static std::int64_t i64(V v)
|
|
{
|
|
return from_text<std::int64_t>(v->GetString(), v->GetStringLength());
|
|
}
|
|
};
|
|
|
|
struct acc_sonic
|
|
{
|
|
using V = const sonic_json::Node*;
|
|
static V get(V v, const char* k)
|
|
{
|
|
return &(*v)[k];
|
|
}
|
|
static V at(V v, std::size_t i)
|
|
{
|
|
return &(*v)[i];
|
|
}
|
|
static std::uint64_t u64(V v)
|
|
{
|
|
return v->GetUint64();
|
|
}
|
|
static std::int64_t i64(V v)
|
|
{
|
|
return v->GetInt64();
|
|
}
|
|
static std::string_view str(V v)
|
|
{
|
|
const auto s = v->GetStringView();
|
|
return {s.data(), s.size()};
|
|
}
|
|
static std::size_t size(V v)
|
|
{
|
|
return v->Size();
|
|
}
|
|
template<class F> static void each(V v, F&& f)
|
|
{
|
|
for (auto e = v->Begin(); e != v->End(); ++e)
|
|
{
|
|
f(&*e);
|
|
}
|
|
}
|
|
};
|
|
|
|
struct acc_boost
|
|
{
|
|
using V = const boost::json::value*;
|
|
static V get(V v, const char* k)
|
|
{
|
|
return &v->get_object().at(k);
|
|
}
|
|
static V at(V v, std::size_t i)
|
|
{
|
|
return &v->get_array().at(i);
|
|
}
|
|
static std::uint64_t u64(V v)
|
|
{
|
|
return v->to_number<std::uint64_t>();
|
|
}
|
|
static std::int64_t i64(V v)
|
|
{
|
|
return v->to_number<std::int64_t>();
|
|
}
|
|
static std::string_view str(V v)
|
|
{
|
|
const auto& s = v->get_string();
|
|
return {s.data(), s.size()};
|
|
}
|
|
static std::size_t size(V v)
|
|
{
|
|
return v->is_array() ? v->get_array().size() : v->get_object().size();
|
|
}
|
|
template<class F> static void each(V v, F&& f)
|
|
{
|
|
for (const auto& e : v->get_array())
|
|
{
|
|
f(&e);
|
|
}
|
|
}
|
|
};
|
|
|
|
#if !BENCH_NO_GLAZE
|
|
template<class G>
|
|
struct acc_glaze_t
|
|
{
|
|
using V = const G*;
|
|
static V get(V v, const char* k)
|
|
{
|
|
return &(*v)[k];
|
|
}
|
|
static V at(V v, std::size_t i)
|
|
{
|
|
return &v->get_array()[i];
|
|
}
|
|
static std::uint64_t u64(V v)
|
|
{
|
|
if constexpr (std::is_same_v<G, glz::generic>)
|
|
{
|
|
return static_cast<std::uint64_t>(std::get<double>(v->data));
|
|
}
|
|
else
|
|
{
|
|
return std::get<std::uint64_t>(v->data);
|
|
}
|
|
}
|
|
static std::int64_t i64(V v)
|
|
{
|
|
if constexpr (std::is_same_v<G, glz::generic>)
|
|
{
|
|
return static_cast<std::int64_t>(std::get<double>(v->data));
|
|
}
|
|
else
|
|
{
|
|
const auto* u = std::get_if<std::uint64_t>(&v->data);
|
|
return u != nullptr ? static_cast<std::int64_t>(*u) : std::get<std::int64_t>(v->data);
|
|
}
|
|
}
|
|
static std::string_view str(V v)
|
|
{
|
|
return std::get<std::string>(v->data);
|
|
}
|
|
static std::size_t size(V v)
|
|
{
|
|
return v->is_array() ? v->get_array().size() : v->get_object().size();
|
|
}
|
|
template<class F> static void each(V v, F&& f)
|
|
{
|
|
for (const auto& e : v->get_array())
|
|
{
|
|
f(&e);
|
|
}
|
|
}
|
|
};
|
|
using acc_glaze = acc_glaze_t<glz::generic_u64>;
|
|
#endif
|
|
|
|
template<class A>
|
|
static double pick(const std::string& name, typename A::V r)
|
|
{
|
|
using V = typename A::V;
|
|
double acc = 0;
|
|
auto status = [&](V s)
|
|
{
|
|
acc += static_cast<double>(A::u64(A::get(s, "id")));
|
|
acc += static_cast<double>(A::str(A::get(A::get(s, "user"), "screen_name")).size());
|
|
acc += static_cast<double>(A::i64(A::get(s, "retweet_count")));
|
|
};
|
|
if (name == "twitter")
|
|
{
|
|
A::each(A::get(r, "statuses"), status);
|
|
}
|
|
else if (name == "status")
|
|
{
|
|
status(r);
|
|
}
|
|
else if (name == "citm_catalog")
|
|
{
|
|
A::each(A::get(r, "performances"), [&](V p)
|
|
{
|
|
acc += static_cast<double>(A::u64(A::get(p, "id")) + A::u64(A::get(p, "eventId")) + A::size(A::get(p, "seatCategories")));
|
|
});
|
|
acc += static_cast<double>(A::size(A::get(r, "events")));
|
|
}
|
|
else if (name == "canada")
|
|
{
|
|
const V g = A::get(A::at(A::get(r, "features"), 0), "geometry");
|
|
acc += static_cast<double>(A::str(A::get(r, "type")).size() + A::str(A::get(g, "type")).size() + A::size(A::get(g, "coordinates")));
|
|
}
|
|
else if (name == "jeopardy")
|
|
{
|
|
A::each(r, [&](V q)
|
|
{
|
|
acc += A::str(A::get(q, "round")) == "Final Jeopardy!" ? 1 : 0;
|
|
acc += static_cast<double>(A::str(A::get(q, "category")).size());
|
|
});
|
|
}
|
|
return acc;
|
|
}
|
|
|
|
static double pick_od(const std::string& name, simdjson::ondemand::document& d)
|
|
{
|
|
double acc = 0;
|
|
if (name == "twitter" || name == "status")
|
|
{
|
|
auto one = [&](simdjson::ondemand::object s)
|
|
{
|
|
acc += static_cast<double>(uint64_t(s["id"]));
|
|
acc += static_cast<double>(std::string_view(s["user"]["screen_name"]).size());
|
|
acc += static_cast<double>(int64_t(s["retweet_count"]));
|
|
};
|
|
if (name == "twitter")
|
|
{
|
|
for (auto s : d["statuses"])
|
|
{
|
|
one(s.get_object());
|
|
}
|
|
}
|
|
else
|
|
{
|
|
one(d.get_object());
|
|
}
|
|
}
|
|
else if (name == "citm_catalog")
|
|
{
|
|
simdjson::ondemand::object ev = d["events"].get_object();
|
|
acc += static_cast<double>(ev.count_fields());
|
|
for (auto p : d["performances"])
|
|
{
|
|
simdjson::ondemand::object o = p.get_object();
|
|
const auto a = uint64_t(o["eventId"]) + uint64_t(o["id"]);
|
|
simdjson::ondemand::array sc = o["seatCategories"].get_array();
|
|
acc += static_cast<double>(a + sc.count_elements());
|
|
}
|
|
}
|
|
else if (name == "canada")
|
|
{
|
|
acc += static_cast<double>(std::string_view(d["type"]).size());
|
|
auto g = d["features"].at(0)["geometry"];
|
|
acc += static_cast<double>(std::string_view(g["type"]).size());
|
|
simdjson::ondemand::array co = g["coordinates"].get_array();
|
|
acc += static_cast<double>(co.count_elements());
|
|
}
|
|
else if (name == "jeopardy")
|
|
{
|
|
for (auto q : d)
|
|
{
|
|
simdjson::ondemand::object o = q.get_object();
|
|
acc += static_cast<double>(std::string_view(o["category"]).size());
|
|
acc += std::string_view(o["round"]) == "Final Jeopardy!" ? 1 : 0;
|
|
}
|
|
}
|
|
return acc;
|
|
}
|
|
|
|
// nlohmann/json reading into structs with from_json(): the NLOHMANN_DEFINE_TYPE_* macros, from a json value
|
|
// (json::parse(s).get<T>()) and from a view (json_document::parse(s).root().get<T>()); members that are only counted
|
|
// read into count_only. Glaze and simdjson read into the same types, serde (bench_rs) into the same fields
|
|
namespace nsel
|
|
{
|
|
struct count_only
|
|
{
|
|
std::size_t n = 0;
|
|
};
|
|
template < typename J, typename std::enable_if < nlohmann::is_json_view<J>::value || std::is_same<J, json>::value, int >::type = 0 >
|
|
void from_json(const J& j, count_only& c)
|
|
{
|
|
c.n = j.size();
|
|
}
|
|
struct user
|
|
{
|
|
std::string screen_name;
|
|
};
|
|
struct status
|
|
{
|
|
std::uint64_t id{};
|
|
nsel::user user;
|
|
std::int64_t retweet_count{};
|
|
};
|
|
struct twitter
|
|
{
|
|
std::vector<status> statuses;
|
|
};
|
|
struct performance
|
|
{
|
|
std::uint64_t id{};
|
|
std::uint64_t eventId{};
|
|
count_only seatCategories;
|
|
};
|
|
struct citm
|
|
{
|
|
std::map<std::string, count_only> events;
|
|
std::vector<performance> performances;
|
|
};
|
|
struct geometry
|
|
{
|
|
std::string type;
|
|
count_only coordinates;
|
|
};
|
|
struct feature
|
|
{
|
|
nsel::geometry geometry;
|
|
};
|
|
struct canada
|
|
{
|
|
std::string type;
|
|
std::vector<feature> features;
|
|
};
|
|
struct question
|
|
{
|
|
std::string category;
|
|
std::string round;
|
|
};
|
|
NLOHMANN_DEFINE_TYPE_NON_INTRUSIVE(user, screen_name)
|
|
NLOHMANN_DEFINE_TYPE_NON_INTRUSIVE(status, id, user, retweet_count)
|
|
NLOHMANN_DEFINE_TYPE_NON_INTRUSIVE(twitter, statuses)
|
|
NLOHMANN_DEFINE_TYPE_NON_INTRUSIVE(performance, id, eventId, seatCategories)
|
|
NLOHMANN_DEFINE_TYPE_NON_INTRUSIVE(citm, events, performances)
|
|
NLOHMANN_DEFINE_TYPE_NON_INTRUSIVE(geometry, type, coordinates)
|
|
NLOHMANN_DEFINE_TYPE_NON_INTRUSIVE(feature, geometry)
|
|
NLOHMANN_DEFINE_TYPE_NON_INTRUSIVE(canada, type, features)
|
|
NLOHMANN_DEFINE_TYPE_NON_INTRUSIVE(question, category, round)
|
|
} // namespace nsel
|
|
|
|
template<class J>
|
|
static double pick_nlohmann_structs(const std::string& name, const J& j)
|
|
{
|
|
double acc = 0;
|
|
auto one = [&](const nsel::status & x)
|
|
{
|
|
acc += static_cast<double>(x.id);
|
|
acc += static_cast<double>(x.user.screen_name.size());
|
|
acc += static_cast<double>(x.retweet_count);
|
|
};
|
|
if (name == "twitter")
|
|
{
|
|
for (const auto& x : j.template get<nsel::twitter>().statuses)
|
|
{
|
|
one(x);
|
|
}
|
|
}
|
|
else if (name == "status")
|
|
{
|
|
one(j.template get<nsel::status>());
|
|
}
|
|
else if (name == "citm_catalog")
|
|
{
|
|
const auto c = j.template get<nsel::citm>();
|
|
for (const auto& p : c.performances)
|
|
{
|
|
acc += static_cast<double>(p.id + p.eventId + p.seatCategories.n);
|
|
}
|
|
acc += static_cast<double>(c.events.size());
|
|
}
|
|
else if (name == "canada")
|
|
{
|
|
const auto c = j.template get<nsel::canada>();
|
|
acc += static_cast<double>(c.type.size() + c.features[0].geometry.type.size() + c.features[0].geometry.coordinates.n);
|
|
}
|
|
else if (name == "jeopardy")
|
|
{
|
|
for (const auto& x : j.template get<std::vector<nsel::question>>())
|
|
{
|
|
acc += x.round == "Final Jeopardy!" ? 1 : 0;
|
|
acc += static_cast<double>(x.category.size());
|
|
}
|
|
}
|
|
return acc;
|
|
}
|
|
|
|
#if !BENCH_NO_GLAZE
|
|
// Glaze reading into the same structs as nlohmann/json's from_json() (nsel; Glaze reflects them), skipping unknown
|
|
// keys; count_only counts the elements of an array or the members of an object, skipping each of them (and
|
|
// validating it with validate_skipped)
|
|
template<>
|
|
struct glz::from<glz::JSON, nsel::count_only>
|
|
{
|
|
template<auto Options>
|
|
static void op(auto&& value, is_context auto&& ctx, auto&& it, auto end)
|
|
{
|
|
constexpr auto Opts = ws_handled_off<Options>();
|
|
if constexpr (!check_ws_handled(Options))
|
|
{
|
|
if (skip_ws<Opts>(ctx, it, end))
|
|
{
|
|
return;
|
|
}
|
|
}
|
|
const bool object = *it == '{';
|
|
if (!object && *it != '[') [[unlikely]]
|
|
{
|
|
ctx.error = error_code::syntax_error;
|
|
return;
|
|
}
|
|
const char close = object ? '}' : ']';
|
|
if (enter_depth(ctx)) [[unlikely]]
|
|
{
|
|
return;
|
|
}
|
|
++it;
|
|
if constexpr (!Opts.null_terminated)
|
|
{
|
|
if (it == end) [[unlikely]]
|
|
{
|
|
ctx.error = error_code::unexpected_end;
|
|
return;
|
|
}
|
|
}
|
|
if (skip_ws<Opts>(ctx, it, end))
|
|
{
|
|
return;
|
|
}
|
|
std::size_t n = 0;
|
|
if (*it != close)
|
|
{
|
|
while (true)
|
|
{
|
|
if (object)
|
|
{
|
|
if (*it != '"') [[unlikely]]
|
|
{
|
|
ctx.error = error_code::syntax_error;
|
|
return;
|
|
}
|
|
skip_string<Opts>(ctx, it, end);
|
|
if (bool(ctx.error)) [[unlikely]]
|
|
{
|
|
return;
|
|
}
|
|
if (skip_ws<Opts>(ctx, it, end) || match_invalid_end<':', Opts>(ctx, it, end) || skip_ws<Opts>(ctx, it, end))
|
|
{
|
|
return;
|
|
}
|
|
}
|
|
skip_value<JSON>::op<Opts>(ctx, it, end);
|
|
if (bool(ctx.error)) [[unlikely]]
|
|
{
|
|
return;
|
|
}
|
|
++n;
|
|
if (skip_ws<Opts>(ctx, it, end))
|
|
{
|
|
return;
|
|
}
|
|
if (*it != ',')
|
|
{
|
|
break;
|
|
}
|
|
++it;
|
|
if constexpr (!Opts.null_terminated)
|
|
{
|
|
if (it == end) [[unlikely]]
|
|
{
|
|
ctx.error = error_code::unexpected_end;
|
|
return;
|
|
}
|
|
}
|
|
if (skip_ws<Opts>(ctx, it, end))
|
|
{
|
|
return;
|
|
}
|
|
}
|
|
if (*it != close) [[unlikely]]
|
|
{
|
|
ctx.error = error_code::syntax_error;
|
|
return;
|
|
}
|
|
}
|
|
++it;
|
|
--ctx.depth;
|
|
value.n = n;
|
|
}
|
|
};
|
|
|
|
// validate_skipped: Glaze's option to validate the values it skips (an extension of glz::opts, hence the struct);
|
|
// validate_trailing_whitespace: without it, Glaze accepts anything after the value ("[1]]", "{}x")
|
|
struct sel_opts_validated : glz::opts
|
|
{
|
|
bool validate_skipped = true;
|
|
bool validate_trailing_whitespace = true;
|
|
};
|
|
// the "(strict)" variants: validate_trailing_whitespace when reading, and escape_control_characters when writing
|
|
// (without it, Glaze writes the control characters of a string, e.g. U+0001, unescaped, which is not JSON)
|
|
struct glz_opts_strict : glz::opts
|
|
{
|
|
bool validate_trailing_whitespace = true;
|
|
bool escape_control_characters = true;
|
|
};
|
|
constexpr glz_opts_strict glz_strict{};
|
|
constexpr glz::opts sel_opts_fast{.error_on_unknown_keys = false};
|
|
constexpr sel_opts_validated sel_opts_full{{.error_on_unknown_keys = false}};
|
|
|
|
template<auto sel_opts>
|
|
static bool pick_glaze_structs(const std::string& name, const std::string& s, double& acc)
|
|
{
|
|
acc = 0;
|
|
auto one = [&](const nsel::status & x)
|
|
{
|
|
acc += static_cast<double>(x.id);
|
|
acc += static_cast<double>(x.user.screen_name.size());
|
|
acc += static_cast<double>(x.retweet_count);
|
|
};
|
|
if (name == "twitter")
|
|
{
|
|
nsel::twitter t;
|
|
if (glz::read<sel_opts>(t, s))
|
|
{
|
|
return false;
|
|
}
|
|
for (const auto& x : t.statuses)
|
|
{
|
|
one(x);
|
|
}
|
|
}
|
|
else if (name == "status")
|
|
{
|
|
nsel::status x;
|
|
if (glz::read<sel_opts>(x, s))
|
|
{
|
|
return false;
|
|
}
|
|
one(x);
|
|
}
|
|
else if (name == "citm_catalog")
|
|
{
|
|
nsel::citm c;
|
|
if (glz::read<sel_opts>(c, s))
|
|
{
|
|
return false;
|
|
}
|
|
for (const auto& p : c.performances)
|
|
{
|
|
acc += static_cast<double>(p.id + p.eventId + p.seatCategories.n);
|
|
}
|
|
acc += static_cast<double>(c.events.size());
|
|
}
|
|
else if (name == "canada")
|
|
{
|
|
nsel::canada c;
|
|
if (glz::read<sel_opts>(c, s))
|
|
{
|
|
return false;
|
|
}
|
|
acc += static_cast<double>(c.type.size() + c.features[0].geometry.type.size() + c.features[0].geometry.coordinates.n);
|
|
}
|
|
else if (name == "jeopardy")
|
|
{
|
|
std::vector<nsel::question> q;
|
|
if (glz::read<sel_opts>(q, s))
|
|
{
|
|
return false;
|
|
}
|
|
for (const auto& x : q)
|
|
{
|
|
acc += x.round == "Final Jeopardy!" ? 1 : 0;
|
|
acc += static_cast<double>(x.category.size());
|
|
}
|
|
}
|
|
return true;
|
|
}
|
|
|
|
template<class Str = std::string, class D>
|
|
static double pick_glaze_lazy(const std::string& name, const D& d)
|
|
{
|
|
double acc = 0;
|
|
auto one = [&](const auto & x)
|
|
{
|
|
acc += static_cast<double>(x["id"].template get<std::uint64_t>().value());
|
|
acc += static_cast<double>(x["user"]["screen_name"].template get<Str>().value().size());
|
|
acc += static_cast<double>(x["retweet_count"].template get<std::int64_t>().value());
|
|
};
|
|
if (name == "twitter")
|
|
{
|
|
for (auto x : d["statuses"])
|
|
{
|
|
one(x);
|
|
}
|
|
}
|
|
else if (name == "status")
|
|
{
|
|
one(d.root());
|
|
}
|
|
else if (name == "citm_catalog")
|
|
{
|
|
for (auto p : d["performances"])
|
|
{
|
|
acc += static_cast<double>(p["id"].template get<std::uint64_t>().value() + p["eventId"].template get<std::uint64_t>().value() + p["seatCategories"].size());
|
|
}
|
|
acc += static_cast<double>(d["events"].size());
|
|
}
|
|
else if (name == "canada")
|
|
{
|
|
auto g = d["features"][0]["geometry"];
|
|
acc += static_cast<double>(d["type"].template get<Str>().value().size() + g["type"].template get<Str>().value().size() + g["coordinates"].size());
|
|
}
|
|
else if (name == "jeopardy")
|
|
{
|
|
for (auto q : d.root())
|
|
{
|
|
acc += q["round"].template get<Str>().value() == "Final Jeopardy!" ? 1 : 0;
|
|
acc += static_cast<double>(q["category"].template get<Str>().value().size());
|
|
}
|
|
}
|
|
return acc;
|
|
}
|
|
#endif
|
|
|
|
// simdjson On-Demand reading into the same structs as nlohmann/json's from_json() (nsel), with simdjson's documented
|
|
// custom-type deserialization (tag_invoke, C++20); fields are looked up in document order; count_only counts with
|
|
// count_elements() / count_fields()
|
|
namespace simdjson
|
|
{
|
|
template<typename V>
|
|
error_code tag_invoke(deserialize_tag /*tag*/, V& val, nsel::count_only& c)
|
|
{
|
|
ondemand::json_type t;
|
|
SIMDJSON_TRY(val.type().get(t));
|
|
std::size_t n = 0;
|
|
if (t == ondemand::json_type::array)
|
|
{
|
|
ondemand::array a;
|
|
SIMDJSON_TRY(val.get_array().get(a));
|
|
SIMDJSON_TRY(a.count_elements().get(n));
|
|
}
|
|
else if (t == ondemand::json_type::object)
|
|
{
|
|
ondemand::object o;
|
|
SIMDJSON_TRY(val.get_object().get(o));
|
|
SIMDJSON_TRY(o.count_fields().get(n));
|
|
}
|
|
else
|
|
{
|
|
return INCORRECT_TYPE;
|
|
}
|
|
c.n = n;
|
|
return SUCCESS;
|
|
}
|
|
|
|
template<typename V>
|
|
error_code tag_invoke(deserialize_tag /*tag*/, V& val, nsel::user& u)
|
|
{
|
|
ondemand::object o;
|
|
SIMDJSON_TRY(val.get_object().get(o));
|
|
return o["screen_name"].get_string(u.screen_name);
|
|
}
|
|
|
|
template<typename V>
|
|
error_code tag_invoke(deserialize_tag /*tag*/, V& val, nsel::status& x)
|
|
{
|
|
ondemand::object o;
|
|
SIMDJSON_TRY(val.get_object().get(o));
|
|
SIMDJSON_TRY(o["id"].get(x.id));
|
|
SIMDJSON_TRY(o["user"].get(x.user));
|
|
return o["retweet_count"].get(x.retweet_count);
|
|
}
|
|
|
|
template<typename V>
|
|
error_code tag_invoke(deserialize_tag /*tag*/, V& val, nsel::twitter& x)
|
|
{
|
|
ondemand::object o;
|
|
SIMDJSON_TRY(val.get_object().get(o));
|
|
return o["statuses"].get(x.statuses);
|
|
}
|
|
|
|
template<typename V>
|
|
error_code tag_invoke(deserialize_tag /*tag*/, V& val, nsel::performance& x)
|
|
{
|
|
ondemand::object o;
|
|
SIMDJSON_TRY(val.get_object().get(o));
|
|
SIMDJSON_TRY(o["eventId"].get(x.eventId));
|
|
SIMDJSON_TRY(o["id"].get(x.id));
|
|
return o["seatCategories"].get(x.seatCategories);
|
|
}
|
|
|
|
template<typename V>
|
|
error_code tag_invoke(deserialize_tag /*tag*/, V& val, nsel::citm& x)
|
|
{
|
|
ondemand::object o;
|
|
SIMDJSON_TRY(val.get_object().get(o));
|
|
ondemand::object ev;
|
|
SIMDJSON_TRY(o["events"].get_object().get(ev));
|
|
for (auto f : ev)
|
|
{
|
|
std::string_view k;
|
|
SIMDJSON_TRY(f.unescaped_key().get(k));
|
|
nsel::count_only c;
|
|
SIMDJSON_TRY(f.value().get(c));
|
|
x.events.emplace(std::string(k), c);
|
|
}
|
|
return o["performances"].get(x.performances);
|
|
}
|
|
|
|
template<typename V>
|
|
error_code tag_invoke(deserialize_tag /*tag*/, V& val, nsel::geometry& x)
|
|
{
|
|
ondemand::object o;
|
|
SIMDJSON_TRY(val.get_object().get(o));
|
|
SIMDJSON_TRY(o["type"].get_string(x.type));
|
|
return o["coordinates"].get(x.coordinates);
|
|
}
|
|
|
|
template<typename V>
|
|
error_code tag_invoke(deserialize_tag /*tag*/, V& val, nsel::feature& x)
|
|
{
|
|
ondemand::object o;
|
|
SIMDJSON_TRY(val.get_object().get(o));
|
|
return o["geometry"].get(x.geometry);
|
|
}
|
|
|
|
template<typename V>
|
|
error_code tag_invoke(deserialize_tag /*tag*/, V& val, nsel::canada& x)
|
|
{
|
|
ondemand::object o;
|
|
SIMDJSON_TRY(val.get_object().get(o));
|
|
SIMDJSON_TRY(o["type"].get_string(x.type));
|
|
return o["features"].get(x.features);
|
|
}
|
|
|
|
template<typename V>
|
|
error_code tag_invoke(deserialize_tag /*tag*/, V& val, nsel::question& x)
|
|
{
|
|
ondemand::object o;
|
|
SIMDJSON_TRY(val.get_object().get(o));
|
|
SIMDJSON_TRY(o["category"].get_string(x.category));
|
|
return o["round"].get_string(x.round);
|
|
}
|
|
} // namespace simdjson
|
|
|
|
// the same accumulation as pick_nlohmann_structs, with simdjson's get<T>() (throws on error)
|
|
static double pick_simdjson_structs(const std::string& name, simdjson::ondemand::document& d)
|
|
{
|
|
double acc = 0;
|
|
auto one = [&](const nsel::status & x)
|
|
{
|
|
acc += static_cast<double>(x.id);
|
|
acc += static_cast<double>(x.user.screen_name.size());
|
|
acc += static_cast<double>(x.retweet_count);
|
|
};
|
|
if (name == "twitter")
|
|
{
|
|
const auto t = d.get<nsel::twitter>().value(); // (a local: see walk())
|
|
for (const auto& x : t.statuses)
|
|
{
|
|
one(x);
|
|
}
|
|
}
|
|
else if (name == "status")
|
|
{
|
|
one(d.get<nsel::status>().value());
|
|
}
|
|
else if (name == "citm_catalog")
|
|
{
|
|
const auto c = d.get<nsel::citm>().value();
|
|
for (const auto& p : c.performances)
|
|
{
|
|
acc += static_cast<double>(p.id + p.eventId + p.seatCategories.n);
|
|
}
|
|
acc += static_cast<double>(c.events.size());
|
|
}
|
|
else if (name == "canada")
|
|
{
|
|
const auto c = d.get<nsel::canada>().value();
|
|
acc += static_cast<double>(c.type.size() + c.features[0].geometry.type.size() + c.features[0].geometry.coordinates.n);
|
|
}
|
|
else if (name == "jeopardy")
|
|
{
|
|
const auto qs = d.get<std::vector<nsel::question>>().value();
|
|
for (const auto& x : qs)
|
|
{
|
|
acc += x.round == "Final Jeopardy!" ? 1 : 0;
|
|
acc += static_cast<double>(x.category.size());
|
|
}
|
|
}
|
|
return acc;
|
|
}
|
|
|
|
// simdjson 5.0 key selectors (object::for_each<"k1", "k2", ...>): each object is walked once, and its keys are
|
|
// matched with a compile-time perfect hash in any order
|
|
static simdjson::error_code pick_od_keys(const std::string& name, simdjson::ondemand::document& d, double& acc)
|
|
{
|
|
using namespace simdjson;
|
|
using ondemand::value;
|
|
acc = 0;
|
|
auto status = [&](ondemand::object s) -> error_code
|
|
{
|
|
std::uint64_t id = 0;
|
|
std::int64_t rc = 0;
|
|
std::string_view sn;
|
|
SIMDJSON_TRY((s.for_each<"id", "user", "retweet_count">(id, [&](value u) -> error_code
|
|
{
|
|
ondemand::object uo;
|
|
SIMDJSON_TRY(u.get_object().get(uo));
|
|
return uo.for_each<"screen_name">(sn);
|
|
}, rc)));
|
|
acc += static_cast<double>(id);
|
|
acc += static_cast<double>(sn.size());
|
|
acc += static_cast<double>(rc);
|
|
return SUCCESS;
|
|
};
|
|
if (name == "twitter" || name == "status" || name == "citm_catalog" || name == "canada")
|
|
{
|
|
ondemand::object root;
|
|
SIMDJSON_TRY(d.get_object().get(root));
|
|
if (name == "status")
|
|
{
|
|
return status(root);
|
|
}
|
|
if (name == "twitter")
|
|
{
|
|
return root.for_each<"statuses">([&](value v) -> error_code
|
|
{
|
|
ondemand::array a;
|
|
SIMDJSON_TRY(v.get_array().get(a));
|
|
for (auto x : a)
|
|
{
|
|
ondemand::object o;
|
|
SIMDJSON_TRY(x.get_object().get(o));
|
|
SIMDJSON_TRY(status(o));
|
|
}
|
|
return SUCCESS;
|
|
});
|
|
}
|
|
if (name == "citm_catalog")
|
|
{
|
|
std::size_t events = 0;
|
|
SIMDJSON_TRY((root.for_each<"events", "performances">([&](value v) -> error_code
|
|
{
|
|
ondemand::object ev;
|
|
SIMDJSON_TRY(v.get_object().get(ev));
|
|
return ev.count_fields().get(events);
|
|
}, [&](value v) -> error_code
|
|
{
|
|
ondemand::array a;
|
|
SIMDJSON_TRY(v.get_array().get(a));
|
|
for (auto x : a)
|
|
{
|
|
ondemand::object p;
|
|
SIMDJSON_TRY(x.get_object().get(p));
|
|
std::uint64_t id = 0, event_id = 0;
|
|
std::size_t n = 0;
|
|
SIMDJSON_TRY((p.for_each<"eventId", "id", "seatCategories">(event_id, id, [&](value sc) -> error_code
|
|
{
|
|
ondemand::array sca;
|
|
SIMDJSON_TRY(sc.get_array().get(sca));
|
|
return sca.count_elements().get(n);
|
|
})));
|
|
acc += static_cast<double>(id + event_id + n);
|
|
}
|
|
return SUCCESS;
|
|
})));
|
|
acc += static_cast<double>(events);
|
|
return SUCCESS;
|
|
}
|
|
std::string_view type, gtype;
|
|
std::size_t n = 0;
|
|
SIMDJSON_TRY((root.for_each<"type", "features">(type, [&](value v) -> error_code
|
|
{
|
|
ondemand::array fa;
|
|
SIMDJSON_TRY(v.get_array().get(fa));
|
|
for (auto f : fa)
|
|
{
|
|
ondemand::object fo;
|
|
SIMDJSON_TRY(f.get_object().get(fo));
|
|
return fo.for_each<"geometry">([&](value g) -> error_code
|
|
{
|
|
ondemand::object go;
|
|
SIMDJSON_TRY(g.get_object().get(go));
|
|
return go.for_each<"type", "coordinates">(gtype, [&](value c) -> error_code
|
|
{
|
|
ondemand::array ca;
|
|
SIMDJSON_TRY(c.get_array().get(ca));
|
|
return ca.count_elements().get(n);
|
|
});
|
|
});
|
|
}
|
|
return SUCCESS;
|
|
})));
|
|
acc += static_cast<double>(type.size() + gtype.size() + n);
|
|
return SUCCESS;
|
|
}
|
|
if (name == "jeopardy")
|
|
{
|
|
ondemand::array a;
|
|
SIMDJSON_TRY(d.get_array().get(a));
|
|
for (auto q : a)
|
|
{
|
|
ondemand::object o;
|
|
SIMDJSON_TRY(q.get_object().get(o));
|
|
std::string_view category, round;
|
|
SIMDJSON_TRY((o.for_each<"category", "round">(category, round)));
|
|
acc += round == "Final Jeopardy!" ? 1 : 0;
|
|
acc += static_cast<double>(category.size());
|
|
}
|
|
}
|
|
return SUCCESS;
|
|
}
|
|
|
|
// Boost.JSON parse_into (direct parsing, no DOM) into Boost.Describe structs; unknown keys are ignored. parse_into
|
|
// has no type that skips any value, so the counted members are "sequences" that only count what parse_into converts
|
|
// into them: count_seq<any_object> (any_object: a described struct without members, which ignores all keys) for
|
|
// seatCategories, three levels of count_seq down to double for canada's coordinates; the events values are
|
|
// any_object, so their members are not counted (nlohmann's count_only counts them)
|
|
namespace bsel
|
|
{
|
|
struct any_object {};
|
|
BOOST_DESCRIBE_STRUCT(any_object, (), ())
|
|
template<class T>
|
|
struct count_seq
|
|
{
|
|
using value_type = T;
|
|
std::size_t n = 0;
|
|
void push_back(const T& /*x*/)
|
|
{
|
|
++n;
|
|
}
|
|
void clear()
|
|
{
|
|
n = 0;
|
|
}
|
|
const T* begin() const
|
|
{
|
|
return nullptr;
|
|
}
|
|
const T* end() const
|
|
{
|
|
return nullptr;
|
|
}
|
|
};
|
|
struct user
|
|
{
|
|
std::string screen_name;
|
|
};
|
|
BOOST_DESCRIBE_STRUCT(user, (), (screen_name))
|
|
struct status
|
|
{
|
|
std::uint64_t id{};
|
|
bsel::user user;
|
|
std::int64_t retweet_count{};
|
|
};
|
|
BOOST_DESCRIBE_STRUCT(status, (), (id, user, retweet_count))
|
|
struct twitter
|
|
{
|
|
std::vector<status> statuses;
|
|
};
|
|
BOOST_DESCRIBE_STRUCT(twitter, (), (statuses))
|
|
struct performance
|
|
{
|
|
std::uint64_t id{};
|
|
std::uint64_t eventId{};
|
|
count_seq<any_object> seatCategories;
|
|
};
|
|
BOOST_DESCRIBE_STRUCT(performance, (), (id, eventId, seatCategories))
|
|
struct citm
|
|
{
|
|
std::map<std::string, any_object> events;
|
|
std::vector<performance> performances;
|
|
};
|
|
BOOST_DESCRIBE_STRUCT(citm, (), (events, performances))
|
|
struct geometry
|
|
{
|
|
std::string type;
|
|
count_seq<count_seq<count_seq<double>>> coordinates;
|
|
};
|
|
BOOST_DESCRIBE_STRUCT(geometry, (), (type, coordinates))
|
|
struct feature
|
|
{
|
|
bsel::geometry geometry;
|
|
};
|
|
BOOST_DESCRIBE_STRUCT(feature, (), (geometry))
|
|
struct canada
|
|
{
|
|
std::string type;
|
|
std::vector<feature> features;
|
|
};
|
|
BOOST_DESCRIBE_STRUCT(canada, (), (type, features))
|
|
struct question
|
|
{
|
|
std::string category;
|
|
std::string round;
|
|
};
|
|
BOOST_DESCRIBE_STRUCT(question, (), (category, round))
|
|
} // namespace bsel
|
|
|
|
static bool pick_boost_into(const std::string& name, const std::string& s, double& acc)
|
|
{
|
|
acc = 0;
|
|
boost::system::error_code ec;
|
|
auto one = [&](const bsel::status & x)
|
|
{
|
|
acc += static_cast<double>(x.id);
|
|
acc += static_cast<double>(x.user.screen_name.size());
|
|
acc += static_cast<double>(x.retweet_count);
|
|
};
|
|
if (name == "twitter")
|
|
{
|
|
bsel::twitter t;
|
|
boost::json::parse_into(t, s, ec, bj_precise);
|
|
if (ec)
|
|
{
|
|
return false;
|
|
}
|
|
for (const auto& x : t.statuses)
|
|
{
|
|
one(x);
|
|
}
|
|
}
|
|
else if (name == "status")
|
|
{
|
|
bsel::status x;
|
|
boost::json::parse_into(x, s, ec, bj_precise);
|
|
if (ec)
|
|
{
|
|
return false;
|
|
}
|
|
one(x);
|
|
}
|
|
else if (name == "citm_catalog")
|
|
{
|
|
bsel::citm c;
|
|
boost::json::parse_into(c, s, ec, bj_precise);
|
|
if (ec)
|
|
{
|
|
return false;
|
|
}
|
|
for (const auto& p : c.performances)
|
|
{
|
|
acc += static_cast<double>(p.id + p.eventId + p.seatCategories.n);
|
|
}
|
|
acc += static_cast<double>(c.events.size());
|
|
}
|
|
else if (name == "canada")
|
|
{
|
|
bsel::canada c;
|
|
boost::json::parse_into(c, s, ec, bj_precise);
|
|
if (ec)
|
|
{
|
|
return false;
|
|
}
|
|
acc += static_cast<double>(c.type.size() + c.features[0].geometry.type.size() + c.features[0].geometry.coordinates.n);
|
|
}
|
|
else if (name == "jeopardy")
|
|
{
|
|
std::vector<bsel::question> q;
|
|
boost::json::parse_into(q, s, ec, bj_precise);
|
|
if (ec)
|
|
{
|
|
return false;
|
|
}
|
|
for (const auto& x : q)
|
|
{
|
|
acc += x.round == "Final Jeopardy!" ? 1 : 0;
|
|
acc += static_cast<double>(x.category.size());
|
|
}
|
|
}
|
|
return true;
|
|
}
|
|
|
|
///////////////////////////////////////////////////////////////////////////////
|
|
// edit: change some values of every record and write the document as compact JSON
|
|
// twitter (and status): for every status: retweet_count + 1, erase "entities", add "seen": true
|
|
// citm_catalog: for every performance: start + 1, erase "seatMapImage", add "seen": true
|
|
// canada: features[0].properties.name = "Kanada"; append [0.5,0.25] to coordinates[0] of every feature
|
|
// jeopardy: for every record: value = null, erase "air_date", add "seen": true
|
|
///////////////////////////////////////////////////////////////////////////////
|
|
|
|
// basic_json: the reference for every other library, and the json/ordered_json variants
|
|
template<class J>
|
|
static void edit(const std::string& name, J& r)
|
|
{
|
|
auto status = [](J & s)
|
|
{
|
|
s["retweet_count"] = s["retweet_count"].template get<std::int64_t>() + 1;
|
|
s.erase("entities");
|
|
s["seen"] = true;
|
|
};
|
|
if (name == "twitter")
|
|
{
|
|
for (J& s : r["statuses"])
|
|
{
|
|
status(s);
|
|
}
|
|
}
|
|
else if (name == "status")
|
|
{
|
|
status(r);
|
|
}
|
|
else if (name == "citm_catalog")
|
|
{
|
|
for (J& p : r["performances"])
|
|
{
|
|
p["start"] = p["start"].template get<std::int64_t>() + 1;
|
|
p.erase("seatMapImage");
|
|
p["seen"] = true;
|
|
}
|
|
}
|
|
else if (name == "canada")
|
|
{
|
|
r["features"][0]["properties"]["name"] = "Kanada";
|
|
for (J& f : r["features"])
|
|
{
|
|
f["geometry"]["coordinates"][0].push_back(J::array({0.5, 0.25}));
|
|
}
|
|
}
|
|
else if (name == "jeopardy")
|
|
{
|
|
for (J& q : r)
|
|
{
|
|
q["value"] = nullptr;
|
|
q.erase("air_date");
|
|
q["seen"] = true;
|
|
}
|
|
}
|
|
}
|
|
|
|
static bool edit_document(const std::string& name, const std::string& s, result& res, bool source_numbers)
|
|
{
|
|
json_editable_document d = json_editable_document::parse(s);
|
|
const json_editable_view r = d.root();
|
|
auto status = [&](json_editable_view st)
|
|
{
|
|
d.set(st, "retweet_count", st["retweet_count"].get<std::int64_t>() + 1);
|
|
d.erase(st, "entities");
|
|
d.set(st, "seen", true);
|
|
};
|
|
if (name == "twitter")
|
|
{
|
|
for (const json_editable_view st : r["statuses"])
|
|
{
|
|
status(st);
|
|
}
|
|
}
|
|
else if (name == "status")
|
|
{
|
|
status(r);
|
|
}
|
|
else if (name == "citm_catalog")
|
|
{
|
|
for (const json_editable_view p : r["performances"])
|
|
{
|
|
d.set(p, "start", p["start"].get<std::int64_t>() + 1);
|
|
d.erase(p, "seatMapImage");
|
|
d.set(p, "seen", true);
|
|
}
|
|
}
|
|
else if (name == "canada")
|
|
{
|
|
d.set(r["features"][0]["properties"], "name", "Kanada");
|
|
for (const json_editable_view f : r["features"])
|
|
{
|
|
d.push_back(f["geometry"]["coordinates"][0], json::array({0.5, 0.25}));
|
|
}
|
|
}
|
|
else if (name == "jeopardy")
|
|
{
|
|
for (const json_editable_view q : r)
|
|
{
|
|
d.set(q, "value", nullptr);
|
|
d.erase(q, "air_date");
|
|
d.set(q, "seen", true);
|
|
}
|
|
}
|
|
return emit(res, source_numbers ? r.dump(-1, ' ', false, json_editable_view::number_format::source) : r.dump());
|
|
}
|
|
|
|
static std::int64_t yy_int(const yyjson_mut_val* v)
|
|
{
|
|
return yyjson_mut_is_uint(v) ? static_cast<std::int64_t>(yyjson_mut_get_uint(v)) : yyjson_mut_get_sint(v);
|
|
}
|
|
|
|
static bool edit_yyjson(const std::string& name, yyjson_doc* idoc, result& res)
|
|
{
|
|
if (idoc == nullptr)
|
|
{
|
|
return false;
|
|
}
|
|
yyjson_mut_doc* d = yyjson_doc_mut_copy(idoc, nullptr);
|
|
yyjson_mut_val* r = yyjson_mut_doc_get_root(d);
|
|
auto get = [](yyjson_mut_val * o, const char* k)
|
|
{
|
|
return yyjson_mut_obj_get(o, k);
|
|
};
|
|
auto status = [&](yyjson_mut_val * st)
|
|
{
|
|
yyjson_mut_val* rc = get(st, "retweet_count");
|
|
yyjson_mut_set_sint(rc, yy_int(rc) + 1);
|
|
yyjson_mut_obj_remove_key(st, "entities");
|
|
yyjson_mut_obj_add_bool(d, st, "seen", true);
|
|
};
|
|
std::size_t idx, max;
|
|
yyjson_mut_val* e;
|
|
if (name == "twitter")
|
|
{
|
|
yyjson_mut_arr_foreach(get(r, "statuses"), idx, max, e)
|
|
{
|
|
status(e);
|
|
}
|
|
}
|
|
else if (name == "status")
|
|
{
|
|
status(r);
|
|
}
|
|
else if (name == "citm_catalog")
|
|
{
|
|
yyjson_mut_arr_foreach(get(r, "performances"), idx, max, e)
|
|
{
|
|
yyjson_mut_val* start = get(e, "start");
|
|
yyjson_mut_set_sint(start, yy_int(start) + 1);
|
|
yyjson_mut_obj_remove_key(e, "seatMapImage");
|
|
yyjson_mut_obj_add_bool(d, e, "seen", true);
|
|
}
|
|
}
|
|
else if (name == "canada")
|
|
{
|
|
static const double point[2] = {0.5, 0.25};
|
|
yyjson_mut_val* features = get(r, "features");
|
|
yyjson_mut_set_str(get(get(yyjson_mut_arr_get(features, 0), "properties"), "name"), "Kanada");
|
|
yyjson_mut_arr_foreach(features, idx, max, e)
|
|
{
|
|
yyjson_mut_arr_append(yyjson_mut_arr_get(get(get(e, "geometry"), "coordinates"), 0), yyjson_mut_arr_with_real(d, point, 2));
|
|
}
|
|
}
|
|
else if (name == "jeopardy")
|
|
{
|
|
yyjson_mut_arr_foreach(r, idx, max, e)
|
|
{
|
|
yyjson_mut_set_null(get(e, "value"));
|
|
yyjson_mut_obj_remove_key(e, "air_date");
|
|
yyjson_mut_obj_add_bool(d, e, "seen", true);
|
|
}
|
|
}
|
|
std::size_t n = 0;
|
|
char* o = yyjson_mut_write(d, 0, &n);
|
|
const bool ok = o != nullptr && emit(res, o, n);
|
|
std::free(o);
|
|
yyjson_mut_doc_free(d);
|
|
return ok;
|
|
}
|
|
|
|
static bool edit_rapidjson(const std::string& name, rapidjson::Document& d, result& res)
|
|
{
|
|
auto& a = d.GetAllocator();
|
|
auto status = [&](rapidjson::Value & st)
|
|
{
|
|
auto& rc = st["retweet_count"];
|
|
rc.SetInt64(rc.GetInt64() + 1);
|
|
st.RemoveMember("entities");
|
|
st.AddMember("seen", true, a);
|
|
};
|
|
if (name == "twitter")
|
|
{
|
|
for (auto& st : d["statuses"].GetArray())
|
|
{
|
|
status(st);
|
|
}
|
|
}
|
|
else if (name == "status")
|
|
{
|
|
status(d);
|
|
}
|
|
else if (name == "citm_catalog")
|
|
{
|
|
for (auto& p : d["performances"].GetArray())
|
|
{
|
|
auto& start = p["start"];
|
|
start.SetInt64(start.GetInt64() + 1);
|
|
p.RemoveMember("seatMapImage");
|
|
p.AddMember("seen", true, a);
|
|
}
|
|
}
|
|
else if (name == "canada")
|
|
{
|
|
d["features"][0u]["properties"]["name"].SetString("Kanada");
|
|
for (auto& f : d["features"].GetArray())
|
|
{
|
|
rapidjson::Value point(rapidjson::kArrayType);
|
|
point.PushBack(0.5, a).PushBack(0.25, a);
|
|
f["geometry"]["coordinates"][0u].PushBack(point, a);
|
|
}
|
|
}
|
|
else if (name == "jeopardy")
|
|
{
|
|
for (auto& q : d.GetArray())
|
|
{
|
|
q["value"].SetNull();
|
|
q.RemoveMember("air_date");
|
|
q.AddMember("seen", true, a);
|
|
}
|
|
}
|
|
rapidjson::StringBuffer sb;
|
|
rapidjson::Writer<rapidjson::StringBuffer> w(sb);
|
|
return d.Accept(w) && emit(res, sb.GetString(), sb.GetSize());
|
|
}
|
|
|
|
static bool edit_sonic(const std::string& name, const std::string& s, result& res)
|
|
{
|
|
sonic_json::Document d;
|
|
d.Parse(s.data(), s.size());
|
|
if (d.HasParseError())
|
|
{
|
|
return false;
|
|
}
|
|
auto& a = d.GetAllocator();
|
|
auto status = [&](sonic_json::Node & st)
|
|
{
|
|
auto& rc = st["retweet_count"];
|
|
rc.SetInt64(rc.GetInt64() + 1);
|
|
st.RemoveMember("entities");
|
|
st.AddMember("seen", sonic_json::Node(true), a);
|
|
};
|
|
if (name == "twitter")
|
|
{
|
|
auto& sts = d["statuses"];
|
|
for (auto it = sts.Begin(); it != sts.End(); ++it)
|
|
{
|
|
status(*it);
|
|
}
|
|
}
|
|
else if (name == "status")
|
|
{
|
|
status(d);
|
|
}
|
|
else if (name == "citm_catalog")
|
|
{
|
|
auto& ps = d["performances"];
|
|
for (auto it = ps.Begin(); it != ps.End(); ++it)
|
|
{
|
|
auto& start = (*it)["start"];
|
|
start.SetInt64(start.GetInt64() + 1);
|
|
it->RemoveMember("seatMapImage");
|
|
it->AddMember("seen", sonic_json::Node(true), a);
|
|
}
|
|
}
|
|
else if (name == "canada")
|
|
{
|
|
auto& fs = d["features"];
|
|
fs[std::size_t{0}]["properties"]["name"].SetString("Kanada", 6, a);
|
|
for (auto it = fs.Begin(); it != fs.End(); ++it)
|
|
{
|
|
sonic_json::Node point(sonic_json::kArray);
|
|
point.PushBack(sonic_json::Node(0.5), a);
|
|
point.PushBack(sonic_json::Node(0.25), a);
|
|
(*it)["geometry"]["coordinates"][std::size_t{0}].PushBack(std::move(point), a);
|
|
}
|
|
}
|
|
else if (name == "jeopardy")
|
|
{
|
|
for (auto it = d.Begin(); it != d.End(); ++it)
|
|
{
|
|
(*it)["value"].SetNull();
|
|
it->RemoveMember("air_date");
|
|
it->AddMember("seen", sonic_json::Node(true), a);
|
|
}
|
|
}
|
|
sonic_json::WriteBuffer wb;
|
|
return d.Serialize(wb) == sonic_json::kErrorNone && emit(res, wb.ToString(), wb.Size());
|
|
}
|
|
|
|
#if !BENCH_NO_GLAZE
|
|
template<class G>
|
|
static void glaze_inc(G& v)
|
|
{
|
|
if constexpr (std::is_same_v<G, glz::generic>)
|
|
{
|
|
++std::get<double>(v.data);
|
|
}
|
|
else if (auto* u = std::get_if<std::uint64_t>(&v.data))
|
|
{
|
|
++*u;
|
|
}
|
|
else
|
|
{
|
|
++std::get<std::int64_t>(v.data);
|
|
}
|
|
}
|
|
|
|
template<class G = glz::generic_u64, bool strict = false>
|
|
static bool edit_glaze(const std::string& name, const std::string& s, result& res)
|
|
{
|
|
G j;
|
|
if (strict ? bool(glz::read<glz_strict>(j, s)) : bool(glz::read_json(j, s)))
|
|
{
|
|
return false;
|
|
}
|
|
auto status = [](G & st)
|
|
{
|
|
glaze_inc(st["retweet_count"]);
|
|
st.get_object().erase("entities");
|
|
st["seen"].data = true;
|
|
};
|
|
if (name == "twitter")
|
|
{
|
|
for (auto& st : j["statuses"].get_array())
|
|
{
|
|
status(st);
|
|
}
|
|
}
|
|
else if (name == "status")
|
|
{
|
|
status(j);
|
|
}
|
|
else if (name == "citm_catalog")
|
|
{
|
|
for (auto& p : j["performances"].get_array())
|
|
{
|
|
glaze_inc(p["start"]);
|
|
p.get_object().erase("seatMapImage");
|
|
p["seen"].data = true;
|
|
}
|
|
}
|
|
else if (name == "canada")
|
|
{
|
|
j["features"][0]["properties"]["name"].data = std::string("Kanada");
|
|
for (auto& f : j["features"].get_array())
|
|
{
|
|
G point;
|
|
point.data = typename G::array_t(2);
|
|
point.get_array()[0].data = 0.5;
|
|
point.get_array()[1].data = 0.25;
|
|
f["geometry"]["coordinates"][0].get_array().push_back(std::move(point));
|
|
}
|
|
}
|
|
else if (name == "jeopardy")
|
|
{
|
|
for (auto& q : j.get_array())
|
|
{
|
|
q["value"].data = nullptr;
|
|
q.get_object().erase("air_date");
|
|
q["seen"].data = true;
|
|
}
|
|
}
|
|
std::string o;
|
|
return !(strict ? bool(glz::write<glz_strict>(j, o)) : bool(glz::write_json(j, o))) && emit(res, std::move(o));
|
|
}
|
|
#endif
|
|
|
|
static bool edit_boost(const std::string& name, const std::string& s, result& res)
|
|
{
|
|
boost::json::monotonic_resource mr;
|
|
boost::system::error_code ec;
|
|
boost::json::value v = boost::json::parse(s, ec, &mr, bj_precise);
|
|
if (ec)
|
|
{
|
|
return false;
|
|
}
|
|
auto status = [](boost::json::object & o)
|
|
{
|
|
boost::json::value& rc = o["retweet_count"];
|
|
rc = rc.to_number<std::int64_t>() + 1;
|
|
o.erase("entities");
|
|
o["seen"] = true;
|
|
};
|
|
if (name == "twitter")
|
|
{
|
|
for (auto& st : v.as_object()["statuses"].as_array())
|
|
{
|
|
status(st.as_object());
|
|
}
|
|
}
|
|
else if (name == "status")
|
|
{
|
|
status(v.as_object());
|
|
}
|
|
else if (name == "citm_catalog")
|
|
{
|
|
for (auto& p : v.as_object()["performances"].as_array())
|
|
{
|
|
auto& o = p.as_object();
|
|
boost::json::value& start = o["start"];
|
|
start = start.to_number<std::int64_t>() + 1;
|
|
o.erase("seatMapImage");
|
|
o["seen"] = true;
|
|
}
|
|
}
|
|
else if (name == "canada")
|
|
{
|
|
auto& fs = v.as_object()["features"].as_array();
|
|
fs.at(0).as_object()["properties"].as_object()["name"] = "Kanada";
|
|
for (auto& f : fs)
|
|
{
|
|
auto& ring = f.as_object()["geometry"].as_object()["coordinates"].as_array().at(0).as_array();
|
|
ring.push_back(boost::json::array({0.5, 0.25}, ring.storage()));
|
|
}
|
|
}
|
|
else if (name == "jeopardy")
|
|
{
|
|
for (auto& q : v.as_array())
|
|
{
|
|
auto& o = q.as_object();
|
|
o["value"] = nullptr;
|
|
o.erase("air_date");
|
|
o["seen"] = true;
|
|
}
|
|
}
|
|
return emit(res, boost::json::serialize(v));
|
|
}
|
|
|
|
///////////////////////////////////////////////////////////////////////////////
|
|
// variants
|
|
///////////////////////////////////////////////////////////////////////////////
|
|
|
|
struct variant
|
|
{
|
|
std::string name; // "<library>: <variant>"
|
|
std::function<bool(const std::string&, result&)> fn;
|
|
bool any_order = false; // sum: visits objects in key order, so its sum may differ in the last bits
|
|
bool untimed_copy = false; // takes its input copy from g_copies, made before the timer starts
|
|
};
|
|
|
|
struct context
|
|
{
|
|
std::string name; // the document whose select and edit apply (twitter for twitter_min)
|
|
json_document& reused;
|
|
simdjson::dom::parser& sj;
|
|
simdjson::ondemand::parser& od;
|
|
yy_alc& yy;
|
|
bj_reuse& bj;
|
|
rj_reuse& rj;
|
|
};
|
|
|
|
constexpr yyjson_read_flag yy_raw = YYJSON_READ_NUMBER_AS_RAW;
|
|
|
|
static std::vector<variant> validate_variants(context& c)
|
|
{
|
|
return
|
|
{
|
|
{"nlohmann json_document: accept", [](const std::string & s, result&) { return json_document::accept(s); }},
|
|
{"nlohmann json_document: parse", [](const std::string & s, result & r) { auto d = json_document::parse(s); r.acc = static_cast<double>(d.node_count()); return true; }},
|
|
{"nlohmann json_document: read (reused)", [&c](const std::string & s, result & r) { c.reused.read(s); r.acc = static_cast<double>(c.reused.node_count()); return true; }},
|
|
{"nlohmann json: accept", [](const std::string & s, result&) { return json::accept(s); }},
|
|
{"nlohmann json: parse", [](const std::string & s, result & r) { json j = json::parse(s); r.acc = static_cast<double>(j.size()); return true; }},
|
|
{"yyjson: read", [](const std::string & s, result&) { yyjson_doc* d = yyjson_read(s.data(), s.size(), 0); const bool ok = d != nullptr; yyjson_doc_free(d); return ok; }},
|
|
{"yyjson: insitu (copy)", [](const std::string & s, result&) { yy_insitu d(s); return d.doc != nullptr; }},
|
|
{"yyjson: insitu (copy untimed)", [](const std::string & s, result&) { yy_insitu_untimed d(s); return d.doc != nullptr; }, false, true},
|
|
{"yyjson: read (raw numbers)", [](const std::string & s, result&) { yyjson_doc* d = yyjson_read(s.data(), s.size(), yy_raw); const bool ok = d != nullptr; yyjson_doc_free(d); return ok; }},
|
|
{"yyjson: read (reused allocator)", [&c](const std::string & s, result&) { yyjson_doc* d = yyjson_read_opts(const_cast<char*>(s.data()), s.size(), 0, c.yy.a, nullptr); const bool ok = d != nullptr; yyjson_doc_free(d); return ok; }},
|
|
{"simdjson: DOM", [](const std::string & s, result&) { simdjson::dom::parser p; return !p.parse(s).error(); }},
|
|
{"simdjson: DOM (reused)", [&c](const std::string & s, result&) { return !c.sj.parse(s).error(); }},
|
|
{"sonic-cpp: Document", [](const std::string & s, result&) { sonic_json::Document d; d.Parse(s.data(), s.size()); return !d.HasParseError(); }},
|
|
#if !BENCH_NO_GLAZE
|
|
{"Glaze: validate_json", [](const std::string & s, result&) { return !glz::validate_json(s); }},
|
|
{"Glaze: generic", [](const std::string & s, result&) { glz::generic_u64 j; return !glz::read_json(j, s); }},
|
|
{"Glaze: generic (double)", [](const std::string & s, result&) { glz::generic j; return !glz::read_json(j, s); }},
|
|
{"Glaze: generic (strict)", [](const std::string & s, result&) { glz::generic_u64 j; return !glz::read<glz_strict>(j, s); }},
|
|
{"Glaze: generic (double, strict)", [](const std::string & s, result&) { glz::generic j; return !glz::read<glz_strict>(j, s); }},
|
|
#endif
|
|
{"Boost.JSON: SAX", [](const std::string & s, result&) { return bj_sax<bj_null>(s); }},
|
|
{"Boost.JSON: parse", [](const std::string & s, result&) { boost::json::monotonic_resource mr; boost::system::error_code ec; auto v = boost::json::parse(s, ec, &mr, bj_precise); return !ec; }},
|
|
{"Boost.JSON: SAX (numbers: none)", [](const std::string & s, result&) { return bj_sax_none(s); }},
|
|
{"Boost.JSON: parse (numbers: none)", [](const std::string & s, result&) { boost::json::monotonic_resource mr; boost::system::error_code ec; auto v = boost::json::parse(s, ec, &mr, bj_none); return !ec; }},
|
|
{"Boost.JSON: SAX (reused)", [&c](const std::string & s, result&) { return bj_reuse::sax(c.bj.sax_null, s); }},
|
|
{"Boost.JSON: parser (reused)", [&c](const std::string & s, result&) { return c.bj.parse(s, [](const boost::json::value&) { return true; }); }},
|
|
};
|
|
}
|
|
|
|
static std::vector<variant> sum_variants(context& c)
|
|
{
|
|
return
|
|
{
|
|
{"nlohmann json_document: parse", [](const std::string & s, result & r) { auto d = json_document::parse(s); walk(d.root(), r.st); return true; }},
|
|
{"nlohmann json_document: parse_copy", [](const std::string & s, result & r) { auto d = json_document::parse_copy(s); walk(d.root(), r.st); return true; }},
|
|
{"nlohmann json_document: read (reused)", [&c](const std::string & s, result & r) { c.reused.read(s); walk(c.reused.root(), r.st); return true; }},
|
|
{"nlohmann json: parse", [](const std::string & s, result & r) { json j = json::parse(s); walk(j, r.st); return true; }, true},
|
|
{"nlohmann json: sax", [](const std::string & s, result & r) { json_sum_sax h{&r.st}; return json::sax_parse(s, &h); }},
|
|
{"yyjson: read", [](const std::string & s, result & r) { yyjson_doc* d = yyjson_read(s.data(), s.size(), 0); if (d == nullptr) { return false; } walk(yyjson_doc_get_root(d), r.st); yyjson_doc_free(d); return true; }},
|
|
{"yyjson: insitu (copy)", [](const std::string & s, result & r) { yy_insitu d(s); if (d.doc == nullptr) { return false; } walk(yyjson_doc_get_root(d.doc), r.st); return true; }},
|
|
{"yyjson: insitu (copy untimed)", [](const std::string & s, result & r) { yy_insitu_untimed d(s); if (d.doc == nullptr) { return false; } walk(yyjson_doc_get_root(d.doc), r.st); return true; }, false, true},
|
|
{"yyjson: read (reused allocator)", [&c](const std::string & s, result & r) { yyjson_doc* d = yyjson_read_opts(const_cast<char*>(s.data()), s.size(), 0, c.yy.a, nullptr); if (d == nullptr) { return false; } walk(yyjson_doc_get_root(d), r.st); yyjson_doc_free(d); return true; }},
|
|
{"simdjson: DOM", [](const std::string & s, result & r) { simdjson::dom::parser p; auto e = p.parse(s); if (e.error()) { return false; } walk(e.value_unsafe(), r.st); return true; }},
|
|
{"simdjson: DOM (reused)", [&c](const std::string & s, result & r) { auto e = c.sj.parse(s); if (e.error()) { return false; } walk(e.value_unsafe(), r.st); return true; }},
|
|
{"simdjson: On-Demand", [](const std::string & s, result & r) { simdjson::ondemand::parser p; auto d = p.iterate(s).value(); walk_od(d.get_value().value(), r.st); return d.at_end(); }},
|
|
{"simdjson: On-Demand (reused)", [&c](const std::string & s, result & r) { auto d = c.od.iterate(s).value(); walk_od(d.get_value().value(), r.st); return d.at_end(); }},
|
|
{"sonic-cpp: Document", [](const std::string & s, result & r) { sonic_json::Document d; d.Parse(s.data(), s.size()); if (d.HasParseError()) { return false; } walk_sonic(d, r.st); return true; }},
|
|
#if !BENCH_NO_GLAZE
|
|
{"Glaze: generic", [](const std::string & s, result & r) { glz::generic_u64 j; if (glz::read_json(j, s)) { return false; } walk(j, r.st); return true; }},
|
|
{"Glaze: lazy_json", [](const std::string & s, result & r) { auto l = glz::lazy_json(s); if (!l) { return false; } walk_lazy(l->root(), r.st); return true; }},
|
|
{"Glaze: generic (double)", [](const std::string & s, result & r) { glz::generic j; if (glz::read_json(j, s)) { return false; } walk(j, r.st); return true; }},
|
|
{"Glaze: generic (strict)", [](const std::string & s, result & r) { glz::generic_u64 j; if (glz::read<glz_strict>(j, s)) { return false; } walk(j, r.st); return true; }},
|
|
{"Glaze: generic (double, strict)", [](const std::string & s, result & r) { glz::generic j; if (glz::read<glz_strict>(j, s)) { return false; } walk(j, r.st); return true; }},
|
|
{"Glaze: lazy_json (string_view)", [](const std::string & s, result & r) { auto l = glz::lazy_json(s); if (!l) { return false; } walk_lazy<std::string_view>(l->root(), r.st); return true; }},
|
|
#endif
|
|
{"Boost.JSON: parse", [](const std::string & s, result & r) { boost::json::monotonic_resource mr; boost::system::error_code ec; auto v = boost::json::parse(s, ec, &mr, bj_precise); if (ec) { return false; } walk(v, r.st); return true; }},
|
|
{"Boost.JSON: SAX", [](const std::string & s, result & r) { return bj_sax<bj_sum>(s, &r.st); }},
|
|
{"Boost.JSON: SAX (reused)", [&c](const std::string & s, result & r) { c.bj.sax_sum.handler().st = &r.st; return bj_reuse::sax(c.bj.sax_sum, s); }},
|
|
{"Boost.JSON: parser (reused)", [&c](const std::string & s, result & r) { return c.bj.parse(s, [&r](const boost::json::value & v) { walk(v, r.st); return true; }); }},
|
|
};
|
|
}
|
|
|
|
static std::vector<variant> select_variants(context& c)
|
|
{
|
|
const std::string n = c.name;
|
|
return
|
|
{
|
|
{"nlohmann json_document: parse", [n](const std::string & s, result & r) { auto d = json_document::parse(s); r.acc = pick<acc_view>(n, d.root()); return true; }},
|
|
{"nlohmann json_document: parse_copy", [n](const std::string & s, result & r) { auto d = json_document::parse_copy(s); r.acc = pick<acc_view>(n, d.root()); return true; }},
|
|
{"nlohmann json_document: read (reused)", [&c, n](const std::string & s, result & r) { c.reused.read(s); r.acc = pick<acc_view>(n, c.reused.root()); return true; }},
|
|
{"nlohmann json: parse", [n](const std::string & s, result & r) { json j = json::parse(s); r.acc = pick<acc_json>(n, &j); return true; }},
|
|
{"nlohmann json_document: structs (from_json)", [n](const std::string & s, result & r) { auto d = json_document::parse(s); r.acc = pick_nlohmann_structs(n, d.root()); return true; }},
|
|
{"nlohmann json: structs (from_json)", [n](const std::string & s, result & r) { r.acc = pick_nlohmann_structs(n, json::parse(s)); return true; }},
|
|
{"yyjson: read", [n](const std::string & s, result & r) { yyjson_doc* d = yyjson_read(s.data(), s.size(), 0); if (d == nullptr) { return false; } r.acc = pick<acc_yyjson>(n, yyjson_doc_get_root(d)); yyjson_doc_free(d); return true; }},
|
|
{"yyjson: insitu (copy)", [n](const std::string & s, result & r) { yy_insitu d(s); if (d.doc == nullptr) { return false; } r.acc = pick<acc_yyjson>(n, yyjson_doc_get_root(d.doc)); return true; }},
|
|
{"yyjson: insitu (copy untimed)", [n](const std::string & s, result & r) { yy_insitu_untimed d(s); if (d.doc == nullptr) { return false; } r.acc = pick<acc_yyjson>(n, yyjson_doc_get_root(d.doc)); return true; }, false, true},
|
|
{"yyjson: read (raw numbers)", [n](const std::string & s, result & r) { yyjson_doc* d = yyjson_read(s.data(), s.size(), yy_raw); if (d == nullptr) { return false; } r.acc = pick<acc_yyjson_raw>(n, yyjson_doc_get_root(d)); yyjson_doc_free(d); return true; }},
|
|
{"yyjson: read (reused allocator)", [&c, n](const std::string & s, result & r) { yyjson_doc* d = yyjson_read_opts(const_cast<char*>(s.data()), s.size(), 0, c.yy.a, nullptr); if (d == nullptr) { return false; } r.acc = pick<acc_yyjson>(n, yyjson_doc_get_root(d)); yyjson_doc_free(d); return true; }},
|
|
{"simdjson: DOM", [n](const std::string & s, result & r) { simdjson::dom::parser p; auto e = p.parse(s); if (e.error()) { return false; } r.acc = pick<acc_simdjson>(n, e.value_unsafe()); return true; }},
|
|
{"simdjson: DOM (reused)", [&c, n](const std::string & s, result & r) { auto e = c.sj.parse(s); if (e.error()) { return false; } r.acc = pick<acc_simdjson>(n, e.value_unsafe()); return true; }},
|
|
{"simdjson: On-Demand", [n](const std::string & s, result & r) { simdjson::ondemand::parser p; auto d = p.iterate(s).value(); r.acc = pick_od(n, d); return true; }},
|
|
{"simdjson: On-Demand (reused)", [&c, n](const std::string & s, result & r) { auto d = c.od.iterate(s).value(); r.acc = pick_od(n, d); return true; }},
|
|
{"simdjson: On-Demand structs", [n](const std::string & s, result & r) { simdjson::ondemand::parser p; auto d = p.iterate(s).value(); r.acc = pick_simdjson_structs(n, d); return true; }},
|
|
{"simdjson: On-Demand (key selectors)", [n](const std::string & s, result & r) { simdjson::ondemand::parser p; auto d = p.iterate(s).value(); return !pick_od_keys(n, d, r.acc); }},
|
|
{"sonic-cpp: Document", [n](const std::string & s, result & r) { sonic_json::Document d; d.Parse(s.data(), s.size()); if (d.HasParseError()) { return false; } r.acc = pick<acc_sonic>(n, &d); return true; }},
|
|
#if !BENCH_NO_GLAZE
|
|
{"Glaze: structs", [n](const std::string & s, result & r) { return pick_glaze_structs<sel_opts_fast>(n, s, r.acc); }},
|
|
{"Glaze: structs (validate_skipped)", [n](const std::string & s, result & r) { return pick_glaze_structs<sel_opts_full>(n, s, r.acc); }},
|
|
{"Glaze: generic", [n](const std::string & s, result & r) { glz::generic_u64 j; if (glz::read_json(j, s)) { return false; } r.acc = pick<acc_glaze>(n, &j); return true; }},
|
|
{"Glaze: lazy_json", [n](const std::string & s, result & r) { auto l = glz::lazy_json(s); if (!l) { return false; } r.acc = pick_glaze_lazy(n, *l); return true; }},
|
|
{"Glaze: generic (double)", [n](const std::string & s, result & r) { glz::generic j; if (glz::read_json(j, s)) { return false; } r.acc = pick<acc_glaze_t<glz::generic>>(n, &j); return true; }},
|
|
{"Glaze: generic (strict)", [n](const std::string & s, result & r) { glz::generic_u64 j; if (glz::read<glz_strict>(j, s)) { return false; } r.acc = pick<acc_glaze>(n, &j); return true; }},
|
|
{"Glaze: generic (double, strict)", [n](const std::string & s, result & r) { glz::generic j; if (glz::read<glz_strict>(j, s)) { return false; } r.acc = pick<acc_glaze_t<glz::generic>>(n, &j); return true; }},
|
|
{"Glaze: lazy_json (string_view)", [n](const std::string & s, result & r) { auto l = glz::lazy_json(s); if (!l) { return false; } r.acc = pick_glaze_lazy<std::string_view>(n, *l); return true; }},
|
|
#endif
|
|
{"Boost.JSON: parse", [n](const std::string & s, result & r) { boost::json::monotonic_resource mr; boost::system::error_code ec; auto v = boost::json::parse(s, ec, &mr, bj_precise); if (ec) { return false; } r.acc = pick<acc_boost>(n, &v); return true; }},
|
|
{"Boost.JSON: parse (numbers: none)", [n](const std::string & s, result & r) { boost::json::monotonic_resource mr; boost::system::error_code ec; auto v = boost::json::parse(s, ec, &mr, bj_none); if (ec) { return false; } r.acc = pick<acc_boost>(n, &v); return true; }},
|
|
{"Boost.JSON: parser (reused)", [&c, n](const std::string & s, result & r) { return c.bj.parse(s, [&](const boost::json::value & v) { r.acc = pick<acc_boost>(n, &v); return true; }); }},
|
|
{"Boost.JSON: parse_into structs", [n](const std::string & s, result & r) { return pick_boost_into(n, s, r.acc); }},
|
|
};
|
|
}
|
|
|
|
static std::vector<variant> edit_variants(context& c)
|
|
{
|
|
const std::string n = c.name;
|
|
return
|
|
{
|
|
{"nlohmann json_document: editable", [n](const std::string & s, result & r) { return edit_document(n, s, r, false); }},
|
|
{"nlohmann json_document: editable (source numbers)", [n](const std::string & s, result & r) { return edit_document(n, s, r, true); }},
|
|
{"nlohmann json: parse", [n](const std::string & s, result & r) { json j = json::parse(s); edit(n, j); return emit(r, j.dump()); }},
|
|
{"nlohmann json: ordered_json", [n](const std::string & s, result & r) { ordered_json j = ordered_json::parse(s); edit(n, j); return emit(r, j.dump()); }},
|
|
{"yyjson: mut copy", [n](const std::string & s, result & r) { yyjson_doc* d = yyjson_read(s.data(), s.size(), 0); const bool ok = edit_yyjson(n, d, r); yyjson_doc_free(d); return ok; }},
|
|
{"yyjson: insitu (copy) + mut copy", [n](const std::string & s, result & r) { yy_insitu d(s); return edit_yyjson(n, d.doc, r); }},
|
|
{"yyjson: insitu (copy untimed) + mut copy", [n](const std::string & s, result & r) { yy_insitu_untimed d(s); return edit_yyjson(n, d.doc, r); }, false, true},
|
|
{"sonic-cpp: Document", [n](const std::string & s, result & r) { return edit_sonic(n, s, r); }},
|
|
#if !BENCH_NO_GLAZE
|
|
{"Glaze: generic", [n](const std::string & s, result & r) { return edit_glaze(n, s, r); }},
|
|
{"Glaze: generic (double)", [n](const std::string & s, result & r) { return edit_glaze<glz::generic>(n, s, r); }},
|
|
{"Glaze: generic (strict)", [n](const std::string & s, result & r) { return edit_glaze<glz::generic_u64, true>(n, s, r); }},
|
|
{"Glaze: generic (double, strict)", [n](const std::string & s, result & r) { return edit_glaze<glz::generic, true>(n, s, r); }},
|
|
#endif
|
|
{"Boost.JSON: parse", [n](const std::string & s, result & r) { return edit_boost(n, s, r); }},
|
|
};
|
|
}
|
|
|
|
static std::vector<variant> minify_variants(context& c)
|
|
{
|
|
return
|
|
{
|
|
{"nlohmann json_document: parse + dump", [](const std::string & s, result & r) { auto d = json_document::parse(s); return emit(r, d.root().dump()); }},
|
|
{"nlohmann json_document: parse_copy + dump", [](const std::string & s, result & r) { auto d = json_document::parse_copy(s); return emit(r, d.root().dump()); }},
|
|
{"nlohmann json_document: parse + dump (source numbers)", [](const std::string & s, result & r) { auto d = json_document::parse(s); return emit(r, d.root().dump(-1, ' ', false, json_view::number_format::source)); }},
|
|
{"nlohmann json_document: read + dump (reused)", [&c](const std::string & s, result & r) { c.reused.read(s); return emit(r, c.reused.root().dump()); }},
|
|
{"nlohmann json: parse + dump", [](const std::string & s, result & r) { json j = json::parse(s); return emit(r, j.dump()); }},
|
|
{"yyjson: read + write", [](const std::string & s, result & r) { yyjson_doc* d = yyjson_read(s.data(), s.size(), 0); if (d == nullptr) { return false; } std::size_t n = 0; char* o = yyjson_write(d, 0, &n); const bool ok = o != nullptr && emit(r, o, n); std::free(o); yyjson_doc_free(d); return ok; }},
|
|
{"yyjson: insitu (copy) + write", [](const std::string & s, result & r) { yy_insitu d(s); if (d.doc == nullptr) { return false; } std::size_t n = 0; char* o = yyjson_write(d.doc, 0, &n); const bool ok = o != nullptr && emit(r, o, n); std::free(o); return ok; }},
|
|
{"yyjson: insitu (copy untimed) + write", [](const std::string & s, result & r) { yy_insitu_untimed d(s); if (d.doc == nullptr) { return false; } std::size_t n = 0; char* o = yyjson_write(d.doc, 0, &n); const bool ok = o != nullptr && emit(r, o, n); std::free(o); return ok; }, false, true},
|
|
// the raw number text is written verbatim
|
|
{"yyjson: read (raw numbers) + write", [](const std::string & s, result & r) { yyjson_doc* d = yyjson_read(s.data(), s.size(), yy_raw); if (d == nullptr) { return false; } std::size_t n = 0; char* o = yyjson_write(d, 0, &n); const bool ok = o != nullptr && emit(r, o, n); std::free(o); yyjson_doc_free(d); return ok; }},
|
|
{"yyjson: read + write (reused allocator)", [&c](const std::string & s, result & r) { yyjson_alc* a = c.yy.a; yyjson_doc* d = yyjson_read_opts(const_cast<char*>(s.data()), s.size(), 0, a, nullptr); if (d == nullptr) { return false; } std::size_t n = 0; char* o = yyjson_write_opts(d, 0, a, &n, nullptr); const bool ok = o != nullptr && emit(r, o, n); if (o != nullptr) { a->free(a->ctx, o); } yyjson_doc_free(d); return ok; }},
|
|
{"simdjson: DOM + to_string", [](const std::string & s, result & r) { simdjson::dom::parser p; auto e = p.parse(s); if (e.error()) { return false; } return emit(r, simdjson::to_string(e.value_unsafe())); }},
|
|
{"simdjson: DOM (reused) + to_string", [&c](const std::string & s, result & r) { auto e = c.sj.parse(s); if (e.error()) { return false; } return emit(r, simdjson::to_string(e.value_unsafe())); }},
|
|
{"simdjson: minify", [](const std::string & s, result & r) { std::unique_ptr<char[]> o(new char[s.size() + simdjson::SIMDJSON_PADDING]); std::size_t n = 0; if (simdjson::minify(s.data(), s.size(), o.get(), n)) { return false; } return emit(r, o.get(), n); }},
|
|
{"sonic-cpp: Document + Serialize", [](const std::string & s, result & r) { sonic_json::Document d; d.Parse(s.data(), s.size()); if (d.HasParseError()) { return false; } sonic_json::WriteBuffer wb; return d.Serialize(wb) == sonic_json::kErrorNone && emit(r, wb.ToString(), wb.Size()); }},
|
|
#if !BENCH_NO_GLAZE
|
|
{"Glaze: generic", [](const std::string & s, result & r) { glz::generic_u64 j; if (glz::read_json(j, s)) { return false; } std::string o; return !glz::write_json(j, o) && emit(r, std::move(o)); }},
|
|
{"Glaze: generic (double)", [](const std::string & s, result & r) { glz::generic j; if (glz::read_json(j, s)) { return false; } std::string o; return !glz::write_json(j, o) && emit(r, std::move(o)); }},
|
|
{"Glaze: generic (strict)", [](const std::string & s, result & r) { glz::generic_u64 j; if (glz::read<glz_strict>(j, s)) { return false; } std::string o; return !glz::write<glz_strict>(j, o) && emit(r, std::move(o)); }},
|
|
{"Glaze: generic (double, strict)", [](const std::string & s, result & r) { glz::generic j; if (glz::read<glz_strict>(j, s)) { return false; } std::string o; return !glz::write<glz_strict>(j, o) && emit(r, std::move(o)); }},
|
|
// minify_json takes a resizable input but only reads it
|
|
{"Glaze: minify_json", [](const std::string & s, result & r) { std::string o; return !glz::minify_json(const_cast<std::string&>(s), o) && emit(r, std::move(o)); }},
|
|
#endif
|
|
{"Boost.JSON: parse + serialize", [](const std::string & s, result & r) { boost::json::monotonic_resource mr; boost::system::error_code ec; auto v = boost::json::parse(s, ec, &mr, bj_precise); if (ec) { return false; } return emit(r, boost::json::serialize(v)); }},
|
|
{"Boost.JSON: parser (reused) + serialize", [&c](const std::string & s, result & r) { return c.bj.parse(s, [&r](const boost::json::value & v) { return emit(r, boost::json::serialize(v)); }); }},
|
|
};
|
|
}
|
|
|
|
// RapidJSON, once with UTF-8 validation (kParseValidateEncodingFlag) and once without; both with
|
|
// kParseFullPrecisionFlag, without which floats are not correctly rounded and sum/edit/minify give wrong results
|
|
constexpr unsigned rj_fast = rapidjson::kParseFullPrecisionFlag;
|
|
|
|
// (raw numbers): kParseNumbersAsStringsFlag, numbers are validated and kept as text, not converted
|
|
constexpr unsigned rj_raw = rapidjson::kParseNumbersAsStringsFlag;
|
|
|
|
template<unsigned F>
|
|
static void add_rapidjson(std::vector<variant>& vs, const std::string& uc, const std::string& n, const char* sfx, rj_reuse& rr)
|
|
{
|
|
rj_reuse* const rp = &rr;
|
|
if (uc == "validate")
|
|
{
|
|
// validate reads no number, so the default (not correctly rounded) precision is allowed here
|
|
vs.push_back({std::string("RapidJSON: SAX") + sfx, [](const std::string & s, result&) { rapidjson::BaseReaderHandler<> h; return rj_read<F>(s, h); }});
|
|
vs.push_back({std::string("RapidJSON: Document") + sfx, [](const std::string & s, result&) { rapidjson::Document d; d.Parse<F>(s.data(), s.size()); return !d.HasParseError(); }});
|
|
vs.push_back({std::string("RapidJSON: insitu (copy)") + sfx, [](const std::string & s, result&) { std::string b = s; rapidjson::Document d; d.ParseInsitu<F>(&b[0]); return !d.HasParseError(); }});
|
|
vs.push_back({std::string("RapidJSON: insitu (copy untimed)") + sfx, [](const std::string & s, result&) { taken_copy b(s); rapidjson::Document d; d.ParseInsitu<F>(b.buf); return !d.HasParseError(); }, false, true});
|
|
vs.push_back({std::string("RapidJSON: SAX (string stream)") + sfx, [](const std::string & s, result&) { rapidjson::BaseReaderHandler<> h; return rj_read_cstr<F>(s, h); }});
|
|
vs.push_back({std::string("RapidJSON: SAX (default precision)") + sfx, [](const std::string & s, result&) { rapidjson::BaseReaderHandler<> h; return rj_read<F & ~rapidjson::kParseFullPrecisionFlag>(s, h); }});
|
|
vs.push_back({std::string("RapidJSON: SAX (string stream, default precision)") + sfx, [](const std::string & s, result&) { rapidjson::BaseReaderHandler<> h; return rj_read_cstr<F & ~rapidjson::kParseFullPrecisionFlag>(s, h); }});
|
|
vs.push_back({std::string("RapidJSON: SAX (raw numbers)") + sfx, [](const std::string & s, result&) { rapidjson::BaseReaderHandler<> h; return rj_read<F | rj_raw>(s, h); }});
|
|
vs.push_back({std::string("RapidJSON: Document (reused allocator)") + sfx, [rp](const std::string & s, result&) { return rp->parse<F>(s, [](const rapidjson::Value&) { return true; }); }});
|
|
}
|
|
else if (uc == "sum")
|
|
{
|
|
vs.push_back({std::string("RapidJSON: Document") + sfx, [](const std::string & s, result & r) { rapidjson::Document d; d.Parse<F>(s.data(), s.size()); if (d.HasParseError()) { return false; } walk(d, r.st); return true; }});
|
|
vs.push_back({std::string("RapidJSON: SAX") + sfx, [](const std::string & s, result & r) { rj_sum h(&r.st); return rj_read<F>(s, h); }});
|
|
vs.push_back({std::string("RapidJSON: insitu (copy)") + sfx, [](const std::string & s, result & r) { std::string b = s; rapidjson::Document d; d.ParseInsitu<F>(&b[0]); if (d.HasParseError()) { return false; } walk(d, r.st); return true; }});
|
|
vs.push_back({std::string("RapidJSON: insitu (copy untimed)") + sfx, [](const std::string & s, result & r) { taken_copy b(s); rapidjson::Document d; d.ParseInsitu<F>(b.buf); if (d.HasParseError()) { return false; } walk(d, r.st); return true; }, false, true});
|
|
vs.push_back({std::string("RapidJSON: SAX (string stream)") + sfx, [](const std::string & s, result & r) { rj_sum h(&r.st); return rj_read_cstr<F>(s, h); }});
|
|
vs.push_back({std::string("RapidJSON: Document (reused allocator)") + sfx, [rp](const std::string & s, result & r) { return rp->parse<F>(s, [&r](const rapidjson::Value & d) { walk(d, r.st); return true; }); }});
|
|
}
|
|
else if (uc == "select")
|
|
{
|
|
vs.push_back({std::string("RapidJSON: Document") + sfx, [n](const std::string & s, result & r) { rapidjson::Document d; d.Parse<F>(s.data(), s.size()); if (d.HasParseError()) { return false; } r.acc = pick<acc_rapidjson>(n, &d); return true; }});
|
|
vs.push_back({std::string("RapidJSON: insitu (copy)") + sfx, [n](const std::string & s, result & r) { std::string b = s; rapidjson::Document d; d.ParseInsitu<F>(&b[0]); if (d.HasParseError()) { return false; } r.acc = pick<acc_rapidjson>(n, &d); return true; }});
|
|
vs.push_back({std::string("RapidJSON: insitu (copy untimed)") + sfx, [n](const std::string & s, result & r) { taken_copy b(s); rapidjson::Document d; d.ParseInsitu<F>(b.buf); if (d.HasParseError()) { return false; } r.acc = pick<acc_rapidjson>(n, &d); return true; }, false, true});
|
|
vs.push_back({std::string("RapidJSON: Document (string stream)") + sfx, [n](const std::string & s, result & r) { rapidjson::Document d; d.Parse<F>(s.c_str()); if (d.HasParseError()) { return false; } r.acc = pick<acc_rapidjson>(n, &d); return true; }});
|
|
vs.push_back({std::string("RapidJSON: Document (raw numbers)") + sfx, [n](const std::string & s, result & r) { rapidjson::Document d; d.Parse<F | rj_raw>(s.data(), s.size()); if (d.HasParseError()) { return false; } r.acc = pick<acc_rapidjson_raw>(n, &d); return true; }});
|
|
vs.push_back({std::string("RapidJSON: Document (reused allocator)") + sfx, [n, rp](const std::string & s, result & r) { return rp->parse<F>(s, [&](const rapidjson::Value & d) { r.acc = pick<acc_rapidjson>(n, &d); return true; }); }});
|
|
}
|
|
else if (uc == "edit")
|
|
{
|
|
vs.push_back({std::string("RapidJSON: Document") + sfx, [n](const std::string & s, result & r) { rapidjson::Document d; d.Parse<F>(s.data(), s.size()); return !d.HasParseError() && edit_rapidjson(n, d, r); }});
|
|
vs.push_back({std::string("RapidJSON: insitu (copy)") + sfx, [n](const std::string & s, result & r) { std::string b = s; rapidjson::Document d; d.ParseInsitu<F>(&b[0]); return !d.HasParseError() && edit_rapidjson(n, d, r); }});
|
|
vs.push_back({std::string("RapidJSON: insitu (copy untimed)") + sfx, [n](const std::string & s, result & r) { taken_copy b(s); rapidjson::Document d; d.ParseInsitu<F>(b.buf); return !d.HasParseError() && edit_rapidjson(n, d, r); }, false, true});
|
|
vs.push_back({std::string("RapidJSON: Document (string stream)") + sfx, [n](const std::string & s, result & r) { rapidjson::Document d; d.Parse<F>(s.c_str()); return !d.HasParseError() && edit_rapidjson(n, d, r); }});
|
|
}
|
|
else if (uc == "minify")
|
|
{
|
|
vs.push_back({std::string("RapidJSON: Document + Writer") + sfx, [](const std::string & s, result & r) { rapidjson::Document d; d.Parse<F>(s.data(), s.size()); if (d.HasParseError()) { return false; } rapidjson::StringBuffer sb; rapidjson::Writer<rapidjson::StringBuffer> w(sb); return d.Accept(w) && emit(r, sb.GetString(), sb.GetSize()); }});
|
|
vs.push_back({std::string("RapidJSON: SAX to Writer") + sfx, [](const std::string & s, result & r) { rapidjson::StringBuffer sb; rapidjson::Writer<rapidjson::StringBuffer> w(sb); return rj_read<F>(s, w) && emit(r, sb.GetString(), sb.GetSize()); }});
|
|
vs.push_back({std::string("RapidJSON: SAX to Writer (string stream)") + sfx, [](const std::string & s, result & r) { rapidjson::StringBuffer sb; rapidjson::Writer<rapidjson::StringBuffer> w(sb); return rj_read_cstr<F>(s, w) && emit(r, sb.GetString(), sb.GetSize()); }});
|
|
vs.push_back({std::string("RapidJSON: SAX to Writer (raw numbers)") + sfx, [](const std::string & s, result & r) { rapidjson::StringBuffer sb; rj_raw_writer w(sb); return rj_read<F | rapidjson::kParseNumbersAsStringsFlag>(s, w) && emit(r, sb.GetString(), sb.GetSize()); }});
|
|
vs.push_back({std::string("RapidJSON: Document + Writer (reused allocator)") + sfx, [rp](const std::string & s, result & r) { return rp->parse<F>(s, [&r](const rapidjson::Value & d) { rapidjson::StringBuffer sb; rapidjson::Writer<rapidjson::StringBuffer> w(sb); return d.Accept(w) && emit(r, sb.GetString(), sb.GetSize()); }); }});
|
|
}
|
|
(void)n;
|
|
}
|
|
|
|
// write: each library's document is parsed once, outside the timer; the input argument is ignored
|
|
static std::vector<variant> write_variants(const std::string& s)
|
|
{
|
|
auto view = std::make_shared<const json_document>(json_document::parse(s));
|
|
auto j = std::make_shared<const json>(json::parse(s));
|
|
std::shared_ptr<yyjson_doc> yy(yyjson_read(s.data(), s.size(), 0), yyjson_doc_free);
|
|
auto sjp = std::make_shared<simdjson::dom::parser>();
|
|
const simdjson::dom::element sje = sjp->parse(s).value();
|
|
auto rj = std::make_shared<rapidjson::Document>();
|
|
rj->Parse<rj_flags>(s.data(), s.size());
|
|
auto so = std::make_shared<sonic_json::Document>();
|
|
so->Parse(s.data(), s.size());
|
|
#if !BENCH_NO_GLAZE
|
|
auto gl = std::make_shared<glz::generic_u64>();
|
|
(void)glz::read_json(*gl, s);
|
|
auto gld = std::make_shared<glz::generic>();
|
|
(void)glz::read_json(*gld, s);
|
|
#endif
|
|
auto bj = std::make_shared<const boost::json::value>(boost::json::parse(s, boost::json::make_shared_resource<boost::json::monotonic_resource>(), bj_precise));
|
|
return
|
|
{
|
|
{"nlohmann json_document: dump", [view](const std::string&, result & r) { return emit(r, view->root().dump()); }},
|
|
{"nlohmann json_document: dump (source numbers)", [view](const std::string&, result & r) { return emit(r, view->root().dump(-1, ' ', false, json_view::number_format::source)); }},
|
|
{"nlohmann json: dump", [j](const std::string&, result & r) { return emit(r, j->dump()); }},
|
|
{"yyjson: write", [yy](const std::string&, result & r) { std::size_t n = 0; char* o = yyjson_write(yy.get(), 0, &n); const bool ok = o != nullptr && emit(r, o, n); std::free(o); return ok; }},
|
|
{"simdjson: to_string", [sjp, sje](const std::string&, result & r) { return emit(r, simdjson::to_string(sje)); }},
|
|
{"RapidJSON: Writer", [rj](const std::string&, result & r) { rapidjson::StringBuffer sb; rapidjson::Writer<rapidjson::StringBuffer> w(sb); return rj->Accept(w) && emit(r, sb.GetString(), sb.GetSize()); }},
|
|
{"sonic-cpp: Serialize", [so](const std::string&, result & r) { sonic_json::WriteBuffer wb; return so->Serialize(wb) == sonic_json::kErrorNone && emit(r, wb.ToString(), wb.Size()); }},
|
|
#if !BENCH_NO_GLAZE
|
|
{"Glaze: generic", [gl](const std::string&, result & r) { std::string o; return !glz::write_json(*gl, o) && emit(r, std::move(o)); }},
|
|
{"Glaze: generic (double)", [gld](const std::string&, result & r) { std::string o; return !glz::write_json(*gld, o) && emit(r, std::move(o)); }},
|
|
{"Glaze: generic (strict)", [gl](const std::string&, result & r) { std::string o; return !glz::write<glz_strict>(*gl, o) && emit(r, std::move(o)); }},
|
|
{"Glaze: generic (double, strict)", [gld](const std::string&, result & r) { std::string o; return !glz::write<glz_strict>(*gld, o) && emit(r, std::move(o)); }},
|
|
#endif
|
|
{"Boost.JSON: serialize", [bj](const std::string&, result & r) { return emit(r, boost::json::serialize(*bj)); }},
|
|
};
|
|
}
|
|
|
|
///////////////////////////////////////////////////////////////////////////////
|
|
// driver
|
|
///////////////////////////////////////////////////////////////////////////////
|
|
|
|
static std::string slurp(const std::string& p)
|
|
{
|
|
std::ifstream f(p, std::ios::binary);
|
|
std::stringstream ss;
|
|
ss << f.rdbuf();
|
|
return ss.str();
|
|
}
|
|
|
|
// the input contract: at least 64 bytes of spare capacity
|
|
static std::string padded(const std::string& s)
|
|
{
|
|
std::string r;
|
|
r.reserve(s.size() + 64);
|
|
r.assign(s);
|
|
return r;
|
|
}
|
|
|
|
// the minified inputs (twitter_min, citm_catalog_min): the whitespace outside strings removed, the bytes inside strings
|
|
// untouched (the same function as strip_ws in rust/src/main.rs)
|
|
static std::string strip_ws(const std::string& s)
|
|
{
|
|
std::string r;
|
|
r.reserve(s.size());
|
|
bool in_string = false;
|
|
for (std::size_t i = 0; i < s.size(); ++i)
|
|
{
|
|
const char ch = s[i];
|
|
if (in_string)
|
|
{
|
|
r += ch;
|
|
if (ch == '\\' && i + 1 < s.size())
|
|
{
|
|
r += s[++i];
|
|
}
|
|
else if (ch == '"')
|
|
{
|
|
in_string = false;
|
|
}
|
|
}
|
|
else if (ch != ' ' && ch != '\n' && ch != '\r' && ch != '\t')
|
|
{
|
|
r += ch;
|
|
in_string = ch == '"';
|
|
}
|
|
}
|
|
return r;
|
|
}
|
|
|
|
struct bad_input
|
|
{
|
|
std::string what, text;
|
|
bool utf8; // the 0xFF probe; the others are structural errors
|
|
};
|
|
|
|
// malformed inputs derived from a valid document: truncated by one byte (after trailing whitespace), a stray comma
|
|
// before the first '}' outside a string, and a 0xFF byte right after the opening quote of the first string value (not
|
|
// a key, so that a library that accepts the byte still finds the keys the use cases look up) in the second half of the
|
|
// document, or of the last string value if the second half has none (canada.json)
|
|
static std::vector<bad_input> malformed(const std::string& s)
|
|
{
|
|
std::size_t first_close = std::string::npos, mid_quote = std::string::npos, last_quote = std::string::npos;
|
|
bool in_string = false;
|
|
std::vector<char> stack; // the open containers
|
|
char prev = 0; // the last structural character outside strings
|
|
for (std::size_t i = 0; i < s.size(); ++i)
|
|
{
|
|
const char ch = s[i];
|
|
if (in_string)
|
|
{
|
|
if (ch == '\\')
|
|
{
|
|
++i;
|
|
}
|
|
else if (ch == '"')
|
|
{
|
|
in_string = false;
|
|
}
|
|
}
|
|
else if (ch == '"')
|
|
{
|
|
in_string = true;
|
|
const bool value = prev == ':' || (!stack.empty() && stack.back() == '[' && (prev == '[' || prev == ','));
|
|
if (value)
|
|
{
|
|
last_quote = i;
|
|
if (mid_quote == std::string::npos && i >= s.size() / 2)
|
|
{
|
|
mid_quote = i;
|
|
}
|
|
}
|
|
prev = '"';
|
|
}
|
|
else if (ch == '{' || ch == '[')
|
|
{
|
|
stack.push_back(ch);
|
|
prev = ch;
|
|
}
|
|
else if (ch == '}' || ch == ']')
|
|
{
|
|
if (ch == '}' && first_close == std::string::npos)
|
|
{
|
|
first_close = i;
|
|
}
|
|
if (!stack.empty())
|
|
{
|
|
stack.pop_back();
|
|
}
|
|
prev = ch;
|
|
}
|
|
else if (ch == ':' || ch == ',')
|
|
{
|
|
prev = ch;
|
|
}
|
|
}
|
|
std::vector<bad_input> out;
|
|
std::string t = s;
|
|
while (!t.empty() && (t.back() == ' ' || t.back() == '\n' || t.back() == '\r' || t.back() == '\t'))
|
|
{
|
|
t.pop_back();
|
|
}
|
|
const std::string trailing = t + " ]x"; // after the value: what Glaze accepts by default
|
|
t.pop_back();
|
|
out.push_back({"truncated input", padded(t), false});
|
|
out.push_back({"trailing garbage", padded(trailing), false});
|
|
t = s;
|
|
if (first_close != std::string::npos)
|
|
{
|
|
t.insert(first_close, ",");
|
|
out.push_back({"a stray comma before '}'", padded(t), false});
|
|
}
|
|
else
|
|
{
|
|
// a file without objects (--files): the comma goes before the last ']'
|
|
t.insert(t.rfind(']'), ",");
|
|
out.push_back({"a stray comma before ']'", padded(t), false});
|
|
}
|
|
// a file without string values (--files) gets no UTF-8 probe
|
|
if (last_quote != std::string::npos)
|
|
{
|
|
t = s;
|
|
t.insert((mid_quote != std::string::npos ? mid_quote : last_quote) + 1, "\xFF");
|
|
out.push_back({"invalid UTF-8 (0xFF in a string)", padded(t), true});
|
|
}
|
|
return out;
|
|
}
|
|
|
|
static bool run(const variant& v, const std::string& in, result& r)
|
|
{
|
|
try
|
|
{
|
|
return v.fn(in, r);
|
|
}
|
|
catch (...)
|
|
{
|
|
return false;
|
|
}
|
|
}
|
|
|
|
// runs a variant in a child process, so that a crash cannot take the benchmark down: on a malformed input (e.g.,
|
|
// sonic-cpp accepts a key holding an invalid byte, and the edit then misses that key) or on the valid input
|
|
enum class verdict { rejected, accepted, crashed };
|
|
static verdict run_isolated(const variant& v, const std::string& in)
|
|
{
|
|
std::fflush(stdout);
|
|
const pid_t pid = fork();
|
|
if (pid == 0)
|
|
{
|
|
alarm(60); // a variant that hangs counts as crashed
|
|
result r;
|
|
std::string o;
|
|
r.out = &o;
|
|
_exit(run(v, in, r) ? 1 : 0);
|
|
}
|
|
int status = 0;
|
|
if (pid < 0 || waitpid(pid, &status, 0) != pid)
|
|
{
|
|
std::perror("fork/waitpid");
|
|
std::exit(1);
|
|
}
|
|
return WIFEXITED(status) ? (WEXITSTATUS(status) == 1 ? verdict::accepted : verdict::rejected) : verdict::crashed;
|
|
}
|
|
|
|
static bool same_text(const std::string& out, const json& expected)
|
|
{
|
|
try
|
|
{
|
|
return json::parse(out) == expected;
|
|
}
|
|
catch (...)
|
|
{
|
|
return false;
|
|
}
|
|
}
|
|
|
|
// the *.json files of a directory, sorted by name
|
|
static std::vector<std::filesystem::path> json_files(const std::string& dir)
|
|
{
|
|
std::vector<std::filesystem::path> fs;
|
|
for (const auto& e : std::filesystem::directory_iterator(dir))
|
|
{
|
|
if (e.path().extension() == ".json")
|
|
{
|
|
fs.push_back(e.path());
|
|
}
|
|
}
|
|
std::sort(fs.begin(), fs.end());
|
|
return fs;
|
|
}
|
|
|
|
// --conformance: every validate variant on every file of a directory (JSONTestSuite's test_parsing: y_ files must be
|
|
// accepted, n_ files rejected, i_ files may go either way), each call in a child process
|
|
static int conformance(const std::string& dir)
|
|
{
|
|
json_document reused;
|
|
simdjson::dom::parser sj;
|
|
simdjson::ondemand::parser od;
|
|
yy_alc yy;
|
|
bj_reuse bj;
|
|
rj_reuse rj;
|
|
context ctx{"", reused, sj, od, yy, bj, rj};
|
|
std::vector<variant> all = validate_variants(ctx);
|
|
add_rapidjson<rj_flags>(all, "validate", "", "", rj);
|
|
add_rapidjson<rj_fast>(all, "validate", "", " (no encoding check)", rj);
|
|
for (const auto& f : json_files(dir))
|
|
{
|
|
const std::string s = padded(slurp(f.string()));
|
|
for (const auto& v : all)
|
|
{
|
|
const verdict vd = run_isolated(v, s);
|
|
std::printf("conformance: %s | %s | %s\n", f.filename().string().c_str(), v.name.c_str(),
|
|
vd == verdict::accepted ? "accepted" : vd == verdict::rejected ? "rejected" : "crashed");
|
|
}
|
|
}
|
|
std::printf("done: conformance\n");
|
|
return 0;
|
|
}
|
|
|
|
int main(int argc, char** argv)
|
|
{
|
|
if (argc > 2 && std::string(argv[1]) == "--conformance")
|
|
{
|
|
return conformance(argv[2]);
|
|
}
|
|
// --files <dir>: every *.json file of the directory instead of the files below, without select and edit
|
|
const bool files = argc > 1 && std::string(argv[1]) == "--files";
|
|
const int a = files ? 2 : 1;
|
|
if (argc <= a)
|
|
{
|
|
std::fprintf(stderr, "usage: %s <json_test_data dir> [rounds] [document] [use case]\n"
|
|
" %s --files <dir> [rounds] [document] [use case]\n"
|
|
" %s --conformance <dir>\n", argv[0], argv[0], argv[0]);
|
|
return 1;
|
|
}
|
|
const std::string T = std::string(argv[a]) + "/";
|
|
const int rounds = argc > a + 1 ? std::max(1, std::atoi(argv[a + 1])) : 30;
|
|
const std::string only = argc > a + 2 ? argv[a + 2] : ""; // only this document; "" for all
|
|
const std::string only_uc = argc > a + 3 ? argv[a + 3] : ""; // only this use case
|
|
struct doc
|
|
{
|
|
std::string name, text;
|
|
int batch;
|
|
std::string base; // the document whose select and edit apply
|
|
};
|
|
std::vector<doc> docs;
|
|
if (files)
|
|
{
|
|
for (const auto& f : json_files(T))
|
|
{
|
|
// small files are timed in batches of about 500 KB of input, as status is
|
|
std::string text = padded(slurp(f.string()));
|
|
const int batch = static_cast<int>(std::max<std::size_t>(1, 500000 / std::max<std::size_t>(1, text.size())));
|
|
docs.push_back({f.stem().string(), std::move(text), batch, f.stem().string()});
|
|
}
|
|
}
|
|
else
|
|
{
|
|
for (const char* f :
|
|
{"nativejson-benchmark/twitter.json", "nativejson-benchmark/citm_catalog.json", "nativejson-benchmark/canada.json", "jeopardy/jeopardy.json"
|
|
})
|
|
{
|
|
std::string n = std::string(f).substr(std::string(f).find('/') + 1);
|
|
docs.push_back({n.substr(0, n.size() - 5), padded(slurp(T + f)), 1, n.substr(0, n.size() - 5)});
|
|
}
|
|
docs.push_back({"status", padded(json::parse(docs[0].text)["statuses"][0].dump()), 200, "status"});
|
|
docs.push_back({"twitter_min", padded(strip_ws(docs[0].text)), 1, "twitter"});
|
|
docs.push_back({"citm_catalog_min", padded(strip_ws(docs[1].text)), 1, "citm_catalog"});
|
|
}
|
|
|
|
json_document reused; // read() reuses its memory, as simdjson's parsers do
|
|
simdjson::dom::parser sj;
|
|
simdjson::ondemand::parser od;
|
|
yy_alc yy;
|
|
bj_reuse bj;
|
|
rj_reuse rj;
|
|
std::size_t excluded_total = 0;
|
|
for (const auto& dc : docs)
|
|
{
|
|
if (!only.empty() && dc.name != only)
|
|
{
|
|
continue;
|
|
}
|
|
const std::string& s = dc.text;
|
|
if (s.capacity() - s.size() < 64)
|
|
{
|
|
std::printf("WARNING: %s has less than 64 bytes of spare capacity\n", dc.name.c_str());
|
|
}
|
|
const std::vector<bad_input> bad = malformed(s);
|
|
context ctx{dc.base, reused, sj, od, yy, bj, rj};
|
|
for (const char* const ucp : {"validate", "sum", "select", "edit", "minify", "write"})
|
|
{
|
|
const std::string uc = ucp;
|
|
if ((!only_uc.empty() && uc != only_uc) || (files && (uc == "select" || uc == "edit")))
|
|
{
|
|
continue;
|
|
}
|
|
std::vector<variant> all = uc == "validate" ? validate_variants(ctx) : uc == "sum" ? sum_variants(ctx) : uc == "select" ? select_variants(ctx)
|
|
: uc == "edit" ? edit_variants(ctx) : uc == "minify" ? minify_variants(ctx) : write_variants(s);
|
|
if (uc != "write")
|
|
{
|
|
add_rapidjson<rj_flags>(all, uc, dc.base, "", rj);
|
|
add_rapidjson<rj_fast>(all, uc, dc.base, " (no encoding check)", rj);
|
|
}
|
|
// BENCH_ONLY=<prefix>[,<prefix>...]: only the variants whose name starts with one of them (A/B runs of two
|
|
// nlohmann headers, or a few libraries)
|
|
if (const char* const only = std::getenv("BENCH_ONLY"))
|
|
{
|
|
std::vector<std::string> prefixes;
|
|
std::string rest = only;
|
|
for (std::size_t comma; (comma = rest.find(',')) != std::string::npos; rest.erase(0, comma + 1))
|
|
{
|
|
prefixes.push_back(rest.substr(0, comma));
|
|
}
|
|
prefixes.push_back(rest);
|
|
all.erase(std::remove_if(all.begin(), all.end(), [&](const variant& v)
|
|
{
|
|
return std::none_of(prefixes.begin(), prefixes.end(), [&](const std::string& p) { return v.name.compare(0, p.size(), p) == 0; });
|
|
}), all.end());
|
|
}
|
|
|
|
// the reference result, from json_document (sum, select) or json (edit, minify, write)
|
|
stats ref_st;
|
|
double ref_acc = 0;
|
|
json ref_json;
|
|
if (uc == "sum")
|
|
{
|
|
const auto d = json_document::parse(s);
|
|
walk(d.root(), ref_st);
|
|
std::printf("check %-13s %-9s reference: nodes %zu str %zu num %.17g\n", dc.name.c_str(), uc.c_str(), ref_st.nodes, ref_st.str, ref_st.num);
|
|
}
|
|
else if (uc == "select")
|
|
{
|
|
const auto d = json_document::parse(s);
|
|
ref_acc = pick<acc_view>(dc.base, d.root());
|
|
std::printf("check %-13s %-9s reference: %.17g\n", dc.name.c_str(), uc.c_str(), ref_acc);
|
|
}
|
|
else if (uc != "validate")
|
|
{
|
|
ref_json = json::parse(s);
|
|
if (uc == "edit")
|
|
{
|
|
edit(dc.base, ref_json);
|
|
}
|
|
}
|
|
|
|
std::vector<variant> vs;
|
|
std::vector<std::string> levels;
|
|
for (auto& v : all)
|
|
{
|
|
std::string why;
|
|
std::string out;
|
|
result r;
|
|
r.out = &out;
|
|
char buf[200];
|
|
if (run_isolated(v, s) == verdict::crashed)
|
|
{
|
|
why = "crashes on the valid input";
|
|
}
|
|
else if (!run(v, s, r))
|
|
{
|
|
why = "rejects the valid input";
|
|
}
|
|
else if (uc == "sum" && !(r.st.nodes == ref_st.nodes && r.st.str == ref_st.str
|
|
&& (v.any_order ? std::fabs(r.st.num - ref_st.num) <= 1e-9 * std::fabs(ref_st.num) : r.st.num == ref_st.num)))
|
|
{
|
|
std::snprintf(buf, sizeof(buf), "wrong result (nodes %zu str %zu num %.17g)", r.st.nodes, r.st.str, r.st.num);
|
|
why = buf;
|
|
}
|
|
else if (uc == "select" && r.acc != ref_acc)
|
|
{
|
|
std::snprintf(buf, sizeof(buf), "wrong result (%.17g)", r.acc);
|
|
why = buf;
|
|
}
|
|
else if ((uc == "edit" || uc == "minify" || uc == "write") && !same_text(out, ref_json))
|
|
{
|
|
why = "the output does not read back as the expected document";
|
|
}
|
|
if (why.empty())
|
|
{
|
|
// the validation level from the malformed-input probes: "full" rejects all of them, "no UTF-8"
|
|
// accepts only the 0xFF byte, "partial" accepts (or crashes on) a structural error or crashes
|
|
// on any probe; write has no input to validate ("n/a")
|
|
std::string level = "n/a";
|
|
if (uc != "write")
|
|
{
|
|
std::string notes;
|
|
bool structural = false, utf8 = false;
|
|
for (const auto& b : bad)
|
|
{
|
|
const verdict vd = run_isolated(v, b.text);
|
|
if (vd == verdict::rejected)
|
|
{
|
|
continue;
|
|
}
|
|
notes += std::string(notes.empty() ? "" : "; ") + (vd == verdict::accepted ? "accepts " : "crashes on ") + b.what;
|
|
(b.utf8 && vd == verdict::accepted ? utf8 : structural) = true;
|
|
}
|
|
level = structural ? "partial" : utf8 ? "no UTF-8" : "full";
|
|
if (!notes.empty())
|
|
{
|
|
std::printf("level: %s %s | %s | %s | %s\n", dc.name.c_str(), uc.c_str(), v.name.c_str(), level.c_str(), notes.c_str());
|
|
}
|
|
}
|
|
vs.push_back(std::move(v));
|
|
levels.push_back(level);
|
|
}
|
|
else
|
|
{
|
|
++excluded_total;
|
|
std::printf("excluded: %s %s | %s | %s\n", dc.name.c_str(), uc.c_str(), v.name.c_str(), why.c_str());
|
|
}
|
|
}
|
|
std::printf("check %-13s %-9s %zu of %zu variants give the right result\n", dc.name.c_str(), uc.c_str(), vs.size(), all.size());
|
|
std::fflush(stdout);
|
|
if (vs.empty())
|
|
{
|
|
continue;
|
|
}
|
|
|
|
std::vector<double> best(vs.size(), 1e300);
|
|
const int r = s.size() > 10000000 ? std::min(rounds, std::max(3, rounds / 5)) : rounds;
|
|
// a new order of the variants in every round: a variant's time can depend on the one before it (up to 15 %)
|
|
std::vector<std::size_t> order(vs.size());
|
|
std::iota(order.begin(), order.end(), std::size_t{0});
|
|
std::mt19937 rng(12345);
|
|
for (int i = 0; i < r; ++i)
|
|
{
|
|
if (i > 0)
|
|
{
|
|
std::shuffle(order.begin(), order.end(), rng);
|
|
}
|
|
for (const std::size_t k : order)
|
|
{
|
|
result res;
|
|
if (vs[k].untimed_copy)
|
|
{
|
|
g_copies.make(s, 1);
|
|
}
|
|
run(vs[k], s, res); // untimed: the previous variant's leftovers are cleaned up here
|
|
g_sink = res.st.num + res.acc;
|
|
if (vs[k].untimed_copy)
|
|
{
|
|
g_copies.make(s, dc.batch);
|
|
}
|
|
const auto t0 = std::chrono::steady_clock::now();
|
|
for (int b = 0; b < dc.batch; ++b)
|
|
{
|
|
result rt;
|
|
g_sink = run(vs[k], s, rt) ? rt.st.num + rt.acc : -1;
|
|
}
|
|
best[k] = std::min(best[k], std::chrono::duration<double, std::nano>(std::chrono::steady_clock::now() - t0).count() / dc.batch);
|
|
g_copies.clear();
|
|
}
|
|
}
|
|
std::printf("%-13s %-9s", dc.name.c_str(), uc.c_str());
|
|
for (std::size_t k = 0; k < vs.size(); ++k)
|
|
{
|
|
std::printf(" | %s [%s] %.1fus (%.2fx)", vs[k].name.c_str(), levels[k].c_str(), best[k] / 1e3, best[k] / best[0]);
|
|
}
|
|
std::printf("\n");
|
|
std::fflush(stdout);
|
|
}
|
|
}
|
|
std::printf("done: %zu variant(s) excluded\n", excluded_total);
|
|
}
|