docs: compare json_document with Glaze, sonic, RapidJSON and Rust parsers

Extends the comparison in "How 3.13.0 got faster" from yyjson, simdjson and
Boost.JSON to eleven engines: sonic-cpp, RapidJSON and Glaze (generic DOM,
lazy_json, reading into structs) in C++, sonic-rs, simd-json and serde_json in
Rust. Measured on an Apple M1 Max (Apple clang 21, Rust 1.99) and an x86-64
Haswell server (GCC 13, Clang 18). Every engine must return the same values,
string bytes and bit-identical number sums before it is timed.

The post now answers whether json_document is the fastest parser: it is the
fastest document parser at parse+dump everywhere and at parsing on Apple
silicon, ties with simdjson's DOM on x86-64, and loses to simdjson On-Demand
and Glaze structs when only a few fields are read.

tests/benchmarks/json_view/others holds the programs (bench_all.cpp, the Rust
crate with its Cargo.lock), summarize.py and the pinned versions and checksums.

Signed-off-by: Niels Lohmann <mail@nlohmann.me>
This commit is contained in:
Niels Lohmann committed 2026-10-30 15:29:46 +01:00
1 parent ea848cd2e8
commit 7341f127d3
9 files changed
+1987 -117

No files matched your search

@@ -0,0 +1,74 @@
# json_document against other JSON libraries
[`compare.py`](../compare.py) compares `json_document` with yyjson, simdjson and Boost.JSON. The programs here add
sonic-cpp, RapidJSON and Glaze in C++, and sonic-rs, simd-json and serde_json in Rust, for the post
[How 3.13.0 got faster](https://json.nlohmann.me/home/blog/how_3_13_0_got_faster/#how-it-compares). They are not
built by CMake or run by CI.
## What is measured
The same files and method as `bench_view.cpp`: `twitter.json`, `citm_catalog.json`, `canada.json`, `jeopardy.json`
and one status object of `twitter.json`; every engine runs interleaved in every round, each timed call after an
untimed one, and the best round is reported.
| workload | what it does |
|---|---|
| `parse` | build the document and free it |
| `traverse` | parse, then visit every value, convert every number, touch every string and key |
| `select` | parse, then read a few fields of every record (the fields are listed in `bench_view.cpp`) |
| `parse+dump` | parse, then write the document as compact JSON |
Before timing, every engine must report the same number of values, the same number of string bytes, and a sum of all
numbers in document order that is bit-identical to `json_document`'s (`json::parse` visits objects in key order and
is compared within 1e-9): every number is converted exactly. For *select*, all engines must return the same value.
| engine | version | notes |
|---|---|---|
| `json_document` | this checkout | `json_view (reused)`: one document whose `read()` reuses its memory |
| yyjson | 0.13.0 | |
| simdjson | 4.6.11 | DOM with a reused parser (as recommended) and a fresh one; On-Demand for *traverse* and *select* |
| sonic-cpp | 1.0.2 | |
| RapidJSON | master 24b5e7a8b27f (2025-02-05) | `kParseFullPrecisionFlag` (correctly rounded floats); also default flags and in situ parsing with a copy of the input |
| Glaze | 9.0.0 | `generic_u64` (owning DOM), `lazy_json` (no validation), and for *select* reading into structs that declare only the fields used |
| Boost.JSON | 1.92.0 (Mac), 1.83 (system, x86-64) | monotonic resource |
| sonic-rs, simd-json, serde_json | see `rust/Cargo.lock` | simd-json parses in place: its times include copying the input; serde_json with `float_roundtrip` |
SHA-256 of the downloads:
```
dd7b033c5bf6e4308615bb7834c541291d5b20f64121d9a68222a39414635517 glaze v9.0.0.tar.gz
30edf9e47c417f653546c39295fdb66ba5f89fb37eb3780122c6ee66605e5837 sonic-cpp v1.0.2.tar.gz
2d2601a82d2d3b7e143a3c8d43ef616671391034bc46891a9816b79cf2d3e7a8 rapidjson 24b5e7a8b27f42fa16b96fc70aade9106cf7102f.tar.gz
34e0f62a2bc11ab20d601e8ca1cc2b2079503aa45119a19133d89d19b94a0fae yyjson 0.13.0.tar.gz
61d948fc24f0d793829ad658058e7597d064988a89b4607ea02e401a82df98ff simdjson v4.6.11.tar.gz
c4a3b310ddd2472416e091067166b0713be97c63f38c212c484ada022fd296ce boost_1_92_0.tar.gz
```
## Building and running
All libraries except yyjson and simdjson are used header-only. With the downloads unpacked into `DL`
(`compare.py --download` fetches yyjson, simdjson and Boost into its build directory):
```sh
cc -O3 -DNDEBUG -c $DL/yyjson-0.13.0/src/yyjson.c -o yyjson.o
c++ -std=c++17 -O3 -DNDEBUG -c $DL/simdjson-4.6.11/singleheader/simdjson.cpp -o simdjson.o
c++ -std=c++23 -O3 -DNDEBUG -DRAPIDJSON_NEON \
-I ../../../../include -I $DL/yyjson-0.13.0/src -I $DL/simdjson-4.6.11/singleheader -I $DL/boost_1_92_0 \
-I $DL/glaze/include -I $DL/rapidjson/include -I $DL/sonic/include \
bench_all.cpp yyjson.o simdjson.o -o bench_all
./bench_all <json_test_data> 30 > mac-cpp-1.log
cd rust && cargo build --release && ./target/release/bench_rs <json_test_data> 30 > ../mac-rs-1.log
python3 summarize.py mac-cpp-*.log --rust mac-rs-*.log
```
On x86-64, use `-march=haswell -DRAPIDJSON_SSE42` instead of `-DRAPIDJSON_NEON` (sonic-cpp needs AVX2), pin the
process to one core, and add the document name as third argument to time one document per process. Glaze 9 needs
`std::expected`, which Clang 18 cannot use from GCC 13's standard library; `-DBENCH_NO_GLAZE=1` builds without it.
## Reading the results
`summarize.py` prints, for every document and workload, the engines ordered by time and the fastest other engine
relative to `json_document`. Fresh documents are compared with fresh ones; the reused `json_document` is compared
with simdjson's reused parser. On a shared machine, differences of a few percent are noise: run twice and compare.
@@ -0,0 +1,852 @@
// __ _____ _____ _____
// __| | __| | | | 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
// Parse/traverse/dump of the nativejson-benchmark files and jeopardy with every engine in one binary,
// interleaved, best round reported (same method as tests/benchmarks/json_view/bench_view.cpp).
// Engines: json_document (nlohmann), yyjson, simdjson DOM and On-Demand, RapidJSON, sonic-cpp,
// Glaze (generic_u64 DOM and lazy_json), Boost.JSON, json::parse.
// Usage: bench_all <json_test_data dir> [rounds] [document]
#include <nlohmann/json_view.hpp>
#include <boost/json.hpp>
#include <boost/json/src.hpp>
#if !BENCH_NO_GLAZE
#include <glaze/glaze.hpp>
#include <glaze/json/lazy.hpp>
#endif
#include <rapidjson/document.h>
#include <rapidjson/stringbuffer.h>
#include <rapidjson/writer.h>
#include <simdjson.h>
#include <sonic/sonic.h>
#include <yyjson.h>
#include <algorithm>
#include <chrono>
#include <cmath>
#include <cstdio>
#include <fstream>
#include <functional>
#include <sstream>
#include <string>
#include <vector>
using nlohmann::json;
using nlohmann::json_document;
using nlohmann::json_view;
static volatile double g_sink;
struct stats
{
double num = 0;
std::size_t str = 0, nodes = 0;
};
// ---- json::parse and json_view
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;
}
}
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;
}
}
// ---- 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;
}
}
// ---- simdjson DOM and On-Demand
static void walk(simdjson::dom::element e, stats& st)
{
++st.nodes;
switch (e.type())
{
case simdjson::dom::element_type::OBJECT:
for (auto f : simdjson::dom::object(e))
{
st.str += f.key.size();
walk(f.value, st);
}
break;
case simdjson::dom::element_type::ARRAY:
for (auto c : simdjson::dom::array(e)) walk(c, st);
break;
case simdjson::dom::element_type::STRING: st.str += std::string_view(e).size(); break;
case simdjson::dom::element_type::INT64: st.num += static_cast<double>(int64_t(e)); break;
case simdjson::dom::element_type::UINT64: st.num += static_cast<double>(uint64_t(e)); break;
case simdjson::dom::element_type::DOUBLE: st.num += double(e); 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: (void)v.is_null(); break;
}
}
// ---- RapidJSON
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;
}
}
// ---- 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 (owning DOM) and lazy_json (on demand, no validation)
static void walk(const glz::generic_u64& j, stats& st)
{
++st.nodes;
std::visit([&](const auto& x)
{
using T = std::decay_t<decltype(x)>;
if constexpr (std::is_same_v<T, glz::generic_u64::object_t>)
{
for (const auto& [k, v] : x)
{
st.str += k.size();
walk(v, st);
}
}
else if constexpr (std::is_same_v<T, glz::generic_u64::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);
}
template<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(item, st);
}
}
else if (v.is_array())
{
for (auto item : v) walk_lazy(item, st);
}
else if (v.is_string())
{
st.str += v.template get<std::string>().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
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;
}
}
// ---- selective access (copied from bench_view.cpp)
template<class V>
static double pick_view(const std::string& name, V r)
{
double acc = 0;
if (name == "twitter" || name == "status")
{
auto one = [&](V s)
{
acc += static_cast<double>(s["id"].template get<std::uint64_t>());
acc += static_cast<double>(s["user"]["screen_name"].get_string().size());
acc += static_cast<double>(s["retweet_count"].template get<std::int64_t>());
};
if (name == "twitter")
{
for (const V s : r["statuses"])
{
one(s);
}
}
else
{
one(r);
}
}
else if (name == "citm_catalog")
{
for (const V p : r["performances"])
{
acc += static_cast<double>(p["id"].template get<std::uint64_t>() + p["eventId"].template get<std::uint64_t>() + p["seatCategories"].size());
}
acc += static_cast<double>(r["events"].size());
}
else if (name == "canada")
{
const V g = r["features"][0]["geometry"];
acc += static_cast<double>(r["type"].get_string().size() + g["type"].get_string().size() + g["coordinates"].size());
}
else if (name == "jeopardy")
{
for (const V q : r)
{
acc += q["round"].get_string() == "Final Jeopardy!" ? 1 : 0;
acc += static_cast<double>(q["category"].get_string().size());
}
}
else if (name == "rpc")
{
acc += static_cast<double>(r["method"].get_string().size());
acc += static_cast<double>(r["params"]["minuend"].template get<std::int64_t>() + r["id"].template get<std::int64_t>());
}
return acc;
}
static double pick(const std::string& name, json_view r)
{
return pick_view<json_view>(name, r);
}
static double pick(const std::string& name, yyjson_val* r)
{
double acc = 0;
auto get = [](yyjson_val * o, const char* k)
{
return yyjson_obj_get(o, k);
};
if (name == "twitter" || name == "status")
{
auto one = [&](yyjson_val * s)
{
acc += static_cast<double>(yyjson_get_uint(get(s, "id")));
acc += static_cast<double>(yyjson_get_len(get(get(s, "user"), "screen_name")));
acc += static_cast<double>(yyjson_get_sint(get(s, "retweet_count")));
};
if (name == "twitter")
{
std::size_t idx, max;
yyjson_val* s;
yyjson_arr_foreach(get(r, "statuses"), idx, max, s)
{
one(s);
}
}
else
{
one(r);
}
}
else if (name == "citm_catalog")
{
std::size_t idx, max;
yyjson_val* p;
yyjson_arr_foreach(get(r, "performances"), idx, max, p)
{
acc += static_cast<double>(yyjson_get_uint(get(p, "id")) + yyjson_get_uint(get(p, "eventId")) + yyjson_arr_size(get(p, "seatCategories")));
}
acc += static_cast<double>(yyjson_obj_size(get(r, "events")));
}
else if (name == "canada")
{
yyjson_val* g = get(yyjson_arr_get(get(r, "features"), 0), "geometry");
acc += static_cast<double>(yyjson_get_len(get(r, "type")) + yyjson_get_len(get(g, "type")) + yyjson_arr_size(get(g, "coordinates")));
}
else if (name == "jeopardy")
{
std::size_t idx, max;
yyjson_val* q;
yyjson_arr_foreach(r, idx, max, q)
{
acc += yyjson_equals_str(get(q, "round"), "Final Jeopardy!") ? 1 : 0;
acc += static_cast<double>(yyjson_get_len(get(q, "category")));
}
}
else if (name == "rpc")
{
acc += static_cast<double>(yyjson_get_len(get(r, "method")));
acc += static_cast<double>(yyjson_get_sint(get(get(r, "params"), "minuend")) + yyjson_get_sint(get(r, "id")));
}
return acc;
}
static double pick(const std::string& name, simdjson::dom::element r)
{
double acc = 0;
if (name == "twitter" || name == "status")
{
auto one = [&](simdjson::dom::element 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 : simdjson::dom::array(r["statuses"]))
{
one(s);
}
}
else
{
one(r);
}
}
else if (name == "citm_catalog")
{
for (auto p : simdjson::dom::array(r["performances"]))
{
acc += static_cast<double>(uint64_t(p["id"]) + uint64_t(p["eventId"]) + simdjson::dom::array(p["seatCategories"]).size());
}
acc += static_cast<double>(simdjson::dom::object(r["events"]).size());
}
else if (name == "canada")
{
auto g = r["features"].at(0)["geometry"];
acc += static_cast<double>(std::string_view(r["type"]).size() + std::string_view(g["type"]).size() + simdjson::dom::array(g["coordinates"]).size());
}
else if (name == "jeopardy")
{
for (auto q : simdjson::dom::array(r))
{
acc += std::string_view(q["round"]) == "Final Jeopardy!" ? 1 : 0;
acc += static_cast<double>(std::string_view(q["category"]).size());
}
}
else if (name == "rpc")
{
acc += static_cast<double>(std::string_view(r["method"]).size());
acc += static_cast<double>(int64_t(r["params"]["minuend"]) + int64_t(r["id"]));
}
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;
}
}
else if (name == "rpc")
{
acc += static_cast<double>(std::string_view(d["method"]).size());
acc += static_cast<double>(int64_t(d["params"]["minuend"]));
acc += static_cast<double>(int64_t(d["id"]));
}
return acc;
}
#if !BENCH_NO_GLAZE
// Glaze reading into structs that declare only the fields the selection uses; unknown keys are skipped
namespace sel
{
struct user { std::string screen_name; };
struct status { std::uint64_t id{}; sel::user user; std::int64_t retweet_count{}; };
struct twitter { std::vector<status> statuses; };
struct performance { std::uint64_t id{}; std::uint64_t eventId{}; std::vector<glz::raw_json_view> seatCategories; };
struct citm { std::map<std::string, glz::raw_json_view> events; std::vector<performance> performances; };
struct geometry { std::string type; std::vector<glz::raw_json_view> coordinates; };
struct feature { sel::geometry geometry; };
struct canada { std::string type; std::vector<feature> features; };
struct question { std::string category; std::string round; };
}
constexpr glz::opts sel_opts{.error_on_unknown_keys = false};
static double pick_glaze_structs(const std::string& name, const std::string& s)
{
double acc = 0;
if (name == "twitter" || name == "status")
{
auto one = [&](const sel::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")
{
sel::twitter t;
if (glz::read<sel_opts>(t, s)) return -1;
for (const auto& x : t.statuses) one(x);
}
else
{
sel::status x;
if (glz::read<sel_opts>(x, s)) return -1;
one(x);
}
}
else if (name == "citm_catalog")
{
sel::citm c;
if (glz::read<sel_opts>(c, s)) return -1;
for (const auto& p : c.performances) acc += static_cast<double>(p.id + p.eventId + p.seatCategories.size());
acc += static_cast<double>(c.events.size());
}
else if (name == "canada")
{
sel::canada c;
if (glz::read<sel_opts>(c, s)) return -1;
acc += static_cast<double>(c.type.size() + c.features[0].geometry.type.size() + c.features[0].geometry.coordinates.size());
}
else if (name == "jeopardy")
{
std::vector<sel::question> q;
if (glz::read<sel_opts>(q, s)) return -1;
for (const auto& x : q)
{
acc += x.round == "Final Jeopardy!" ? 1 : 0;
acc += static_cast<double>(x.category.size());
}
}
return acc;
}
template<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<std::string>().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<std::string>().value().size() + g["type"].template get<std::string>().value().size() + g["coordinates"].size());
}
else if (name == "jeopardy")
{
for (auto q : d.root())
{
acc += q["round"].template get<std::string>().value() == "Final Jeopardy!" ? 1 : 0;
acc += static_cast<double>(q["category"].template get<std::string>().value().size());
}
}
return acc;
}
#endif
static std::string slurp(const std::string& p)
{
std::ifstream f(p, std::ios::binary);
std::stringstream ss;
ss << f.rdbuf();
return ss.str();
}
struct engine
{
std::string name;
std::function<void()> fn;
};
static bool same(const stats& a, const stats& b)
{
return a.nodes == b.nodes && a.str == b.str && std::fabs(a.num - b.num) <= 1e-9 * std::fabs(a.num);
}
constexpr auto rj_flags = rapidjson::kParseFullPrecisionFlag; // correctly rounded floats, as every other engine
int main(int argc, char** argv)
{
const std::string T = std::string(argv[1]) + "/";
const int rounds = argc > 2 ? std::atoi(argv[2]) : 30;
const std::string only = argc > 3 ? argv[3] : ""; // time only this document (the check covers all)
struct doc
{
std::string name, text;
int batch;
};
std::vector<doc> docs;
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), slurp(T + f), 1});
}
docs.push_back({"status", json::parse(docs[0].text)["statuses"][0].dump(), 200});
// correctness: every engine must see the same values as json::parse
bool all_ok = true;
for (const auto& dc : docs)
{
stats ref;
walk(json::parse(dc.text), ref);
std::vector<std::pair<std::string, stats>> got(9, {"(not built)", stats{}});
{ auto d = json_document::parse(dc.text); walk(d.root(), got[0].second); got[0].first = "json_view"; }
{ yyjson_doc* d = yyjson_read(dc.text.data(), dc.text.size(), 0); walk(yyjson_doc_get_root(d), got[1].second); yyjson_doc_free(d); got[1].first = "yyjson"; }
{ simdjson::dom::parser p; walk(p.parse(dc.text).value(), got[2].second); got[2].first = "simdjson DOM"; }
{ simdjson::ondemand::parser p; simdjson::padded_string ps(dc.text); auto d = p.iterate(ps).value(); walk_od(d.get_value().value(), got[3].second); got[3].first = "simdjson OD"; }
{ rapidjson::Document d; d.Parse<rj_flags>(dc.text.data(), dc.text.size()); if (d.HasParseError()) std::puts("RapidJSON parse error"); walk(d, got[4].second); got[4].first = "RapidJSON"; }
{ rapidjson::Document d; d.Parse(dc.text.data(), dc.text.size()); stats x; walk(d, x); std::printf("info %-13s RapidJSON default flags: sum %s\n", dc.name.c_str(), x.num == got[0].second.num ? "exact" : "differs from the correctly rounded one"); }
{ sonic_json::Document d; d.Parse(dc.text.data(), dc.text.size()); if (d.HasParseError()) std::puts("sonic parse error"); walk_sonic(d, got[5].second); got[5].first = "sonic-cpp"; }
#if !BENCH_NO_GLAZE
{ glz::generic_u64 j; auto ec = glz::read_json(j, dc.text); if (ec) std::puts("glaze parse error"); walk(j, got[6].second); got[6].first = "Glaze generic"; }
#endif
#if !BENCH_NO_GLAZE
{ auto r = glz::lazy_json(dc.text); walk_lazy(r->root(), got[7].second); got[7].first = "Glaze lazy"; }
#endif
{ auto v = boost::json::parse(dc.text); walk(v, got[8].second); got[8].first = "Boost.JSON"; }
std::printf("check %-13s", dc.name.c_str());
for (const auto& g : got)
{
if (g.first == "(not built)") continue;
const bool ok = same(ref, g.second);
all_ok = all_ok && ok;
// every engine but json::parse (std::map order) visits the values in document order, so their sums
// must be bit-identical to json_view's if every number was converted to the same double
const bool exact = g.second.num == got[0].second.num;
std::printf(" %s:%s%s", g.first.c_str(), ok ? "OK" : "MISMATCH", exact ? "" : "(inexact)");
if (!ok) std::printf("(nodes %zu/%zu str %zu/%zu num %.17g/%.17g)", g.second.nodes, ref.nodes, g.second.str, ref.str, g.second.num, ref.num);
}
std::printf("\n");
}
for (const auto& dc : docs)
{
const auto d = json_document::parse(dc.text);
const double ref = pick(dc.name, d.root());
yyjson_doc* y = yyjson_read(dc.text.data(), dc.text.size(), 0);
const double py = pick(dc.name, yyjson_doc_get_root(y));
yyjson_doc_free(y);
simdjson::ondemand::parser p;
simdjson::padded_string ps(dc.text);
auto od_doc = p.iterate(ps).value();
const double po = pick_od(dc.name, od_doc);
#if !BENCH_NO_GLAZE
const double pg = pick_glaze_structs(dc.name, dc.text);
auto lz = glz::lazy_json(dc.text);
const double pl = pick_glaze_lazy(dc.name, *lz);
#else
const double pg = ref, pl = ref;
#endif
const bool ok = py == ref && po == ref && pg == ref && pl == ref;
all_ok = all_ok && ok;
std::printf("check %-13s select %s (json_view %.17g yyjson %.17g OD %.17g Glaze structs %.17g lazy %.17g)\n", dc.name.c_str(), ok ? "OK" : "MISMATCH", ref, py, po, pg, pl);
}
if (!all_ok) std::printf("WARNING: some engines disagree, see above\n");
json_document reused; // read() reuses its memory, as simdjson DOM reuses its parser
simdjson::dom::parser sj;
simdjson::ondemand::parser od;
for (const auto& dc : docs)
{
if (!only.empty() && dc.name != only) continue;
const std::string& s = dc.text;
const simdjson::padded_string ps(s);
std::vector<std::pair<std::string, std::vector<engine>>> workloads;
workloads.push_back({"parse", {
{"json_view", [&] { auto d = json_document::parse(s); g_sink = static_cast<double>(d.node_count()); }},
{"json_view (reused)", [&] { reused.read(s); g_sink = static_cast<double>(reused.node_count()); }},
{"yyjson", [&] { yyjson_doc* d = yyjson_read(s.data(), s.size(), 0); g_sink = static_cast<double>(yyjson_doc_get_val_count(d)); yyjson_doc_free(d); }},
{"simdjson DOM", [&] { auto e = sj.parse(ps).value_unsafe(); g_sink = e.is_object(); }},
{"simdjson DOM (fresh)", [&] { simdjson::dom::parser p; auto e = p.parse(ps).value_unsafe(); g_sink = e.is_object(); }},
{"RapidJSON", [&] { rapidjson::Document d; d.Parse<rj_flags>(s.data(), s.size()); g_sink = d.IsObject(); }},
{"RapidJSON (default flags)", [&] { rapidjson::Document d; d.Parse(s.data(), s.size()); g_sink = d.IsObject(); }},
{"RapidJSON (in situ, with copy)", [&] { std::string c = s; rapidjson::Document d; d.ParseInsitu<rj_flags>(&c[0]); g_sink = d.IsObject(); }},
{"sonic-cpp", [&] { sonic_json::Document d; d.Parse(s.data(), s.size()); g_sink = d.IsObject(); }},
#if !BENCH_NO_GLAZE
{"Glaze generic", [&] { glz::generic_u64 j; auto ec = glz::read_json(j, s); g_sink = static_cast<double>(bool(ec)); }},
#endif
{"Boost.JSON", [&] { boost::json::monotonic_resource mr; auto v = boost::json::parse(s, &mr); g_sink = v.is_object(); }},
{"json::parse", [&] { json j = json::parse(s); g_sink = static_cast<double>(j.size()); }},
}});
workloads.push_back({"traverse", {
{"json_view", [&] { auto d = json_document::parse(s); stats st; walk(d.root(), st); g_sink = st.num; }},
{"json_view (reused)", [&] { reused.read(s); stats st; walk(reused.root(), st); g_sink = st.num; }},
{"yyjson", [&] { yyjson_doc* d = yyjson_read(s.data(), s.size(), 0); stats st; walk(yyjson_doc_get_root(d), st); g_sink = st.num; yyjson_doc_free(d); }},
{"simdjson DOM", [&] { stats st; walk(sj.parse(ps).value_unsafe(), st); g_sink = st.num; }},
{"simdjson OD", [&] { auto d = od.iterate(ps).value_unsafe(); stats st; walk_od(d.get_value().value_unsafe(), st); g_sink = st.num; }},
{"RapidJSON", [&] { rapidjson::Document d; d.Parse<rj_flags>(s.data(), s.size()); stats st; walk(d, st); g_sink = st.num; }},
{"RapidJSON (default flags)", [&] { rapidjson::Document d; d.Parse(s.data(), s.size()); stats st; walk(d, st); g_sink = st.num; }},
{"sonic-cpp", [&] { sonic_json::Document d; d.Parse(s.data(), s.size()); stats st; walk_sonic(d, st); g_sink = st.num; }},
#if !BENCH_NO_GLAZE
{"Glaze generic", [&] { glz::generic_u64 j; auto ec = glz::read_json(j, s); stats st; walk(j, st); g_sink = st.num + static_cast<double>(bool(ec)); }},
#endif
#if !BENCH_NO_GLAZE
{"Glaze lazy", [&] { auto r = glz::lazy_json(s); stats st; walk_lazy(r->root(), st); g_sink = st.num; }},
#endif
{"Boost.JSON", [&] { boost::json::monotonic_resource mr; auto v = boost::json::parse(s, &mr); stats st; walk(v, st); g_sink = st.num; }},
{"json::parse", [&] { json j = json::parse(s); stats st; walk(j, st); g_sink = st.num; }},
}});
{
const std::string name = dc.name;
workloads.push_back({"select", {
{"json_view", [&, name] { auto d = json_document::parse(s); g_sink = pick(name, d.root()); }},
{"json_view (reused)", [&, name] { reused.read(s); g_sink = pick(name, reused.root()); }},
{"yyjson", [&, name] { yyjson_doc* d = yyjson_read(s.data(), s.size(), 0); g_sink = pick(name, yyjson_doc_get_root(d)); yyjson_doc_free(d); }},
{"simdjson OD", [&, name] { auto d = od.iterate(ps).value_unsafe(); g_sink = pick_od(name, d); }},
#if !BENCH_NO_GLAZE
{"Glaze structs", [&, name] { g_sink = pick_glaze_structs(name, s); }},
{"Glaze lazy", [&, name] { auto r = glz::lazy_json(s); g_sink = pick_glaze_lazy(name, *r); }},
#endif
}});
}
workloads.push_back({"parse+dump", {
{"json_view", [&] { auto d = json_document::parse(s); std::string o = d.root().dump(); g_sink = static_cast<double>(o.size()); }},
{"yyjson", [&] { yyjson_doc* d = yyjson_read(s.data(), s.size(), 0); std::size_t n = 0; char* o = yyjson_write(d, 0, &n); g_sink = static_cast<double>(n); std::free(o); yyjson_doc_free(d); }},
{"simdjson DOM", [&] { std::string o = simdjson::to_string(sj.parse(ps).value_unsafe()); g_sink = static_cast<double>(o.size()); }},
{"RapidJSON", [&] { rapidjson::Document d; d.Parse<rj_flags>(s.data(), s.size()); rapidjson::StringBuffer sb; rapidjson::Writer<rapidjson::StringBuffer> w(sb); d.Accept(w); g_sink = static_cast<double>(sb.GetSize()); }},
{"sonic-cpp", [&] { sonic_json::Document d; d.Parse(s.data(), s.size()); sonic_json::WriteBuffer wb; d.Serialize(wb); g_sink = static_cast<double>(wb.Size()); }},
#if !BENCH_NO_GLAZE
{"Glaze generic", [&] { glz::generic_u64 j; (void)glz::read_json(j, s); std::string o; (void)glz::write_json(j, o); g_sink = static_cast<double>(o.size()); }},
#endif
{"Boost.JSON", [&] { boost::json::monotonic_resource mr; auto v = boost::json::parse(s, &mr); std::string o = boost::json::serialize(v); g_sink = static_cast<double>(o.size()); }},
{"json::parse", [&] { json j = json::parse(s); std::string o = j.dump(); g_sink = static_cast<double>(o.size()); }},
}});
for (auto& wl : workloads)
{
std::vector<double> best(wl.second.size(), 1e300);
const int r = s.size() > 10000000 ? std::max(3, rounds / 5) : rounds;
for (int i = 0; i < r; ++i)
{
for (std::size_t k = 0; k < wl.second.size(); ++k)
{
wl.second[k].fn(); // untimed: the previous engine's leftovers are cleaned up here
const auto t0 = std::chrono::steady_clock::now();
for (int b = 0; b < dc.batch; ++b) wl.second[k].fn();
best[k] = std::min(best[k], std::chrono::duration<double, std::nano>(std::chrono::steady_clock::now() - t0).count() / dc.batch);
}
}
std::printf("%-13s %-11s", dc.name.c_str(), wl.first.c_str());
for (std::size_t k = 0; k < wl.second.size(); ++k)
std::printf(" %s %.1fus (%.2fx)", wl.second[k].name.c_str(), best[k] / 1e3, best[k] / best[0]);
std::printf("\n");
std::fflush(stdout);
}
}
}
+626
View File
@@ -0,0 +1,626 @@
# This file is automatically @generated by Cargo.
# It is not intended for manual editing.
version = 4
[[package]]
name = "ahash"
version = "0.8.12"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5a15f179cd60c4584b8a8c596927aadc462e27f2ca70c04e0071964a73ba7a75"
dependencies = [
"cfg-if",
"getrandom",
"once_cell",
"version_check",
"zerocopy",
]
[[package]]
name = "allocator-api2"
version = "0.2.21"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "683d7910e743518b0e34f1186f92494becacb047c7b6bf616c96772180fef923"
[[package]]
name = "autocfg"
version = "1.5.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f2032f911046de80f0a198e0901378627c33f59ea0ac00e363d481118bd70a53"
[[package]]
name = "bench_rs"
version = "0.1.0"
dependencies = [
"serde_json",
"simd-json",
"sonic-rs",
]
[[package]]
name = "bumpalo"
version = "3.20.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "72f5acc6cb2ba439de613abc23857ec3d78374d8ed5ac84e9d11336e87da8649"
[[package]]
name = "bytes"
version = "1.12.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fc652a48c352aef3ea3aed32080501cf3ef6ed5da78602a020c991775b0aff04"
[[package]]
name = "cfg-if"
version = "1.0.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4e7648175b45a9a48536d676f68d918270699102aa8dab5496df06904c914600"
[[package]]
name = "equivalent"
version = "1.0.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "00d174d5400e5e8fd687ad1049e2f578285fa914201b1af7e8b112a4546bd826"
[[package]]
name = "faststr"
version = "0.2.34"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1ca7d44d22004409a61c393afb3369c8f7bb74abcae49fe249ee01dcc3002113"
dependencies = [
"bytes",
"rkyv",
"serde",
"simdutf8",
]
[[package]]
name = "float-cmp"
version = "0.10.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b09cf3155332e944990140d967ff5eceb70df778b34f77d8075db46e4704e6d8"
dependencies = [
"num-traits",
]
[[package]]
name = "foldhash"
version = "0.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "77ce24cb58228fbb8aa041425bb1050850ac19177686ea6e0f41a70416f56fdb"
[[package]]
name = "futures-core"
version = "0.3.34"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "92d699e522242e69e3003b94ecc1f960f3a5e015aa7c5d7486e65ad01dd94f5e"
[[package]]
name = "futures-task"
version = "0.3.34"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cd417de3d1d015fc3bfd2b1ea46dfc7bab72ef86f1cc7cc9c78e728b34a6d1fd"
[[package]]
name = "futures-util"
version = "0.3.34"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0d50a92467f8ba5dd6e3ee5d4bd04d73ab2e4e1c44474a0674821dfce14b79bc"
dependencies = [
"futures-core",
"futures-task",
"pin-project-lite",
"slab",
]
[[package]]
name = "getrandom"
version = "0.3.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "899def5c37c4fd7b2664648c28120ecec138e4d395b459e5ca34f9cce2dd77fd"
dependencies = [
"cfg-if",
"libc",
"r-efi",
"wasip2",
]
[[package]]
name = "halfbrown"
version = "0.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0c7ed2f2edad8a14c8186b847909a41fbb9c3eafa44f88bd891114ed5019da09"
dependencies = [
"hashbrown 0.16.1",
"serde",
]
[[package]]
name = "hashbrown"
version = "0.16.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "841d1cc9bed7f9236f321df977030373f4a4163ae1a7dbfe1a51a2c1a51d9100"
dependencies = [
"allocator-api2",
"equivalent",
"foldhash",
]
[[package]]
name = "hashbrown"
version = "0.17.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ed5909b6e89a2db4456e54cd5f673791d7eca6732202bbf2a9cc504fe2f9b84a"
[[package]]
name = "indexmap"
version = "2.14.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cc4e190f5d26ca7051642629da2c52fc03bde85a03197c99408dcd291734c855"
dependencies = [
"equivalent",
"hashbrown 0.17.1",
]
[[package]]
name = "itoa"
version = "1.0.18"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682"
[[package]]
name = "js-sys"
version = "0.3.106"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7883d941dae510fb2d978fc3fe018c71c9e2892fd38854de3e8b92c2e5ad9cc5"
dependencies = [
"cfg-if",
"futures-util",
"wasm-bindgen",
]
[[package]]
name = "libc"
version = "0.2.191"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "65cd87c9a6d60536580b316ad985726104730a5151c99845f2490428ef91240a"
[[package]]
name = "memchr"
version = "2.8.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cf8baf1c55e62ffcace7a9f06f4bd9cd3f0c4beb022d3b367256b91b87513d98"
[[package]]
name = "munge"
version = "0.4.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5e17401f259eba956ca16491461b6e8f72913a0a114e39736ce404410f915a0c"
dependencies = [
"munge_macro",
]
[[package]]
name = "munge_macro"
version = "0.4.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4568f25ccbd45ab5d5603dc34318c1ec56b117531781260002151b8530a9f931"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.119",
]
[[package]]
name = "num-traits"
version = "0.2.19"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841"
dependencies = [
"autocfg",
]
[[package]]
name = "once_cell"
version = "1.21.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50"
[[package]]
name = "pin-project-lite"
version = "0.2.17"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a89322df9ebe1c1578d689c92318e070967d1042b512afbe49518723f4e6d5cd"
[[package]]
name = "proc-macro2"
version = "1.0.107"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "985e7ec9bb745e6ce6535b544d84d6cd6f7ad8bd711c398938ae983b91a766d9"
dependencies = [
"unicode-ident",
]
[[package]]
name = "ptr_meta"
version = "0.3.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "743da816b98c921cdbe8628ef7381b76f25ecf4da599fc80aca90eae7ef70cc0"
dependencies = [
"ptr_meta_derive",
]
[[package]]
name = "ptr_meta_derive"
version = "0.3.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1c8d9ca532f185d5d4db7a7c9d51420b452168ea1c2b913953281bd6fe1fcbd0"
dependencies = [
"proc-macro2",
"quote",
"syn 3.0.9",
]
[[package]]
name = "quote"
version = "1.0.47"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1fbf4db142a473a8d80c26bbf18454ed458bf8d26c8219c331daecfdbd079001"
dependencies = [
"proc-macro2",
]
[[package]]
name = "r-efi"
version = "5.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "69cdb34c158ceb288df11e18b4bd39de994f6657d83847bdffdbd7f346754b0f"
[[package]]
name = "rancor"
version = "0.1.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9b534442d0fcdb55d66f373d9cac6d33b6293a2335bc2136dbd06ce0e87d2572"
dependencies = [
"ptr_meta",
]
[[package]]
name = "ref-cast"
version = "1.0.27"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7e440fb4e4b4147295338efb76001ab9e4efc0e5839df2c47fc5ac2381d365c3"
dependencies = [
"ref-cast-impl",
]
[[package]]
name = "ref-cast-impl"
version = "1.0.27"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "92ecd8964f8453721699a1ed72037b0db49ce2f5a5138486ee89bed6f67cdf3a"
dependencies = [
"proc-macro2",
"quote",
"syn 3.0.9",
]
[[package]]
name = "rend"
version = "0.5.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "663ba70707f96e871406fe10d68128412e619b06d1d47cb91c3a4c6501176240"
[[package]]
name = "rkyv"
version = "0.8.18"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d9776093b7ca170454ab1406954f7b7d97a57c51dc6c0642957fb2ef25c2d399"
dependencies = [
"bytes",
"hashbrown 0.17.1",
"indexmap",
"munge",
"ptr_meta",
"rancor",
"rend",
"rkyv_derive",
"tinyvec",
"uuid",
]
[[package]]
name = "rkyv_derive"
version = "0.8.18"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1c25ef604ac7dd839d44d64648952ea23c97866f124ff671b0ed2cf3ad9bb06e"
dependencies = [
"proc-macro2",
"quote",
"syn 3.0.9",
]
[[package]]
name = "rustversion"
version = "1.0.23"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cf54715a573b99ac80df0bc206da022bcd442c974952c7b9720069370852e21f"
[[package]]
name = "ryu"
version = "1.0.23"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9774ba4a74de5f7b1c1451ed6cd5285a32eddb5cccb8cc655a4e50009e06477f"
[[package]]
name = "serde"
version = "1.0.229"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4148590afebada386688f18773da617792bf2ef03ffc1e4cbd2b1d45b023e0ba"
dependencies = [
"serde_core",
"serde_derive",
]
[[package]]
name = "serde_core"
version = "1.0.229"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "67dca2c9c51e58a4791a4b1ed58308b39c64224d349a935ab5039aa360942a48"
dependencies = [
"serde_derive",
]
[[package]]
name = "serde_derive"
version = "1.0.229"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e7a5d71263a5a7d47b41f6b3f06ba276f10cc18b0931f1799f710578e2309348"
dependencies = [
"proc-macro2",
"quote",
"syn 3.0.9",
]
[[package]]
name = "serde_json"
version = "1.0.154"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e7e9cc8b1b85264074fbcc02a88680c4096b1e47df8f739dceb03bf482f04bd6"
dependencies = [
"itoa",
"memchr",
"serde",
"serde_core",
"zmij",
]
[[package]]
name = "simd-json"
version = "0.19.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e8f2e4d523646f7da6dde3983bd18ede0babc21c258ab708b3dd93ea0259719d"
dependencies = [
"halfbrown",
"serde",
"serde_json",
"simdutf8",
"value-trait",
]
[[package]]
name = "simdutf8"
version = "0.1.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e3a9fe34e3e7a50316060351f37187a3f546bce95496156754b601a5fa71b76e"
[[package]]
name = "slab"
version = "0.4.12"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0c790de23124f9ab44544d7ac05d60440adc586479ce501c1d6d7da3cd8c9cf5"
[[package]]
name = "sonic-number"
version = "0.1.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d7c350a05f295f922fc153a3af8b443c10db70904a9291a624ddb6058c6110d0"
dependencies = [
"cfg-if",
]
[[package]]
name = "sonic-rs"
version = "0.5.10"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "756586caeeb21126a57c17d1ef2b9386e2613ba784aae0f27104f5e9982335ec"
dependencies = [
"ahash",
"bumpalo",
"bytes",
"cfg-if",
"faststr",
"itoa",
"ref-cast",
"serde",
"simdutf8",
"sonic-number",
"sonic-simd",
"thiserror",
"zmij",
]
[[package]]
name = "sonic-simd"
version = "0.1.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f99e664ecd2d85a68c87e3c7a3cfe691f647ea9e835de984aba4d54a41f817d4"
dependencies = [
"cfg-if",
]
[[package]]
name = "syn"
version = "2.0.119"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "872831b642d1a07999a962a351ed35b955ea2cfc8f3862091e2a240a84f17297"
dependencies = [
"proc-macro2",
"quote",
"unicode-ident",
]
[[package]]
name = "syn"
version = "3.0.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d78c8dee4c7bf0e14673097256fed6142ce9d3b85a408189d07482442145823b"
dependencies = [
"proc-macro2",
"quote",
"unicode-ident",
]
[[package]]
name = "thiserror"
version = "2.0.21"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "09e52cb86a36cede5cb101bf8908837b3e4c6e5e59fe7fd85c23fb56200d189e"
dependencies = [
"thiserror-impl",
]
[[package]]
name = "thiserror-impl"
version = "2.0.21"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fe5197923287db20a58125f0bc85c062f7f2c892de97b18c356f9efb14b28524"
dependencies = [
"proc-macro2",
"quote",
"syn 3.0.9",
]
[[package]]
name = "tinyvec"
version = "1.13.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fd3ca314f692efd6c868f8408f53fe444634a845f96c028b97d35f6a1f79f0ee"
[[package]]
name = "unicode-ident"
version = "1.0.27"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a2c754d6c33795a1c324727428e5a7dedb5b06195f9890bdbcba760d3e246563"
[[package]]
name = "uuid"
version = "1.29.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4a0e2d11620c50695fe12bbbfde96b3477fe606e7284c22d505c12ce51214514"
dependencies = [
"js-sys",
"wasm-bindgen",
]
[[package]]
name = "value-trait"
version = "0.12.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "545a3d7089f11f1741d12963c057adf9443fc24325110a6a6daf582e825fd51b"
dependencies = [
"float-cmp",
"halfbrown",
"itoa",
"ryu",
]
[[package]]
name = "version_check"
version = "0.9.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a"
[[package]]
name = "wasip2"
version = "1.0.4+wasi-0.2.12"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b67efb37e106e55ce722a510d6b5f9c17f083e5fc79afc2badeb12cc313d9487"
dependencies = [
"wit-bindgen",
]
[[package]]
name = "wasm-bindgen"
version = "0.2.129"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9bb54f33acc68fd454578d9820b0bde1a1a3d17aa17bb7b6595806d02886d409"
dependencies = [
"cfg-if",
"once_cell",
"rustversion",
"wasm-bindgen-macro",
"wasm-bindgen-shared",
]
[[package]]
name = "wasm-bindgen-macro"
version = "0.2.129"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2e29d0c35b16e224a7eeb5cd2d25e3e1968fbd65604117b44d3b789d00ee8535"
dependencies = [
"quote",
"wasm-bindgen-macro-support",
]
[[package]]
name = "wasm-bindgen-macro-support"
version = "0.2.129"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6f501a8bc3719dba86ef8ae4728879c08001bea749eb1333ac5b91e040e2a6b7"
dependencies = [
"bumpalo",
"proc-macro2",
"quote",
"syn 3.0.9",
"wasm-bindgen-shared",
]
[[package]]
name = "wasm-bindgen-shared"
version = "0.2.129"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "23f0c9c52aa7cd7d77769a4cfe2a9adb1b331f489a41d912ce14513d5ab995c6"
dependencies = [
"unicode-ident",
]
[[package]]
name = "wit-bindgen"
version = "0.57.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1ebf944e87a7c253233ad6766e082e3cd714b5d03812acc24c318f549614536e"
[[package]]
name = "zerocopy"
version = "0.8.63"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e5fe1f8f1b06191a00962174c61aa5005e0bb391a6d80d07e24d115c01a92ed8"
dependencies = [
"zerocopy-derive",
]
[[package]]
name = "zerocopy-derive"
version = "0.8.63"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "863ad3ac83293fb4d740aedbfdc9240dd8d1a50c1099acd76ce80ce7c7230c7f"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.119",
]
[[package]]
name = "zmij"
version = "1.0.23"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "29666d0abbfad1e3dc4dcf6144730dd3a3ab225bbbdac83319345b1b44ccfc1b"
@@ -0,0 +1,14 @@
[package]
name = "bench_rs"
version = "0.1.0"
edition = "2021"
[dependencies]
simd-json = "*"
sonic-rs = "*"
serde_json = { version = "*", features = ["float_roundtrip"] }
[profile.release]
opt-level = 3
lto = "fat"
codegen-units = 1
@@ -0,0 +1,172 @@
// __ _____ _____ _____
// __| | __| | | | 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
// Same workloads and method as bench_all.cpp: parse (build + free), traverse (parse, then visit every
// value, convert every number, touch every string and key), parse+dump; every engine interleaved, an
// untimed call before each timed one, best round reported.
// simd-json parses in place, so its timings include copying the input into a mutable buffer.
use simd_json::prelude::*;
use std::time::Instant;
#[derive(Default, Debug, Clone, Copy)]
struct Stats {
num: f64,
str_: usize,
nodes: usize,
}
fn walk_simd(v: &simd_json::BorrowedValue, st: &mut Stats) {
st.nodes += 1;
if let Some(o) = v.as_object() {
for (k, x) in o.iter() {
st.str_ += k.len();
walk_simd(x, st);
}
} else if let Some(a) = v.as_array() {
for x in a.iter() {
walk_simd(x, st);
}
} else if let Some(s) = v.as_str() {
st.str_ += s.len();
} else if v.is_u64() {
st.num += v.as_u64().unwrap() as f64;
} else if v.is_i64() {
st.num += v.as_i64().unwrap() as f64;
} else if v.is_f64() {
st.num += v.as_f64().unwrap();
}
}
fn walk_sonic(v: &sonic_rs::Value, st: &mut Stats) {
use sonic_rs::{JsonContainerTrait, JsonValueTrait};
st.nodes += 1;
if let Some(o) = v.as_object() {
for (k, x) in o.iter() {
st.str_ += k.len();
walk_sonic(x, st);
}
} else if let Some(a) = v.as_array() {
for x in a.iter() {
walk_sonic(x, st);
}
} else if let Some(s) = v.as_str() {
st.str_ += s.len();
} else if v.is_number() {
if let Some(u) = v.as_u64() {
st.num += u as f64;
} else if let Some(i) = v.as_i64() {
st.num += i as f64;
} else {
st.num += v.as_f64().unwrap();
}
}
}
fn walk_serde(v: &serde_json::Value, st: &mut Stats) {
st.nodes += 1;
match v {
serde_json::Value::Object(o) => {
for (k, x) in o.iter() {
st.str_ += k.len();
walk_serde(x, st);
}
}
serde_json::Value::Array(a) => {
for x in a.iter() {
walk_serde(x, st);
}
}
serde_json::Value::String(s) => st.str_ += s.len(),
serde_json::Value::Number(n) => {
if let Some(u) = n.as_u64() {
st.num += u as f64;
} else if let Some(i) = n.as_i64() {
st.num += i as f64;
} else {
st.num += n.as_f64().unwrap();
}
}
_ => {}
}
}
fn same(a: &Stats, b: &Stats) -> bool {
a.nodes == b.nodes && a.str_ == b.str_ && (a.num - b.num).abs() <= 1e-9 * a.num.abs()
}
static mut SINK: f64 = 0.0;
fn sink(x: f64) {
unsafe { std::ptr::write_volatile(std::ptr::addr_of_mut!(SINK), x) }
}
fn main() {
let args: Vec<String> = std::env::args().collect();
let t = format!("{}/", args[1]);
let rounds: usize = args.get(2).map(|r| r.parse().unwrap()).unwrap_or(30);
let mut docs: Vec<(String, String, usize)> = Vec::new();
for f in ["nativejson-benchmark/twitter.json", "nativejson-benchmark/citm_catalog.json", "nativejson-benchmark/canada.json", "jeopardy/jeopardy.json"] {
let name = f.split('/').nth(1).unwrap().trim_end_matches(".json").to_string();
docs.push((name, std::fs::read_to_string(format!("{}{}", t, f)).unwrap(), 1));
}
let tw: serde_json::Value = serde_json::from_str(&docs[0].1).unwrap();
docs.push(("status".to_string(), serde_json::to_string(&tw["statuses"][0]).unwrap(), 200));
for (name, text, _) in &docs {
let mut r = Stats::default();
walk_serde(&serde_json::from_str(text).unwrap(), &mut r);
let mut a = Stats::default();
let mut buf = text.as_bytes().to_vec();
walk_simd(&simd_json::to_borrowed_value(&mut buf).unwrap(), &mut a);
let mut b = Stats::default();
walk_sonic(&sonic_rs::from_str(text).unwrap(), &mut b);
// sonic-rs keeps document order: its sum is compared bit for bit by the caller with json_view's
println!("check {:<13} simd-json:{} sonic-rs:{} serde_json:OK (nodes {} str {} num {:.17e} sonic-num {:.17e})",
name, if same(&r, &a) { "OK" } else { "MISMATCH" }, if same(&r, &b) { "OK" } else { "MISMATCH" }, r.nodes, r.str_, r.num, b.num);
}
for (name, s, batch) in &docs {
type Eng<'a> = (&'static str, Box<dyn Fn() + 'a>);
let workloads: Vec<(&str, Vec<Eng>)> = vec![
("parse", vec![
("simd-json", Box::new(|| { let mut b = s.as_bytes().to_vec(); let v = simd_json::to_borrowed_value(&mut b).unwrap(); sink(v.is_object() as u8 as f64); })),
("sonic-rs", Box::new(|| { let v: sonic_rs::Value = sonic_rs::from_str(s).unwrap(); sink(sonic_rs::JsonValueTrait::is_object(&v) as u8 as f64); })),
("serde_json", Box::new(|| { let v: serde_json::Value = serde_json::from_str(s).unwrap(); sink(v.is_object() as u8 as f64); })),
]),
("traverse", vec![
("simd-json", Box::new(|| { let mut b = s.as_bytes().to_vec(); let v = simd_json::to_borrowed_value(&mut b).unwrap(); let mut st = Stats::default(); walk_simd(&v, &mut st); sink(st.num); })),
("sonic-rs", Box::new(|| { let v: sonic_rs::Value = sonic_rs::from_str(s).unwrap(); let mut st = Stats::default(); walk_sonic(&v, &mut st); sink(st.num); })),
("serde_json", Box::new(|| { let v: serde_json::Value = serde_json::from_str(s).unwrap(); let mut st = Stats::default(); walk_serde(&v, &mut st); sink(st.num); })),
]),
("parse+dump", vec![
("simd-json", Box::new(|| { let mut b = s.as_bytes().to_vec(); let v = simd_json::to_borrowed_value(&mut b).unwrap(); let o = simd_json::to_string(&v).unwrap(); sink(o.len() as f64); })),
("sonic-rs", Box::new(|| { let v: sonic_rs::Value = sonic_rs::from_str(s).unwrap(); let o = sonic_rs::to_string(&v).unwrap(); sink(o.len() as f64); })),
("serde_json", Box::new(|| { let v: serde_json::Value = serde_json::from_str(s).unwrap(); let o = serde_json::to_string(&v).unwrap(); sink(o.len() as f64); })),
]),
];
for (wl, engines) in &workloads {
let r = if s.len() > 10_000_000 { std::cmp::max(3, rounds / 5) } else { rounds };
let mut best = vec![f64::MAX; engines.len()];
for _ in 0..r {
for (k, (_, f)) in engines.iter().enumerate() {
f();
let t0 = Instant::now();
for _ in 0..*batch {
f();
}
let ns = t0.elapsed().as_nanos() as f64 / *batch as f64;
best[k] = best[k].min(ns);
}
}
print!("{:<13} {:<11}", name, wl);
for (k, (n, _)) in engines.iter().enumerate() {
print!(" {} {:.1}us", n, best[k] / 1e3);
}
println!();
}
}
}
+71
View File
@@ -0,0 +1,71 @@
#!/usr/bin/env python3
# __ _____ _____ _____
# __| | __| | | | 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
"""Summarize runs of bench_all and bench_rs: for every document and workload, the engines ordered by time, and the
fastest other engine relative to json_document (above 1: json_document is ahead).
Usage: summarize.py LOG... [--rust LOG...]
Several logs of the same program are combined by taking each cell's best time. Fresh documents are compared with
fresh ones: "json_view (reused)" and "simdjson DOM" (a reused parser) are left out of the "fastest other" column
and compared with each other instead.
"""
import re
import sys
REUSED = {"json_view (reused)", "simdjson DOM"}
DOCS = ["twitter", "citm_catalog", "canada", "jeopardy", "status"]
WORKLOADS = ["parse", "traverse", "select", "parse+dump"]
def parse(path, suffix=""):
cells = {}
for line in open(path):
if line.startswith(("check", "info", "WARNING")):
continue
m = re.match(r"(\S+)\s+(\S+)\s+(.*)", line)
if not m:
continue
for engine, us in re.findall(r"\s*(.+?) ([\d.]+)us(?: \([\d.]+x\))?", m[3]):
cells[(m[1], m[2], engine.strip() + suffix)] = float(us)
return cells
def best(paths, suffix=""):
runs = [parse(p, suffix) for p in paths]
keys = set(runs[0]).intersection(*runs[1:])
return {k: min(r[k] for r in runs) for k in keys}
def main():
args = sys.argv[1:]
cpp = args[:args.index("--rust")] if "--rust" in args else args
rust = args[args.index("--rust") + 1:] if "--rust" in args else []
cells = best(cpp)
if rust:
cells.update(best(rust, " (Rust)"))
for wl in WORKLOADS:
print("\n== " + wl)
for doc in DOCS:
cell = {e: v for (d, w, e), v in cells.items() if d == doc and w == wl}
if "json_view" not in cell:
continue
jv = cell["json_view"]
order = sorted(cell.items(), key=lambda x: x[1])
fresh = {e: v for e, v in cell.items() if e not in REUSED and e != "json_view"}
other, t = min(fresh.items(), key=lambda x: x[1])
line = "%-13s json_view %10.1f us fastest other: %s %.2f" % (doc, jv, other, t / jv)
if "json_view (reused)" in cell and "simdjson DOM" in cell:
line += " | reused: simdjson DOM %.2f" % (cell["simdjson DOM"] / cell["json_view (reused)"])
print(line)
print(" " + ", ".join("%s %.2f" % (e, v / jv) for e, v in order))
if __name__ == "__main__":
main()