Files
json/tests/benchmarks/json_view/others
Niels Lohmann 7341f127d3 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>
2026-10-30 15:29:46 +01:00
..

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.