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>
json_document against other JSON libraries
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. 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):
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.