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

@@ -8,9 +8,10 @@ Since 2016, the README of this library has said this about speed:
That sentence is still true, but the gap is a lot smaller now. Swap the 3.12.0 header for the 3.13.0 one and recompile,
and the 182 benchmarks of the library's suite run on average 2.9× as fast, without a line of your code changing.
Parsing the 1.7 MB `citm_catalog.json` takes 1.75 ms instead of 7.20 ms, and `dump()` of documents made of objects
and strings is 4.7 to 6.7× as fast. The new read-only [`json_document`](../../features/json_view.md) parses the
usual test files faster than yyjson and simdjson's DOM on Apple silicon (with a caveat for float-heavy files that we
explain below).
and strings is 4.7 to 6.7× as fast. The new read-only [`json_document`](../../features/json_view.md) parses and
writes faster than the other document parsers we measured on Apple silicon, including yyjson, simdjson and
sonic-rs, and ties with simdjson on x86-64. It is not the fastest everywhere; [section 8](#8-build-a-different-data-structure)
says where it loses.
This post is about *how*. None of the techniques is new: almost every one of them is borrowed from simdjson, yyjson,
fast_float, Żmij, serde_json, SQLite, Boost.JSON or a textbook, and we name the source at each step. What we think is
@@ -552,42 +553,77 @@ entries in document order instead of trees of separate allocations, and yyjson's
returning the last one meant scanning every object to its end and made lookups in small objects 1.6 to 3.4 times
slower ([#5622][5622]).
Parse times in µs on the M1 Max (C++17, `-O3`, best of 30 interleaved rounds; a fresh `json_document` and yyjson
document each time, simdjson's parser reused as its documentation recommends):
### How it compares
| file | `json_document` | yyjson 0.13.0 | simdjson 4.6.11 DOM | Boost.JSON 1.92.0 | `json::parse` |
| --- | --: | --: | --: | --: | --: |
| `twitter.json` (632 KB) | **182** | 192 | 216 | 866 | 1,368 |
| `citm_catalog.json` (1.7 MB) | **360** | 478 | 490 | 1,480 | 2,409 |
| `canada.json` (2.3 MB) | **960** | 1,489 | 2,098 | 2,501 | 5,890 |
| `jeopardy.json` (56 MB) | **22,687** | 30,528 | 26,875 | 88,900 | 167,664 |
We compared `json_document` with eleven other engines. In C++: yyjson 0.13.0, simdjson 4.6.11 (its DOM and its
On-Demand API), sonic-cpp 1.0.2, RapidJSON (master, 24b5e7a), Glaze 9.0.0 (its `generic_u64` DOM, its `lazy_json`,
and reading straight into structs), and Boost.JSON (1.92 on the Mac, 1.83 on x86-64). In Rust: sonic-rs 0.5.10, simd-json 0.19.0 and serde_json
1.0.154. Plus `json::parse`. Before anything is timed, every engine must report the same number of values, the same
number of string bytes, and a bit-identical sum of all numbers in document order. That means every engine converts
every number exactly; RapidJSON guarantees that only with `kParseFullPrecisionFlag`, so that is what it runs with
(with its default flags it happened to produce the same values for these files, and those rows are in the data).
The workloads: *parse* (build the document and free it), *traverse* (parse, then visit every value and convert every
number), *select* (parse, then read a few fields of every record) and *parse+dump*. We ran them on the M1 Max with
Apple clang 21 (and Rust 1.99), and on the x86-64 Haswell server with GCC 13 and Clang 18 (`-march=haswell`, one
pinned core). That server has other work running, so differences of a few percent there are noise. Glaze 9 could
not be built with Clang 18 on it (Clang 18 can't use GCC 13's `std::expected`), and the Rust engines ran on the Mac
only.
Parse times in µs on the M1 Max, a fresh document each time (C++23 and Rust `--release`, `-O3`, best of 30
interleaved rounds):
| engine | `twitter.json` (632 KB) | `citm_catalog.json` (1.7 MB) | `canada.json` (2.3 MB) | `jeopardy.json` (56 MB) |
| --- | --: | --: | --: | --: |
| `json_document` | **183** | **364** | **943** | **22,681** |
| yyjson | 194 | 478 | 1,489 | 30,342 |
| simdjson DOM | 202 | 486 | 2,097 | 26,904 |
| sonic-rs (Rust) | 302 | 664 | 1,907 | 30,202 |
| sonic-cpp | 379 | 986 | 2,580 | 40,804 |
| simd-json (Rust, input copy included) | 567 | 1,616 | 4,939 | 59,364 |
| Boost.JSON | 868 | 1,483 | 2,481 | 88,925 |
| RapidJSON | 1,183 | 1,613 | 7,358 | 143,044 |
| serde_json (Rust) | 1,202 | 2,337 | 6,836 | 139,829 |
| Glaze `generic_u64` | 1,268 | 2,727 | 9,262 | 132,273 |
| `json::parse` | 1,352 | 2,386 | 5,915 | 163,750 |
<div class="nj-fig-wrap" markdown>
--8<-- "images/perf_blog_view.svg"
</div>
On the same four files, reading every value takes yyjson 1.06 to 1.30 times as long as `json_document`, writing
them back with `dump()` 1.06 to 1.35 times, and parsing, editing a few values and writing them 1.34 to 1.85 times (1.0 to 1.57 times for an edit in every
record). Integers are
already converted when you read them, and the writer copies strings and integer tokens straight from the source
([#5633][5633]).
The fastest *other* engine for each workload, as its time divided by `json_document`'s, over the four files above:
above 1, `json_document` is ahead; below 1, it is behind.
It is not faster everywhere, and the harness ([#5627][5627], [#5914][5914]) is built to show where:
| workload | M1 Max, Apple clang 21 | x86-64, GCC 13 | x86-64, Clang 18 |
| --- | --- | --- | --- |
| parse | 1.06–1.58 (yyjson, simdjson DOM) | 0.97–1.01 (simdjson DOM) on `twitter`, `citm_catalog`; 1.89 on `canada`; **0.71** (sonic-cpp) on `jeopardy` | 0.96–0.97 (simdjson DOM) on `twitter`, `citm_catalog`; 1.21–1.56 on `canada`, `jeopardy` |
| traverse | 1.04–1.18 (yyjson, simdjson On-Demand) | **0.58–0.86** (simdjson On-Demand) | **0.86**–1.20 (sonic-cpp, simdjson On-Demand, yyjson) |
| select | **0.34–0.84** (Glaze structs, simdjson On-Demand) | **0.26–0.65** | **0.33–0.86** |
| parse+dump | 1.18–1.35 (yyjson) | 1.20–1.41 (yyjson, sonic-cpp) | 1.16–1.27 (yyjson, sonic-cpp) |
- **Reading a few fields.** simdjson's On-Demand API builds nothing and can only read forward. Picking a few fields
out of `twitter.json`, `canada.json` and `jeopardy.json` takes it 0.67 to 0.80 times as long as `json_document`.
- **Floats are converted later.** `json_document` converts a float when you read it; yyjson and simdjson convert
every number while parsing. For `canada.json`, which is almost all floats, the parse row therefore flatters
`json_document` (1.55× ahead of yyjson); reading every value after parsing is the fair comparison, and there it
is 1.07× ahead.
- **Other CPUs.** On the x86-64 machine with GCC, simdjson's DOM with AVX2 was 19 % ahead on parsing
`twitter.json` and 7 % on `citm_catalog.json` ([#5627][5627]), and yyjson's `dump()` of `canada.json` still is
ahead: `json_document` takes 1.4 times as long there ([#5908][5908]). Our tables are from one Apple silicon
machine.
- **Small documents.** For a 2.5 KB status message, all three engines parse in about a microsecond, and yyjson is
6 % ahead. For a 94-byte message, the engines trade places between runs.
- **`json::parse` itself.** Boost.JSON builds a mutable tree, as `json` does, so it is the fair comparison for
`json::parse`, not for `json_document`. It is still 1.6 to 2.4 times as fast.
So, is it the fastest JSON parser? Not in general. Among the parsers that build a document you can read in any
order, it is the fastest at parsing and writing back in every configuration we ran, and the fastest at parsing on
Apple silicon. On x86-64 it ties with simdjson's DOM on typical documents. Where it loses:
- **Reading a few known fields.** simdjson's On-Demand API builds nothing and reads forward only; Glaze reading into
structs that declare just the needed fields skips everything else without building it. Both are faster for
*select*, Glaze by up to 3.8× on `canada.json`, where it skips the coordinates.
- **One pass over everything on x86-64 with GCC.** simdjson On-Demand visits every value in 0.58 to 0.86 of the time.
- **Fresh large documents on Linux.** A fresh 56 MB `jeopardy.json` pays for the page faults of a new index of
3.25 million 16-byte nodes (52 MB). With GCC, sonic-cpp parses it in 0.71 of the time. Reusing the document (`read()`) halves `json_document`'s time
there, and then it is ahead of a reused simdjson parser (0.53 against 0.61 of the fresh time).
- **Float-heavy documents, measured fairly.** `json_document` converts a float when you read it; the others convert
while parsing. The `canada.json` parse row flatters it; *traverse* is the fair comparison, and there it is
1.04× ahead on the Mac and behind sonic-cpp with Clang on x86-64 (0.88).
- **Small documents.** On a 2.5 KB message, all document parsers take about a microsecond, and yyjson and
simdjson's DOM are as fast or a few percent faster.
And `json::parse`? Boost.JSON builds a mutable tree as `json` does, so it is the fair comparison. It is 1.6 to 2.4
times as fast on the Mac, and 2.4 to 4.5 times on the x86-64 server.
The programs are in
[`tests/benchmarks/json_view/others`](https://github.com/nlohmann/json/tree/develop/tests/benchmarks/json_view/others),
with the pinned versions and checksums of every library.
## What didn't work
+92 -72
View File
@@ -1,6 +1,6 @@
<svg class="nj-fig nj-charts nj-charts--blog" viewBox="0 0 580 404" role="img" aria-labelledby="nj-blog-view nj-blog-view-d" xmlns="http://www.w3.org/2000/svg">
<title id="nj-blog-view">Parse throughput of json_document, yyjson, simdjson&#x27;s D&#8203;OM, Boost.J&#8203;SON and json::parse, Apple M1 Max</title>
<desc id="nj-blog-view-d">Grouped horizontal bars in GB/s. twitter.json json_document 3.48 GB/s; twitter.json yyjson 3.29 GB/s; twitter.json simdjson D&#8203;OM 2.92 GB/s; twitter.json Boost.J&#8203;SON 0.73 GB/s; twitter.json json::parse 0.46 GB/s; citm_catalog.json json_document 4.80 GB/s; citm_catalog.json yyjson 3.61 GB/s; citm_catalog.json simdjson D&#8203;OM 3.52 GB/s; citm_catalog.json Boost.J&#8203;SON 1.17 GB/s; citm_catalog.json json::parse 0.72 GB/s; canada.json json_document 2.35 GB/s; canada.json yyjson 1.51 GB/s; canada.json simdjson D&#8203;OM 1.07 GB/s; canada.json Boost.J&#8203;SON 0.90 GB/s; canada.json json::parse 0.38 GB/s; jeopardy.json json_document 2.45 GB/s; jeopardy.json yyjson 1.82 GB/s; jeopardy.json simdjson D&#8203;OM 2.07 GB/s; jeopardy.json Boost.J&#8203;SON 0.62 GB/s; jeopardy.json json::parse 0.33 GB/s.</desc>
<svg class="nj-fig nj-charts nj-charts--blog" viewBox="0 0 580 472" role="img" aria-labelledby="nj-blog-view nj-blog-view-d" xmlns="http://www.w3.org/2000/svg">
<title id="nj-blog-view">Parse throughput of json_document and six other document parsers, fresh documents, Apple M1 Max</title>
<desc id="nj-blog-view-d">Grouped horizontal bars in GB/s. twitter.json json_document 3.45 GB/s; twitter.json yyjson 3.26 GB/s; twitter.json simdjson D&#8203;OM 3.13 GB/s; twitter.json sonic-rs (Rust) 2.09 GB/s; twitter.json sonic-cpp 1.67 GB/s; twitter.json Boost.J&#8203;SON 0.73 GB/s; twitter.json json::parse 0.47 GB/s; citm_catalog.json json_document 4.75 GB/s; citm_catalog.json yyjson 3.61 GB/s; citm_catalog.json simdjson D&#8203;OM 3.55 GB/s; citm_catalog.json sonic-rs (Rust) 2.60 GB/s; citm_catalog.json sonic-cpp 1.75 GB/s; citm_catalog.json Boost.J&#8203;SON 1.16 GB/s; citm_catalog.json json::parse 0.72 GB/s; canada.json json_document 2.39 GB/s; canada.json yyjson 1.51 GB/s; canada.json simdjson D&#8203;OM 1.07 GB/s; canada.json sonic-rs (Rust) 1.18 GB/s; canada.json sonic-cpp 0.87 GB/s; canada.json Boost.J&#8203;SON 0.91 GB/s; canada.json json::parse 0.38 GB/s; jeopardy.json json_document 2.45 GB/s; jeopardy.json yyjson 1.83 GB/s; jeopardy.json simdjson D&#8203;OM 2.06 GB/s; jeopardy.json sonic-rs (Rust) 1.84 GB/s; jeopardy.json sonic-cpp 1.36 GB/s; jeopardy.json Boost.J&#8203;SON 0.62 GB/s; jeopardy.json json::parse 0.34 GB/s.</desc>
<rect class="nj-charts-c1" x="8" y="10" width="12" height="12" rx="3"/>
<text class="nj-charts-leg" x="26" y="20.5">json_document</text>
<rect class="nj-charts-c2" x="138.2" y="10" width="12" height="12" rx="3"/>
@@ -8,73 +8,93 @@
<rect class="nj-charts-c3" x="216.6" y="10" width="12" height="12" rx="3"/>
<text class="nj-charts-leg" x="234.6" y="20.5">simdjson D&#8203;OM</text>
<rect class="nj-charts-c4" x="339.4" y="10" width="12" height="12" rx="3"/>
<text class="nj-charts-leg" x="357.4" y="20.5">Boost.J&#8203;SON</text>
<rect class="nj-charts-c5" x="447.4" y="10" width="12" height="12" rx="3"/>
<text class="nj-charts-leg" x="465.4" y="20.5">json::parse</text>
<line class="nj-charts-grid" x1="182.3" y1="40" x2="182.3" y2="360"/>
<text class="nj-charts-tick" x="182.3" y="376">0.5</text>
<line class="nj-charts-grid" x1="222.6" y1="40" x2="222.6" y2="360"/>
<text class="nj-charts-tick" x="222.6" y="376">1</text>
<line class="nj-charts-grid" x1="262.9" y1="40" x2="262.9" y2="360"/>
<text class="nj-charts-tick" x="262.9" y="376">1.5</text>
<line class="nj-charts-grid" x1="303.2" y1="40" x2="303.2" y2="360"/>
<text class="nj-charts-tick" x="303.2" y="376">2</text>
<line class="nj-charts-grid" x1="343.5" y1="40" x2="343.5" y2="360"/>
<text class="nj-charts-tick" x="343.5" y="376">2.5</text>
<line class="nj-charts-grid" x1="383.8" y1="40" x2="383.8" y2="360"/>
<text class="nj-charts-tick" x="383.8" y="376">3</text>
<line class="nj-charts-grid" x1="424.1" y1="40" x2="424.1" y2="360"/>
<text class="nj-charts-tick" x="424.1" y="376">3.5</text>
<line class="nj-charts-grid" x1="464.4" y1="40" x2="464.4" y2="360"/>
<text class="nj-charts-tick" x="464.4" y="376">4</text>
<line class="nj-charts-grid" x1="504.7" y1="40" x2="504.7" y2="360"/>
<text class="nj-charts-tick" x="504.7" y="376">4.5</text>
<line class="nj-charts-grid" x1="545" y1="40" x2="545" y2="360"/>
<text class="nj-charts-tick" x="545" y="376">5</text>
<text class="nj-charts-tick" x="343.5" y="394">GB/s, longer is faster</text>
<text class="nj-charts-rowlabel" x="134" y="83.5" text-anchor="end">twitter.json</text>
<path class="nj-charts-c1" d="M142 44 H418.132 Q422.132 44 422.132 48 V51 Q422.132 55 418.132 55 H142 Z"><title>json_document, twitter.json: 3.48 GB/s</title></path>
<text class="nj-charts-val" x="426.132" y="53.5">3.48</text>
<path class="nj-charts-c2" d="M142 57 H403.104 Q407.104 57 407.104 61 V64 Q407.104 68 403.104 68 H142 Z"><title>yyjson, twitter.json: 3.29 GB/s</title></path>
<text class="nj-charts-val" x="411.104" y="66.5">3.29</text>
<path class="nj-charts-c3" d="M142 70 H373.213 Q377.213 70 377.213 74 V77 Q377.213 81 373.213 81 H142 Z"><title>simdjson D&#8203;OM, twitter.json: 2.92 GB/s</title></path>
<text class="nj-charts-val" x="381.213" y="79.5">2.92</text>
<path class="nj-charts-c4" d="M142 83 H196.776 Q200.776 83 200.776 87 V90 Q200.776 94 196.776 94 H142 Z"><title>Boost.J&#8203;SON, twitter.json: 0.73 GB/s</title></path>
<text class="nj-charts-val" x="204.776" y="92.5">0.73</text>
<path class="nj-charts-c5" d="M142 96 H175.208 Q179.208 96 179.208 100 V103 Q179.208 107 175.208 107 H142 Z"><title>json::parse, twitter.json: 0.46 GB/s</title></path>
<text class="nj-charts-val" x="183.208" y="105.5">0.46</text>
<text class="nj-charts-rowlabel" x="134" y="162.5" text-anchor="end">citm_catalog.json</text>
<path class="nj-charts-c1" d="M142 123 H524.809 Q528.809 123 528.809 127 V130 Q528.809 134 524.809 134 H142 Z"><title>json_document, citm_catalog.json: 4.80 GB/s</title></path>
<text class="nj-charts-val" x="532.809" y="132.5">4.80</text>
<path class="nj-charts-c2" d="M142 136 H429.179 Q433.179 136 433.179 140 V143 Q433.179 147 429.179 147 H142 Z"><title>yyjson, citm_catalog.json: 3.61 GB/s</title></path>
<text class="nj-charts-val" x="437.179" y="145.5">3.61</text>
<path class="nj-charts-c3" d="M142 149 H421.992 Q425.992 149 425.992 153 V156 Q425.992 160 421.992 160 H142 Z"><title>simdjson D&#8203;OM, citm_catalog.json: 3.52 GB/s</title></path>
<text class="nj-charts-val" x="429.992" y="158.5">3.52</text>
<path class="nj-charts-c4" d="M142 162 H232.063 Q236.063 162 236.063 166 V169 Q236.063 173 232.063 173 H142 Z"><title>Boost.J&#8203;SON, citm_catalog.json: 1.17 GB/s</title></path>
<text class="nj-charts-val" x="240.063" y="171.5">1.17</text>
<path class="nj-charts-c5" d="M142 175 H195.789 Q199.789 175 199.789 179 V182 Q199.789 186 195.789 186 H142 Z"><title>json::parse, citm_catalog.json: 0.72 GB/s</title></path>
<text class="nj-charts-val" x="203.789" y="184.5">0.72</text>
<text class="nj-charts-rowlabel" x="134" y="241.5" text-anchor="end">canada.json</text>
<path class="nj-charts-c1" d="M142 202 H327.093 Q331.093 202 331.093 206 V209 Q331.093 213 327.093 213 H142 Z"><title>json_document, canada.json: 2.35 GB/s</title></path>
<text class="nj-charts-val" x="335.093" y="211.5">2.35</text>
<path class="nj-charts-c2" d="M142 215 H259.85 Q263.85 215 263.85 219 V222 Q263.85 226 259.85 226 H142 Z"><title>yyjson, canada.json: 1.51 GB/s</title></path>
<text class="nj-charts-val" x="267.85" y="224.5">1.51</text>
<path class="nj-charts-c3" d="M142 228 H224.48 Q228.48 228 228.48 232 V235 Q228.48 239 224.48 239 H142 Z"><title>simdjson D&#8203;OM, canada.json: 1.07 GB/s</title></path>
<text class="nj-charts-val" x="232.48" y="237.5">1.07</text>
<path class="nj-charts-c4" d="M142 241 H210.545 Q214.545 241 214.545 245 V248 Q214.545 252 210.545 252 H142 Z"><title>Boost.J&#8203;SON, canada.json: 0.90 GB/s</title></path>
<text class="nj-charts-val" x="218.545" y="250.5">0.90</text>
<path class="nj-charts-c5" d="M142 254 H168.804 Q172.804 254 172.804 258 V261 Q172.804 265 168.804 265 H142 Z"><title>json::parse, canada.json: 0.38 GB/s</title></path>
<text class="nj-charts-val" x="176.804" y="263.5">0.38</text>
<text class="nj-charts-rowlabel" x="134" y="320.5" text-anchor="end">jeopardy.json</text>
<path class="nj-charts-c1" d="M142 281 H335.369 Q339.369 281 339.369 285 V288 Q339.369 292 335.369 292 H142 Z"><title>json_document, jeopardy.json: 2.45 GB/s</title></path>
<text class="nj-charts-val" x="343.369" y="290.5">2.45</text>
<path class="nj-charts-c2" d="M142 294 H284.675 Q288.675 294 288.675 298 V301 Q288.675 305 284.675 305 H142 Z"><title>yyjson, jeopardy.json: 1.82 GB/s</title></path>
<text class="nj-charts-val" x="292.675" y="303.5">1.82</text>
<path class="nj-charts-c3" d="M142 307 H304.612 Q308.612 307 308.612 311 V314 Q308.612 318 304.612 318 H142 Z"><title>simdjson D&#8203;OM, jeopardy.json: 2.07 GB/s</title></path>
<text class="nj-charts-val" x="312.612" y="316.5">2.07</text>
<path class="nj-charts-c4" d="M142 320 H188.368 Q192.368 320 192.368 324 V327 Q192.368 331 188.368 331 H142 Z"><title>Boost.J&#8203;SON, jeopardy.json: 0.62 GB/s</title></path>
<text class="nj-charts-val" x="196.368" y="329.5">0.62</text>
<path class="nj-charts-c5" d="M142 333 H164.706 Q168.706 333 168.706 337 V340 Q168.706 344 164.706 344 H142 Z"><title>json::parse, jeopardy.json: 0.33 GB/s</title></path>
<text class="nj-charts-val" x="172.706" y="342.5">0.33</text>
<line class="nj-charts-zero" x1="142" y1="40" x2="142" y2="360"/>
<text class="nj-charts-leg" x="357.4" y="20.5">sonic-rs (Rust)</text>
<rect class="nj-charts-c5" x="8" y="30" width="12" height="12" rx="3"/>
<text class="nj-charts-leg" x="26" y="40.5">sonic-cpp</text>
<rect class="nj-charts-c6" x="108.6" y="30" width="12" height="12" rx="3"/>
<text class="nj-charts-leg" x="126.6" y="40.5">Boost.J&#8203;SON</text>
<rect class="nj-charts-c1 nj-charts-dim" x="216.6" y="30" width="12" height="12" rx="3"/>
<text class="nj-charts-leg" x="234.6" y="40.5">json::parse</text>
<line class="nj-charts-grid" x1="182.3" y1="60" x2="182.3" y2="428"/>
<text class="nj-charts-tick" x="182.3" y="444">0.5</text>
<line class="nj-charts-grid" x1="222.6" y1="60" x2="222.6" y2="428"/>
<text class="nj-charts-tick" x="222.6" y="444">1</text>
<line class="nj-charts-grid" x1="262.9" y1="60" x2="262.9" y2="428"/>
<text class="nj-charts-tick" x="262.9" y="444">1.5</text>
<line class="nj-charts-grid" x1="303.2" y1="60" x2="303.2" y2="428"/>
<text class="nj-charts-tick" x="303.2" y="444">2</text>
<line class="nj-charts-grid" x1="343.5" y1="60" x2="343.5" y2="428"/>
<text class="nj-charts-tick" x="343.5" y="444">2.5</text>
<line class="nj-charts-grid" x1="383.8" y1="60" x2="383.8" y2="428"/>
<text class="nj-charts-tick" x="383.8" y="444">3</text>
<line class="nj-charts-grid" x1="424.1" y1="60" x2="424.1" y2="428"/>
<text class="nj-charts-tick" x="424.1" y="444">3.5</text>
<line class="nj-charts-grid" x1="464.4" y1="60" x2="464.4" y2="428"/>
<text class="nj-charts-tick" x="464.4" y="444">4</text>
<line class="nj-charts-grid" x1="504.7" y1="60" x2="504.7" y2="428"/>
<text class="nj-charts-tick" x="504.7" y="444">4.5</text>
<line class="nj-charts-grid" x1="545" y1="60" x2="545" y2="428"/>
<text class="nj-charts-tick" x="545" y="444">5</text>
<text class="nj-charts-tick" x="343.5" y="462">GB/s, longer is faster</text>
<text class="nj-charts-rowlabel" x="134" y="109.5" text-anchor="end">twitter.json</text>
<path class="nj-charts-c1" d="M142 64 H416.142 Q420.142 64 420.142 68 V69 Q420.142 73 416.142 73 H142 Z"><title>json_document, twitter.json: 3.45 GB/s</title></path>
<text class="nj-charts-val" x="424.142" y="71.5">3.45</text>
<path class="nj-charts-c2" d="M142 75 H400.371 Q404.371 75 404.371 79 V80 Q404.371 84 400.371 84 H142 Z"><title>yyjson, twitter.json: 3.26 GB/s</title></path>
<text class="nj-charts-val" x="408.371" y="82.5">3.26</text>
<path class="nj-charts-c3" d="M142 86 H389.98 Q393.98 86 393.98 90 V91 Q393.98 95 389.98 95 H142 Z"><title>simdjson D&#8203;OM, twitter.json: 3.13 GB/s</title></path>
<text class="nj-charts-val" x="397.98" y="93.5">3.13</text>
<path class="nj-charts-c4" d="M142 97 H306.543 Q310.543 97 310.543 101 V102 Q310.543 106 306.543 106 H142 Z"><title>sonic-rs (Rust), twitter.json: 2.09 GB/s</title></path>
<text class="nj-charts-val" x="314.543" y="104.5">2.09</text>
<path class="nj-charts-c5" d="M142 108 H272.301 Q276.301 108 276.301 112 V113 Q276.301 117 272.301 117 H142 Z"><title>sonic-cpp, twitter.json: 1.67 GB/s</title></path>
<text class="nj-charts-val" x="280.301" y="115.5">1.67</text>
<path class="nj-charts-c6" d="M142 119 H196.641 Q200.641 119 200.641 123 V124 Q200.641 128 196.641 128 H142 Z"><title>Boost.J&#8203;SON, twitter.json: 0.73 GB/s</title></path>
<text class="nj-charts-val" x="204.641" y="126.5">0.73</text>
<path class="nj-charts-c1 nj-charts-dim" d="M142 130 H175.648 Q179.648 130 179.648 134 V135 Q179.648 139 175.648 139 H142 Z"><title>json::parse, twitter.json: 0.47 GB/s</title></path>
<text class="nj-charts-val" x="183.648" y="137.5">0.47</text>
<text class="nj-charts-rowlabel" x="134" y="200.5" text-anchor="end">citm_catalog.json</text>
<path class="nj-charts-c1" d="M142 155 H520.452 Q524.452 155 524.452 159 V160 Q524.452 164 520.452 164 H142 Z"><title>json_document, citm_catalog.json: 4.75 GB/s</title></path>
<text class="nj-charts-val" x="528.452" y="162.5">4.75</text>
<path class="nj-charts-c2" d="M142 166 H429.24 Q433.24 166 433.24 170 V171 Q433.24 175 429.24 175 H142 Z"><title>yyjson, citm_catalog.json: 3.61 GB/s</title></path>
<text class="nj-charts-val" x="437.24" y="173.5">3.61</text>
<path class="nj-charts-c3" d="M142 177 H424.446 Q428.446 177 428.446 181 V182 Q428.446 186 424.446 186 H142 Z"><title>simdjson D&#8203;OM, citm_catalog.json: 3.55 GB/s</title></path>
<text class="nj-charts-val" x="432.446" y="184.5">3.55</text>
<path class="nj-charts-c4" d="M142 188 H347.658 Q351.658 188 351.658 192 V193 Q351.658 197 347.658 197 H142 Z"><title>sonic-rs (Rust), citm_catalog.json: 2.60 GB/s</title></path>
<text class="nj-charts-val" x="355.658" y="195.5">2.60</text>
<path class="nj-charts-c5" d="M142 199 H279.189 Q283.189 199 283.189 203 V204 Q283.189 208 279.189 208 H142 Z"><title>sonic-cpp, citm_catalog.json: 1.75 GB/s</title></path>
<text class="nj-charts-val" x="287.189" y="206.5">1.75</text>
<path class="nj-charts-c6" d="M142 210 H231.872 Q235.872 210 235.872 214 V215 Q235.872 219 231.872 219 H142 Z"><title>Boost.J&#8203;SON, citm_catalog.json: 1.16 GB/s</title></path>
<text class="nj-charts-val" x="239.872" y="217.5">1.16</text>
<path class="nj-charts-c1 nj-charts-dim" d="M142 221 H196.346 Q200.346 221 200.346 225 V226 Q200.346 230 196.346 230 H142 Z"><title>json::parse, citm_catalog.json: 0.72 GB/s</title></path>
<text class="nj-charts-val" x="204.346" y="228.5">0.72</text>
<text class="nj-charts-rowlabel" x="134" y="291.5" text-anchor="end">canada.json</text>
<path class="nj-charts-c1" d="M142 246 H330.402 Q334.402 246 334.402 250 V251 Q334.402 255 330.402 255 H142 Z"><title>json_document, canada.json: 2.39 GB/s</title></path>
<text class="nj-charts-val" x="338.402" y="253.5">2.39</text>
<path class="nj-charts-c2" d="M142 257 H259.85 Q263.85 257 263.85 261 V262 Q263.85 266 259.85 266 H142 Z"><title>yyjson, canada.json: 1.51 GB/s</title></path>
<text class="nj-charts-val" x="267.85" y="264.5">1.51</text>
<path class="nj-charts-c3" d="M142 268 H224.521 Q228.521 268 228.521 272 V273 Q228.521 277 224.521 277 H142 Z"><title>simdjson D&#8203;OM, canada.json: 1.07 GB/s</title></path>
<text class="nj-charts-val" x="232.521" y="275.5">1.07</text>
<path class="nj-charts-c4" d="M142 279 H233.141 Q237.141 279 237.141 283 V284 Q237.141 288 233.141 288 H142 Z"><title>sonic-rs (Rust), canada.json: 1.18 GB/s</title></path>
<text class="nj-charts-val" x="241.141" y="286.5">1.18</text>
<path class="nj-charts-c5" d="M142 290 H208.324 Q212.324 290 212.324 294 V295 Q212.324 299 208.324 299 H142 Z"><title>sonic-cpp, canada.json: 0.87 GB/s</title></path>
<text class="nj-charts-val" x="216.324" y="297.5">0.87</text>
<path class="nj-charts-c6" d="M142 301 H211.13 Q215.13 301 215.13 305 V306 Q215.13 310 211.13 310 H142 Z"><title>Boost.J&#8203;SON, canada.json: 0.91 GB/s</title></path>
<text class="nj-charts-val" x="219.13" y="308.5">0.91</text>
<path class="nj-charts-c1 nj-charts-dim" d="M142 312 H168.674 Q172.674 312 172.674 316 V317 Q172.674 321 168.674 321 H142 Z"><title>json::parse, canada.json: 0.38 GB/s</title></path>
<text class="nj-charts-val" x="176.674" y="319.5">0.38</text>
<text class="nj-charts-rowlabel" x="134" y="382.5" text-anchor="end">jeopardy.json</text>
<path class="nj-charts-c1" d="M142 337 H335.421 Q339.421 337 339.421 341 V342 Q339.421 346 335.421 346 H142 Z"><title>json_document, jeopardy.json: 2.45 GB/s</title></path>
<text class="nj-charts-val" x="343.421" y="344.5">2.45</text>
<path class="nj-charts-c2" d="M142 348 H285.574 Q289.574 348 289.574 352 V353 Q289.574 357 285.574 357 H142 Z"><title>yyjson, jeopardy.json: 1.83 GB/s</title></path>
<text class="nj-charts-val" x="293.574" y="355.5">1.83</text>
<path class="nj-charts-c3" d="M142 359 H304.433 Q308.433 359 308.433 363 V364 Q308.433 368 304.433 368 H142 Z"><title>simdjson D&#8203;OM, jeopardy.json: 2.06 GB/s</title></path>
<text class="nj-charts-val" x="312.433" y="366.5">2.06</text>
<path class="nj-charts-c4" d="M142 370 H286.258 Q290.258 370 290.258 374 V375 Q290.258 379 286.258 379 H142 Z"><title>sonic-rs (Rust), jeopardy.json: 1.84 GB/s</title></path>
<text class="nj-charts-val" x="294.258" y="377.5">1.84</text>
<path class="nj-charts-c5" d="M142 381 H247.737 Q251.737 381 251.737 385 V386 Q251.737 390 247.737 390 H142 Z"><title>sonic-cpp, jeopardy.json: 1.36 GB/s</title></path>
<text class="nj-charts-val" x="255.737" y="388.5">1.36</text>
<path class="nj-charts-c6" d="M142 392 H188.354 Q192.354 392 192.354 396 V397 Q192.354 401 188.354 401 H142 Z"><title>Boost.J&#8203;SON, jeopardy.json: 0.62 GB/s</title></path>
<text class="nj-charts-val" x="196.354" y="399.5">0.62</text>
<path class="nj-charts-c1 nj-charts-dim" d="M142 403 H165.345 Q169.345 403 169.345 407 V408 Q169.345 412 165.345 412 H142 Z"><title>json::parse, jeopardy.json: 0.34 GB/s</title></path>
<text class="nj-charts-val" x="173.345" y="410.5">0.34</text>
<line class="nj-charts-zero" x1="142" y1="60" x2="142" y2="428"/>
</svg>

Before

Width:  |  Height:  |  Size: 8.1 KiB

After

Width:  |  Height:  |  Size: 11 KiB

+19 -14
View File
@@ -7,7 +7,7 @@ Generated files:
json.hpp or json_view.hpp since 3.12.0, one panel each
- perf_blog_swar.svg: the eight-byte string test on one word of input
- perf_blog_cliffs.svg: operations that were quadratic or worse, 3.12.0 against 3.13.0 (log scale)
- perf_blog_view.svg: parse throughput of json_document, yyjson, simdjson's DOM, Boost.JSON and json::parse
- perf_blog_view.svg: parse throughput of json_document and six other document parsers
The numbers are copied from the tables of the post (the timeline from data/perf_timeline.csv, which
`tests/benchmarks/history/history.py summarize` writes); the tables stay on the page as the accessible fallback. Update both together.
@@ -303,12 +303,14 @@ def cliffs():
# engines and GB/s per file, copied from the table in "Build a different data structure"
VIEW_FILES = ["twitter.json", "citm_catalog.json", "canada.json", "jeopardy.json"]
VIEW_BYTES = [631514, 1727204, 2251051, 55554625]
VIEW_US = [ # (engine, css class, [µs per file])
("json_document", "nj-charts-c1", [181.7, 359.9, 959.5, 22687]),
("yyjson", "nj-charts-c2", [192.0, 478.1, 1489, 30528]),
("simdjson DOM", "nj-charts-c3", [216.4, 490.2, 2098, 26875]),
("Boost.JSON", "nj-charts-c4", [866.0, 1480, 2501, 88900]),
("json::parse", "nj-charts-c5", [1368, 2409, 5890, 167664]),
VIEW_US = [ # (engine, css class, [µs per file]), fresh documents, from the table in "How it compares"
("json_document", "nj-charts-c1", [183, 364, 943, 22681]),
("yyjson", "nj-charts-c2", [194, 478, 1489, 30342]),
("simdjson DOM", "nj-charts-c3", [202, 486, 2097, 26904]),
("sonic-rs (Rust)", "nj-charts-c4", [302, 664, 1907, 30202]),
("sonic-cpp", "nj-charts-c5", [379, 986, 2580, 40804]),
("Boost.JSON", "nj-charts-c6", [868, 1483, 2481, 88925]),
("json::parse", "nj-charts-c1 nj-charts-dim", [1352, 2386, 5915, 163750]),
]
VIEW = [(e, c, [n / (t * 1000) for n, t in zip(VIEW_BYTES, us)]) for e, c, us in VIEW_US] # GB/s
@@ -318,15 +320,18 @@ def view():
hi = max(max(v) for _, _, v in VIEW)
hi = math.ceil(hi * 2) / 2
X = lambda v: L + v / hi * (R - L)
bh, gap = 11, 2
top = 44
bh, gap = 9, 2
top = 64
group_h = len(VIEW) * (bh + gap) + 14
b, desc = [], []
x = 8
x, ly = 8, 10
for j, (eng, cls, _) in enumerate(VIEW):
b.append('<rect class="%s" x="%g" y="10" width="12" height="12" rx="3"/>' % (cls, x))
b.append('<text class="nj-charts-leg" x="%g" y="20.5">%s</text>' % (x + 18, esc(eng)))
x += 18 + 7.4 * len(eng) + 16
w = 18 + 7.4 * len(eng) + 16
if x + w > W:
x, ly = 8, ly + 20
b.append('<rect class="%s" x="%g" y="%g" width="12" height="12" rx="3"/>' % (cls, x, ly))
b.append('<text class="nj-charts-leg" x="%g" y="%g">%s</text>' % (x + 18, ly + 10.5, esc(eng)))
x += w
axis_y = top + group_h * len(VIEW_FILES)
t = 0.5
while t <= hi + 1e-9:
@@ -344,7 +349,7 @@ def view():
desc.append("%s %s %.2f GB/s" % (f, eng, vals[i]))
b.append('<line class="nj-charts-zero" x1="%g" y1="%g" x2="%g" y2="%g"/>' % (X(0), top - 4, X(0), axis_y))
write("perf_blog_view.svg", W, axis_y + 44, "nj-charts--blog", "nj-blog-view",
"Parse throughput of json_document, yyjson, simdjson's DOM, Boost.JSON and json::parse, Apple M1 Max",
"Parse throughput of json_document and six other document parsers, fresh documents, Apple M1 Max",
"Grouped horizontal bars in GB/s. " + "; ".join(desc) + ".", b)
@@ -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()