- a dump workload in bench_all and bench_rs: each engine writes a document parsed beforehand, and every output must read back with bit-identical numbers; an optional argument selects one workload - the post's comparison gains a write table, a throughput chart, a dump row for all three configurations, and the x86-64 cases it loses - the thanks section is split into adapted code and borrowed ideas 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 |
dump |
write a document parsed beforehand as compact JSON; only the writing is timed |
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.
For dump, every engine's output must read back as the same values with the same sum, so every float is written
exactly.
| 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. A fourth
argument times only one workload (./bench_all <json_test_data> 30 "" dump); bench_rs takes the workload as third
argument. 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.