diff --git a/docs/mkdocs/docs/images/perf_blog_view.svg b/docs/mkdocs/docs/images/perf_blog_view.svg
deleted file mode 100644
index e355db7cf..000000000
--- a/docs/mkdocs/docs/images/perf_blog_view.svg
+++ /dev/null
@@ -1,100 +0,0 @@
-
-Parse throughput of nlohmann/json's json_document and json::parse and five other document parsers, fresh documents, Apple M1 Max
-Grouped horizontal bars of parse throughput in GB/s, more is better. twitter.json nlohmann/json json_document 3.45 GB/s; twitter.json yyjson 3.26 GB/s; twitter.json simdjson DOM 3.13 GB/s; twitter.json sonic-rs (Rust) 2.09 GB/s; twitter.json sonic-cpp 1.67 GB/s; twitter.json Boost.JSON 0.73 GB/s; twitter.json nlohmann/json json::parse 0.47 GB/s; citm_catalog.json nlohmann/json json_document 4.75 GB/s; citm_catalog.json yyjson 3.61 GB/s; citm_catalog.json simdjson DOM 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.JSON 1.16 GB/s; citm_catalog.json nlohmann/json json::parse 0.72 GB/s; canada.json nlohmann/json json_document 2.39 GB/s; canada.json yyjson 1.51 GB/s; canada.json simdjson DOM 1.07 GB/s; canada.json sonic-rs (Rust) 1.18 GB/s; canada.json sonic-cpp 0.87 GB/s; canada.json Boost.JSON 0.91 GB/s; canada.json nlohmann/json json::parse 0.38 GB/s; jeopardy.json nlohmann/json json_document 2.45 GB/s; jeopardy.json yyjson 1.83 GB/s; jeopardy.json simdjson DOM 2.06 GB/s; jeopardy.json sonic-rs (Rust) 1.84 GB/s; jeopardy.json sonic-cpp 1.36 GB/s; jeopardy.json Boost.JSON 0.62 GB/s; jeopardy.json nlohmann/json json::parse 0.34 GB/s.
-
-nlohmann/json json_document
-
-yyjson
-
-simdjson DOM
-
-sonic-rs (Rust)
-
-sonic-cpp
-
-Boost.JSON
-
-nlohmann/json json::parse
-
-0.5
-
-1
-
-1.5
-
-2
-
-2.5
-
-3
-
-3.5
-
-4
-
-4.5
-
-5
-GB/s, more is better
-twitter.json
-nlohmann/json json_document, twitter.json: 3.45 GB/s
-3.45
-yyjson, twitter.json: 3.26 GB/s
-3.26
-simdjson DOM, twitter.json: 3.13 GB/s
-3.13
-sonic-rs (Rust), twitter.json: 2.09 GB/s
-2.09
-sonic-cpp, twitter.json: 1.67 GB/s
-1.67
-Boost.JSON, twitter.json: 0.73 GB/s
-0.73
-nlohmann/json json::parse, twitter.json: 0.47 GB/s
-0.47
-citm_catalog.json
-nlohmann/json json_document, citm_catalog.json: 4.75 GB/s
-4.75
-yyjson, citm_catalog.json: 3.61 GB/s
-3.61
-simdjson DOM, citm_catalog.json: 3.55 GB/s
-3.55
-sonic-rs (Rust), citm_catalog.json: 2.60 GB/s
-2.60
-sonic-cpp, citm_catalog.json: 1.75 GB/s
-1.75
-Boost.JSON, citm_catalog.json: 1.16 GB/s
-1.16
-nlohmann/json json::parse, citm_catalog.json: 0.72 GB/s
-0.72
-canada.json
-nlohmann/json json_document, canada.json: 2.39 GB/s
-2.39
-yyjson, canada.json: 1.51 GB/s
-1.51
-simdjson DOM, canada.json: 1.07 GB/s
-1.07
-sonic-rs (Rust), canada.json: 1.18 GB/s
-1.18
-sonic-cpp, canada.json: 0.87 GB/s
-0.87
-Boost.JSON, canada.json: 0.91 GB/s
-0.91
-nlohmann/json json::parse, canada.json: 0.38 GB/s
-0.38
-jeopardy.json
-nlohmann/json json_document, jeopardy.json: 2.45 GB/s
-2.45
-yyjson, jeopardy.json: 1.83 GB/s
-1.83
-simdjson DOM, jeopardy.json: 2.06 GB/s
-2.06
-sonic-rs (Rust), jeopardy.json: 1.84 GB/s
-1.84
-sonic-cpp, jeopardy.json: 1.36 GB/s
-1.36
-Boost.JSON, jeopardy.json: 0.62 GB/s
-0.62
-nlohmann/json json::parse, jeopardy.json: 0.34 GB/s
-0.34
-
-
diff --git a/docs/mkdocs/docs/images/perf_blog_view_dump.svg b/docs/mkdocs/docs/images/perf_blog_view_dump.svg
deleted file mode 100644
index 7bb658e13..000000000
--- a/docs/mkdocs/docs/images/perf_blog_view_dump.svg
+++ /dev/null
@@ -1,106 +0,0 @@
-
-Write throughput of nlohmann/json's json_document and json, and five other libraries, each writing a document parsed beforehand, Apple M1 Max
-Grouped horizontal bars of write throughput in GB/s, more is better. twitter.json nlohmann/json json_document 6.27 GB/s; twitter.json yyjson 4.48 GB/s; twitter.json simdjson DOM 1.32 GB/s; twitter.json sonic-rs (Rust) 2.20 GB/s; twitter.json sonic-cpp 4.46 GB/s; twitter.json Boost.JSON 0.94 GB/s; twitter.json nlohmann/json json::dump 2.15 GB/s; citm_catalog.json nlohmann/json json_document 3.77 GB/s; citm_catalog.json yyjson 3.02 GB/s; citm_catalog.json simdjson DOM 1.00 GB/s; citm_catalog.json sonic-rs (Rust) 1.13 GB/s; citm_catalog.json sonic-cpp 2.25 GB/s; citm_catalog.json Boost.JSON 1.00 GB/s; citm_catalog.json nlohmann/json json::dump 1.75 GB/s; canada.json nlohmann/json json_document 1.29 GB/s; canada.json yyjson 1.22 GB/s; canada.json simdjson DOM 0.31 GB/s; canada.json sonic-rs (Rust) 0.86 GB/s; canada.json sonic-cpp 0.69 GB/s; canada.json Boost.JSON 0.71 GB/s; canada.json nlohmann/json json::dump 0.93 GB/s; jeopardy.json nlohmann/json json_document 3.56 GB/s; jeopardy.json yyjson 2.61 GB/s; jeopardy.json simdjson DOM 0.96 GB/s; jeopardy.json sonic-rs (Rust) 1.55 GB/s; jeopardy.json sonic-cpp 2.65 GB/s; jeopardy.json Boost.JSON 0.83 GB/s; jeopardy.json nlohmann/json json::dump 1.68 GB/s.
-
-nlohmann/json json_document
-
-yyjson
-
-simdjson DOM
-
-sonic-rs (Rust)
-
-sonic-cpp
-
-Boost.JSON
-
-nlohmann/json json::dump
-
-0.5
-
-1
-
-1.5
-
-2
-
-2.5
-
-3
-
-3.5
-
-4
-
-4.5
-
-5
-
-5.5
-
-6
-
-6.5
-GB/s, more is better
-twitter.json
-nlohmann/json json_document, twitter.json: 6.27 GB/s
-6.27
-yyjson, twitter.json: 4.48 GB/s
-4.48
-simdjson DOM, twitter.json: 1.32 GB/s
-1.32
-sonic-rs (Rust), twitter.json: 2.20 GB/s
-2.20
-sonic-cpp, twitter.json: 4.46 GB/s
-4.46
-Boost.JSON, twitter.json: 0.94 GB/s
-0.94
-nlohmann/json json::dump, twitter.json: 2.15 GB/s
-2.15
-citm_catalog.json
-nlohmann/json json_document, citm_catalog.json: 3.77 GB/s
-3.77
-yyjson, citm_catalog.json: 3.02 GB/s
-3.02
-simdjson DOM, citm_catalog.json: 1.00 GB/s
-1.00
-sonic-rs (Rust), citm_catalog.json: 1.13 GB/s
-1.13
-sonic-cpp, citm_catalog.json: 2.25 GB/s
-2.25
-Boost.JSON, citm_catalog.json: 1.00 GB/s
-1.00
-nlohmann/json json::dump, citm_catalog.json: 1.75 GB/s
-1.75
-canada.json
-nlohmann/json json_document, canada.json: 1.29 GB/s
-1.29
-yyjson, canada.json: 1.22 GB/s
-1.22
-simdjson DOM, canada.json: 0.31 GB/s
-0.31
-sonic-rs (Rust), canada.json: 0.86 GB/s
-0.86
-sonic-cpp, canada.json: 0.69 GB/s
-0.69
-Boost.JSON, canada.json: 0.71 GB/s
-0.71
-nlohmann/json json::dump, canada.json: 0.93 GB/s
-0.93
-jeopardy.json
-nlohmann/json json_document, jeopardy.json: 3.56 GB/s
-3.56
-yyjson, jeopardy.json: 2.61 GB/s
-2.61
-simdjson DOM, jeopardy.json: 0.96 GB/s
-0.96
-sonic-rs (Rust), jeopardy.json: 1.55 GB/s
-1.55
-sonic-cpp, jeopardy.json: 2.65 GB/s
-2.65
-Boost.JSON, jeopardy.json: 0.83 GB/s
-0.83
-nlohmann/json json::dump, jeopardy.json: 1.68 GB/s
-1.68
-
-
diff --git a/tests/benchmarks/json_view/others/README.md b/tests/benchmarks/json_view/others/README.md
index 7c69da094..d70ae3678 100644
--- a/tests/benchmarks/json_view/others/README.md
+++ b/tests/benchmarks/json_view/others/README.md
@@ -7,34 +7,120 @@ 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.
+The programs compare **use cases**, not library functions. A library may implement a use case in every reasonable way
+(another API, SAX events, in-situ parsing, a struct binding); each such way is a *variant*, named
+`: ` (`yyjson: insitu (copy)`, `RapidJSON: SAX`, `nlohmann json_document: accept`). The report shows
+each library's fastest correct variant. `nlohmann json` (`basic_json`) and `nlohmann json_document` (with
+`json_view`) count as two libraries.
-| workload | what it does |
+| use case | 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 |
+| `validate` | is this text valid JSON? returns a bool |
+| `sum` | visit every value, convert every number, touch every string and key; sum all numbers in document order |
+| `select` | read a few fields of every record (the fields of `bench_view.cpp`, e.g. `id`, `user.screen_name` and `retweet_count` of every status) |
+| `edit` | change some values of every record and write the document as compact JSON (see below) |
+| `minify` | text in, compact text out |
+| `write` | 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.
+The edits:
-| engine | version | notes |
+| file | edit |
+|---|---|
+| `twitter.json` | for every element of `statuses`: `retweet_count` + 1, erase `entities`, add `"seen": true` |
+| status | the same edit on the one status |
+| `citm_catalog.json` | for every element of `performances`: `start` + 1, erase `seatMapImage`, add `"seen": true` |
+| `canada.json` | `features[0].properties.name` = `"Kanada"`; append `[0.5,0.25]` to the first ring (`geometry.coordinates[0]`) of every feature |
+| `jeopardy.json` | for every record: `value` = `null`, erase `air_date`, add `"seen": true` |
+
+The files are `twitter.json`, `citm_catalog.json`, `canada.json` (nativejson-benchmark), `jeopardy.json`, and the first
+status of `twitter.json` (written by `json::dump()`, timed in batches of 200).
+
+### Input contract
+
+The input is a `const std::string` holding the file, with at least 64 bytes of spare capacity (`reserve`), so libraries
+that need padding (simdjson) can use it without a copy. A variant that modifies the input (in-situ parsing: yyjson
+`YYJSON_READ_INSITU`, RapidJSON `ParseInsitu`, simd-json) copies it inside the timed call; its name says `(copy)`.
+Each timed call starts from the text and frees everything it built, except variants named `(reused)`, which keep a
+parser or document across calls (simdjson's reused parsers, `json_document::read()`, simd-json's buffers). These are
+reported but never chosen as a library's best. In Rust, the input is the file as bytes (`&[u8]`), so every library
+has to validate UTF-8 itself.
+
+### Correctness rules
+
+Before timing, every variant runs on the file and must produce the right result:
+
+- `validate`: `true`.
+- `sum`: the same number of values, the same number of string and key bytes, and a sum of all numbers that is
+ bit-identical to `json_document`'s (document order), so every number is converted exactly; variants that visit
+ objects in another order (`nlohmann json: parse`, simd-json and serde_json values) are compared within 1e-9.
+- `select`: the same value as `json_document`.
+- `edit`, `minify`, `write`: the output, read with `json::parse`, compares equal (`==`) to the expected document (for
+ `edit`, the edit applied to a `json` value; floats compare exactly, so every float must be written exactly). In
+ Rust, `serde_json::Value` (with `float_roundtrip`) is the reference.
+
+A variant that gives a wrong result on a file, or crashes on it, is printed as
+`excluded: | | ` and not timed.
+
+### Validation levels
+
+Except for `write`, every variant is also probed with three malformed inputs derived from each file: the file
+truncated by one byte (after trailing whitespace), a stray `,` before the first `}`, and a `0xFF` byte inside a string
+value (the first in the second half of the file, or the last one; never a key, so that a variant that accepts the byte
+still finds the keys it looks up). The probes run in a child process, so a crash is caught. The result is the
+variant's *validation level*, printed in its cell (`sonic-cpp: Document [no UTF-8] 380.1us`):
+
+| level | meaning |
+|---|---|
+| `full` | rejects all three probes |
+| `no UTF-8` | rejects the truncated input and the stray comma, accepts the `0xFF` byte |
+| `partial` | accepts the truncated input or the stray comma, or crashes on any probe |
+| `n/a` | `write`: there is no input to validate (counts as `full`) |
+
+What a variant accepts is printed as `level: | | | `. A variant's level in
+the summary is the worst it got for any file. UTF-8 validation is enabled where a library has a switch (RapidJSON
+`kParseValidateEncodingFlag`; the `(no encoding check)` variants leave it off). sonic-cpp 1.0.2 cannot validate
+UTF-8 at all; serde_json does not validate UTF-8 in the parts it skips (`IgnoredAny`, unknown struct fields) unless
+the input is checked with `std::str::from_utf8` first (the `from_utf8 + ...` variants).
+
+The sum and select reference values are printed as `check ... reference:` lines; `summarize.py` warns if the C++ and
+Rust logs disagree.
+
+### Variants
+
+Level on the Mac in brackets where it is not `full` (`P` = partial, `U` = no UTF-8); every `write` variant is `n/a`.
+
+| library | validate | sum | select | edit | minify | write |
+|---|---|---|---|---|---|---|
+| nlohmann json_document | `accept`, `parse`, `read (reused)` | `parse`, `read (reused)` | `parse`, `structs (from_json)`, `read (reused)` | `editable`, `editable (source numbers)` (`json_editable_document`: set/erase, `dump` with `number_format::source`) | `parse + dump`, `parse + dump (source numbers)`, `read + dump (reused)` | `dump`, `dump (source numbers)` |
+| nlohmann json | `accept`, `parse` | `parse`, `sax` | `parse`, `structs (from_json)` | `parse`, `ordered_json` | `parse + dump` | `dump` |
+| yyjson | `read`, `insitu (copy)` | `read`, `insitu (copy)` | `read`, `insitu (copy)` | `mut copy`, `insitu (copy) + mut copy` (`yyjson_doc_mut_copy`) | `read + write`, `insitu (copy) + write` | `write` |
+| simdjson | `DOM`, `DOM (reused)` | `DOM`, `DOM (reused)`, `On-Demand`, `On-Demand (reused)` | `DOM`, `DOM (reused)`, `On-Demand` [P], `On-Demand (reused)` [P] | n/a (no mutable document) | `DOM + to_string`, `DOM (reused) + to_string`, `minify` [P] | `to_string` |
+| RapidJSON | `SAX` (null handler), `Document`, `insitu (copy)` | `Document`, `SAX`, `insitu (copy)` | `Document`, `insitu (copy)` | `Document`, `insitu (copy)` | `Document + Writer`, `SAX to Writer`, `SAX to Writer (raw numbers)` | `Writer` |
+| RapidJSON, `(no encoding check)` | the same [U] | the same [U] | the same [U] | the same [U] | the same [U] | |
+| sonic-cpp | `Document` [U] | `Document` [U] | `Document` [U] | `Document` [U] | `Document + Serialize` [U] | `Serialize` |
+| Glaze | `validate_json`, `generic` | `generic`, `lazy_json` [P] | `structs` [P], `structs (validate_skipped)`, `generic`, `lazy_json` [P] | `generic` | `generic`, `minify_json` [P] | `generic` |
+| Boost.JSON | `SAX` (`basic_parser`), `parse` | `parse`, `SAX` | `parse` | `parse` | `parse + serialize` | `serialize` |
+| serde_json | `IgnoredAny` [U], `from_utf8 + IgnoredAny`, `Value` | `visitor`, `Value` | `structs` [U], `from_utf8 + structs`, `Value` | `Value` | `Value` | `Value` |
+| sonic-rs | `IgnoredAny`, `Value` | `visitor`, `Value` | `structs`, `Value` | `Value` | `Value` | `Value` |
+| simd-json | `tape (copy)`, `borrowed (copy)`, `tape (reused)` | `visitor (copy)`, `borrowed (copy)`, `borrowed (reused)` | `structs (copy)`, `borrowed (copy)` | `owned (copy)`, `borrowed (copy)` | `borrowed (copy)` | `borrowed` |
+
+`structs` read into types that declare only the fields `select` uses (Glaze with `error_on_unknown_keys = false`,
+and with `validate_skipped` to validate what it skips; serde derive; nlohmann/json's `from_json()` of the
+`NLOHMANN_DEFINE_TYPE_NON_INTRUSIVE` macro, from a `json` value or from a `json_document`'s view); `visitor` is a serde `DeserializeSeed` that sums without building a value; `SAX` and `sax` are the
+libraries' event interfaces with a summing (or, for `validate`, an empty) handler. RapidJSON parses with
+`kParseFullPrecisionFlag` (correctly rounded floats; without it, sums and written floats are wrong) and Boost.JSON with
+`number_precision::precise`, as every other library rounds correctly by default.
+
+| library | version | notes |
|---|---|---|
-| `json_document` | this checkout | `json_view (reused)`: one document whose `read()` reuses its memory |
+| `json_document`, `json` | this checkout | |
| 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* |
+| simdjson | 4.6.11 | |
| 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` |
+| RapidJSON | master 24b5e7a8b27f (2025-02-05) | |
+| Glaze | 9.0.0 | `generic_u64` (owning DOM, member order kept) |
+| Boost.JSON | 1.92.0 | monotonic resource |
+| sonic-rs, simd-json, serde_json | see `rust/Cargo.lock` | serde_json with `float_roundtrip` |
SHA-256 of the downloads:
@@ -47,6 +133,12 @@ dd7b033c5bf6e4308615bb7834c541291d5b20f64121d9a68222a39414635517 glaze v9.0.0.t
c4a3b310ddd2472416e091067166b0713be97c63f38c212c484ada022fd296ce boost_1_92_0.tar.gz
```
+### Timing
+
+Every variant of a use case runs interleaved in every round, each timed call after an untimed one, and the best
+round is reported (the method of `bench_view.cpp`). `jeopardy.json` (50 MB) gets a fifth of the rounds (at least 3,
+at most the given number).
+
## Building and running
All libraries except yyjson and simdjson are used header-only. With the downloads unpacked into `DL`
@@ -63,17 +155,26 @@ c++ -std=c++23 -O3 -DNDEBUG -DRAPIDJSON_NEON \
cd rust && cargo build --release && ./target/release/bench_rs 30 > ../mac-rs-1.log
-python3 summarize.py mac-cpp-*.log --rust mac-rs-*.log
+python3 summarize.py mac-cpp-*.log --rust mac-rs-*.log --json mac.json
```
+Both programs take ` [rounds] [file] [use case]`: `./bench_all 30 canada edit` times
+only the edit of `canada.json`; `""` stands for all files. The checks cover only the selected files and use cases.
+bench_all uses `fork()` for the probes and the crash check (macOS, Linux).
+
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 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.
+process to one core, and time one file per process. Glaze 9 needs `std::expected`, which Clang 18 and older cannot
+use from GCC 13's standard library (Clang 19 and newer can); `-DBENCH_NO_GLAZE=1` builds without it. For Rust, build
+with `RUSTFLAGS="-C target-cpu=haswell"` to match `-march=haswell`.
## 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.
+A log line holds the file, the use case, and one cell per variant: `| : [] us
+(x)`, the ratio relative to the first variant of the line. `summarize.py` prints, for every use case and file,
+(a) per library the best `full` variant and, if faster, the best variant at any level, with their ratios to the best
+`full` `nlohmann json_document` variant (`(reused)` variants are never a library's best), (b) all variants with their
+levels and times, and (c) what the variants below `full` accept, and the excluded variants. `--json` writes
+`{use case: {file: {library: {"best": {"variant", "us", "level"} or null, "best_any": {...}, "variants": {variant:
+{"us", "level"}}, "excluded": {...}}}}}` for the chart scripts. Several logs are combined by taking each cell's best
+time; a variant excluded for one file is left out for every file of that use case.
+On a shared machine, differences of a few percent are noise: run twice and compare.
diff --git a/tests/benchmarks/json_view/others/bench_all.cpp b/tests/benchmarks/json_view/others/bench_all.cpp
index 8e079a36b..b376e62e6 100644
--- a/tests/benchmarks/json_view/others/bench_all.cpp
+++ b/tests/benchmarks/json_view/others/bench_all.cpp
@@ -6,20 +6,32 @@
// SPDX-FileCopyrightText: 2013-2026 Niels Lohmann
// 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 [rounds] [document]
+// Use cases of the nativejson-benchmark files, jeopardy and one twitter status with every C++ library in one binary:
+// validate, sum, select, edit, minify, write (see README.md). A library may implement a use case in every reasonable
+// way ("variants": other APIs, SAX, in-situ parsing, reused parsers). Before timing, every variant must produce the
+// right result on the file (else it is printed as "excluded" and not timed), and it is probed with malformed input
+// derived from the file, which gives its validation level: "full", "no UTF-8" or "partial" (printed in its cell).
+// All variants of a use case run interleaved in every round, each timed call after an untimed one, and the best round
+// is reported (same method as tests/benchmarks/json_view/bench_view.cpp).
+//
+// Input contract: the input is a const std::string holding the file with at least 64 bytes of spare capacity, so
+// simdjson can use it without a copy. A variant that modifies its input copies it inside the timed call. Each timed
+// call starts from the text and frees everything it built, except variants named "(reused)".
+//
+// Usage: bench_all [rounds] [document] [use case]
#include
#include
#include
+#include
#if !BENCH_NO_GLAZE
#include
#include
#endif
#include
+#include
+#include
+#include
#include
#include
#include
@@ -29,16 +41,29 @@
#include
#include
#include
+#include
#include
+#include
+#include
#include
#include
+#include
+#include
#include
#include
+#include
+#include
#include
+#include
+#include
+
using nlohmann::json;
using nlohmann::json_document;
+using nlohmann::json_editable_document;
+using nlohmann::json_editable_view;
using nlohmann::json_view;
+using nlohmann::ordered_json;
static volatile double g_sink;
@@ -48,7 +73,52 @@ struct stats
std::size_t str = 0, nodes = 0;
};
-// ---- json::parse and json_view
+// what a variant returns besides success: the stats (sum), the selected value (select), or the output text
+// (edit, minify, write; only copied when out is set, i.e., when checking)
+struct result
+{
+ stats st;
+ double acc = 0;
+ std::string* out = nullptr;
+};
+
+static bool emit(result& r, const char* p, std::size_t n)
+{
+ r.acc = static_cast(n);
+ if (r.out != nullptr)
+ {
+ r.out->assign(p, n);
+ }
+ return true;
+}
+
+static bool emit(result& r, std::string&& o)
+{
+ r.acc = static_cast(o.size());
+ if (r.out != nullptr)
+ {
+ *r.out = std::move(o);
+ }
+ return true;
+}
+
+// RapidJSON: correctly rounded floats, as every other library, and UTF-8 validation
+constexpr unsigned rj_flags = rapidjson::kParseFullPrecisionFlag | rapidjson::kParseValidateEncodingFlag;
+
+// Boost.JSON: correctly rounded floats (the default is number_precision::imprecise)
+static boost::json::parse_options bj_opts()
+{
+ boost::json::parse_options o;
+ o.numbers = boost::json::number_precision::precise;
+ return o;
+}
+static const boost::json::parse_options bj_precise = bj_opts();
+
+///////////////////////////////////////////////////////////////////////////////
+// sum: walk a DOM, visit every value, convert every number, touch every string and key
+///////////////////////////////////////////////////////////////////////////////
+
+// ---- json::parse
static void walk(const json& j, stats& st)
{
++st.nodes;
@@ -84,6 +154,7 @@ static void walk(const json& j, stats& st)
}
}
+// ---- json_view
static void walk(json_view v, stats& st)
{
++st.nodes;
@@ -119,6 +190,77 @@ static void walk(json_view v, stats& st)
}
}
+// ---- json::sax_parse: the same visit without a DOM, in document order
+struct json_sum_sax
+{
+ stats* st;
+ bool null()
+ {
+ ++st->nodes;
+ return true;
+ }
+ bool boolean(bool /*b*/)
+ {
+ ++st->nodes;
+ return true;
+ }
+ bool number_integer(json::number_integer_t i)
+ {
+ ++st->nodes;
+ st->num += static_cast(i);
+ return true;
+ }
+ bool number_unsigned(json::number_unsigned_t u)
+ {
+ ++st->nodes;
+ st->num += static_cast(u);
+ return true;
+ }
+ bool number_float(json::number_float_t f, const json::string_t& /*s*/)
+ {
+ ++st->nodes;
+ st->num += f;
+ return true;
+ }
+ bool string(json::string_t& s)
+ {
+ ++st->nodes;
+ st->str += s.size();
+ return true;
+ }
+ bool binary(json::binary_t& /*b*/)
+ {
+ return true;
+ }
+ bool start_object(std::size_t /*n*/)
+ {
+ ++st->nodes;
+ return true;
+ }
+ bool key(json::string_t& k)
+ {
+ st->str += k.size();
+ return true;
+ }
+ bool end_object()
+ {
+ return true;
+ }
+ bool start_array(std::size_t /*n*/)
+ {
+ ++st->nodes;
+ return true;
+ }
+ bool end_array()
+ {
+ return true;
+ }
+ bool parse_error(std::size_t /*pos*/, const std::string& /*tok*/, const json::exception& /*ex*/)
+ {
+ return false;
+ }
+};
+
// ---- yyjson
static void walk(yyjson_val* v, stats& st)
{
@@ -165,6 +307,27 @@ static void walk(yyjson_val* v, stats& st)
}
}
+// yyjson in situ: the input is copied into a padded, mutable buffer
+struct yy_insitu
+{
+ char* buf = nullptr;
+ yyjson_doc* doc = nullptr;
+ explicit yy_insitu(const std::string& s)
+ : buf(static_cast(std::malloc(s.size() + YYJSON_PADDING_SIZE)))
+ {
+ std::memcpy(buf, s.data(), s.size());
+ std::memset(buf + s.size(), 0, YYJSON_PADDING_SIZE);
+ doc = yyjson_read_opts(buf, s.size(), YYJSON_READ_INSITU, nullptr, nullptr);
+ }
+ yy_insitu(const yy_insitu&) = delete;
+ yy_insitu& operator=(const yy_insitu&) = delete;
+ ~yy_insitu()
+ {
+ yyjson_doc_free(doc);
+ std::free(buf);
+ }
+};
+
// ---- simdjson DOM and On-Demand
static void walk(simdjson::dom::element e, stats& st)
{
@@ -243,12 +406,15 @@ static void walk_od(simdjson::ondemand::value v, stats& st)
(void)bool(v.get_bool());
break;
default:
- (void)v.is_null();
+ if (!v.is_null())
+ {
+ throw simdjson::simdjson_error(simdjson::INCORRECT_TYPE);
+ }
break;
}
}
-// ---- RapidJSON
+// ---- RapidJSON DOM and SAX
static void walk(const rapidjson::Value& v, stats& st)
{
++st.nodes;
@@ -289,6 +455,101 @@ static void walk(const rapidjson::Value& v, stats& st)
}
}
+struct rj_sum : rapidjson::BaseReaderHandler, rj_sum>
+{
+ stats* st;
+ explicit rj_sum(stats* s) : st(s) {}
+ bool Null()
+ {
+ ++st->nodes;
+ return true;
+ }
+ bool Bool(bool /*b*/)
+ {
+ ++st->nodes;
+ return true;
+ }
+ bool Int(int i)
+ {
+ ++st->nodes;
+ st->num += static_cast(i);
+ return true;
+ }
+ bool Uint(unsigned u)
+ {
+ ++st->nodes;
+ st->num += static_cast(u);
+ return true;
+ }
+ bool Int64(std::int64_t i)
+ {
+ ++st->nodes;
+ st->num += static_cast(i);
+ return true;
+ }
+ bool Uint64(std::uint64_t u)
+ {
+ ++st->nodes;
+ st->num += static_cast(u);
+ return true;
+ }
+ bool Double(double d)
+ {
+ ++st->nodes;
+ st->num += d;
+ return true;
+ }
+ bool String(const char* /*s*/, rapidjson::SizeType n, bool /*copy*/)
+ {
+ ++st->nodes;
+ st->str += n;
+ return true;
+ }
+ bool Key(const char* /*s*/, rapidjson::SizeType n, bool /*copy*/)
+ {
+ st->str += n;
+ return true;
+ }
+ bool StartObject()
+ {
+ ++st->nodes;
+ return true;
+ }
+ bool EndObject(rapidjson::SizeType /*n*/)
+ {
+ return true;
+ }
+ bool StartArray()
+ {
+ ++st->nodes;
+ return true;
+ }
+ bool EndArray(rapidjson::SizeType /*n*/)
+ {
+ return true;
+ }
+};
+
+// RapidJSON's Reader on (data, size), as Document::Parse(data, size) does it
+template
+static bool rj_read(const std::string& s, Handler& h)
+{
+ rapidjson::MemoryStream ms(s.data(), s.size());
+ rapidjson::EncodedInputStream, rapidjson::MemoryStream> is(ms);
+ rapidjson::Reader reader;
+ return !reader.Parse(is, h).IsError();
+}
+
+// RapidJSON's Writer writes a raw number (kParseNumbersAsStringsFlag) as a string: write its text instead
+struct rj_raw_writer : rapidjson::Writer
+{
+ using Writer::Writer;
+ bool RawNumber(const char* str, rapidjson::SizeType n, bool /*copy*/)
+ {
+ return RawValue(str, n, rapidjson::kNumberType);
+ }
+};
+
// ---- sonic-cpp
template
static void walk_sonic(const N& v, stats& st)
@@ -331,7 +592,7 @@ static void walk_sonic(const N& v, stats& st)
}
#if !BENCH_NO_GLAZE
-// ---- Glaze generic_u64 (owning DOM) and lazy_json (on demand, no validation)
+// ---- Glaze generic_u64 (owning DOM) and lazy_json (on demand)
static void walk(const glz::generic_u64& j, stats& st)
{
++st.nodes;
@@ -404,10 +665,9 @@ static void walk_lazy(const V& v, stats& st)
}
}
}
-
#endif
-// ---- Boost.JSON
+// ---- Boost.JSON DOM and basic_parser (SAX)
static void walk(const boost::json::value& v, stats& st)
{
++st.nodes;
@@ -443,175 +703,491 @@ static void walk(const boost::json::value& v, stats& st)
}
}
-// ---- selective access (copied from bench_view.cpp)
-template
-static double pick_view(const std::string& name, V r)
+struct bj_null
{
- double acc = 0;
- if (name == "twitter" || name == "status")
+ using ec_t = boost::system::error_code;
+ using sv_t = boost::json::string_view;
+ static constexpr std::size_t max_array_size = static_cast(-1);
+ static constexpr std::size_t max_object_size = static_cast(-1);
+ static constexpr std::size_t max_string_size = static_cast(-1);
+ static constexpr std::size_t max_key_size = static_cast(-1);
+ bool on_document_begin(ec_t& /*ec*/)
{
- auto one = [&](V s)
- {
- acc += static_cast(s["id"].template get());
- acc += static_cast(s["user"]["screen_name"].get_string().size());
- acc += static_cast(s["retweet_count"].template get());
- };
- if (name == "twitter")
- {
- for (const V s : r["statuses"])
- {
- one(s);
- }
- }
- else
- {
- one(r);
- }
+ return true;
}
- else if (name == "citm_catalog")
+ bool on_document_end(ec_t& /*ec*/)
{
- for (const V p : r["performances"])
- {
- acc += static_cast(p["id"].template get() + p["eventId"].template get() + p["seatCategories"].size());
- }
- acc += static_cast(r["events"].size());
+ return true;
}
- else if (name == "canada")
+ bool on_array_begin(ec_t& /*ec*/)
{
- const V g = r["features"][0]["geometry"];
- acc += static_cast(r["type"].get_string().size() + g["type"].get_string().size() + g["coordinates"].size());
+ return true;
}
- else if (name == "jeopardy")
+ bool on_array_end(std::size_t /*n*/, ec_t& /*ec*/)
{
- for (const V q : r)
- {
- acc += q["round"].get_string() == "Final Jeopardy!" ? 1 : 0;
- acc += static_cast(q["category"].get_string().size());
- }
+ return true;
}
- else if (name == "rpc")
+ bool on_object_begin(ec_t& /*ec*/)
{
- acc += static_cast(r["method"].get_string().size());
- acc += static_cast(r["params"]["minuend"].template get() + r["id"].template get());
+ return true;
}
- return acc;
+ bool on_object_end(std::size_t /*n*/, ec_t& /*ec*/)
+ {
+ return true;
+ }
+ bool on_string_part(sv_t /*s*/, std::size_t /*n*/, ec_t& /*ec*/)
+ {
+ return true;
+ }
+ bool on_string(sv_t /*s*/, std::size_t /*n*/, ec_t& /*ec*/)
+ {
+ return true;
+ }
+ bool on_key_part(sv_t /*s*/, std::size_t /*n*/, ec_t& /*ec*/)
+ {
+ return true;
+ }
+ bool on_key(sv_t /*s*/, std::size_t /*n*/, ec_t& /*ec*/)
+ {
+ return true;
+ }
+ bool on_number_part(sv_t /*s*/, ec_t& /*ec*/)
+ {
+ return true;
+ }
+ bool on_int64(std::int64_t /*i*/, sv_t /*s*/, ec_t& /*ec*/)
+ {
+ return true;
+ }
+ bool on_uint64(std::uint64_t /*u*/, sv_t /*s*/, ec_t& /*ec*/)
+ {
+ return true;
+ }
+ bool on_double(double /*d*/, sv_t /*s*/, ec_t& /*ec*/)
+ {
+ return true;
+ }
+ bool on_bool(bool /*b*/, ec_t& /*ec*/)
+ {
+ return true;
+ }
+ bool on_null(ec_t& /*ec*/)
+ {
+ return true;
+ }
+ bool on_comment_part(sv_t /*s*/, ec_t& /*ec*/)
+ {
+ return true;
+ }
+ bool on_comment(sv_t /*s*/, ec_t& /*ec*/)
+ {
+ return true;
+ }
+};
+
+struct bj_sum : bj_null
+{
+ stats* st;
+ explicit bj_sum(stats* s) : st(s) {}
+ bool on_array_begin(ec_t& /*ec*/)
+ {
+ ++st->nodes;
+ return true;
+ }
+ bool on_object_begin(ec_t& /*ec*/)
+ {
+ ++st->nodes;
+ return true;
+ }
+ bool on_string(sv_t /*s*/, std::size_t n, ec_t& /*ec*/)
+ {
+ ++st->nodes;
+ st->str += n;
+ return true;
+ }
+ bool on_key(sv_t /*s*/, std::size_t n, ec_t& /*ec*/)
+ {
+ st->str += n;
+ return true;
+ }
+ bool on_int64(std::int64_t i, sv_t /*s*/, ec_t& /*ec*/)
+ {
+ ++st->nodes;
+ st->num += static_cast(i);
+ return true;
+ }
+ bool on_uint64(std::uint64_t u, sv_t /*s*/, ec_t& /*ec*/)
+ {
+ ++st->nodes;
+ st->num += static_cast(u);
+ return true;
+ }
+ bool on_double(double d, sv_t /*s*/, ec_t& /*ec*/)
+ {
+ ++st->nodes;
+ st->num += d;
+ return true;
+ }
+ bool on_bool(bool /*b*/, ec_t& /*ec*/)
+ {
+ ++st->nodes;
+ return true;
+ }
+ bool on_null(ec_t& /*ec*/)
+ {
+ ++st->nodes;
+ return true;
+ }
+};
+
+template
+static bool bj_sax(const std::string& s, Args&& ... args)
+{
+ boost::json::basic_parser p(bj_precise, std::forward(args)...);
+ boost::system::error_code ec;
+ const std::size_t n = p.write_some(false, s.data(), s.size(), ec);
+ return !ec && n == s.size();
}
-static double pick(const std::string& name, json_view r)
-{
- return pick_view(name, r);
-}
+///////////////////////////////////////////////////////////////////////////////
+// select: read a few fields of every record (the fields of bench_view.cpp), one accessor per DOM
+///////////////////////////////////////////////////////////////////////////////
-static double pick(const std::string& name, yyjson_val* r)
+struct acc_json
{
- double acc = 0;
- auto get = [](yyjson_val * o, const char* k)
+ using V = const json*;
+ static V get(V v, const char* k)
{
- return yyjson_obj_get(o, k);
+ return &(*v)[k];
+ }
+ static V at(V v, std::size_t i)
+ {
+ return &(*v)[i];
+ }
+ static std::uint64_t u64(V v)
+ {
+ return v->get();
+ }
+ static std::int64_t i64(V v)
+ {
+ return v->get();
+ }
+ static std::string_view str(V v)
+ {
+ return v->get_ref();
+ }
+ static std::size_t size(V v)
+ {
+ return v->size();
+ }
+ template static void each(V v, F&& f)
+ {
+ for (const json& e : *v)
+ {
+ f(&e);
+ }
+ }
+};
+
+struct acc_view
+{
+ using V = json_view;
+ static V get(V v, const char* k)
+ {
+ return v[k];
+ }
+ static V at(V v, std::size_t i)
+ {
+ return v[i];
+ }
+ static std::uint64_t u64(V v)
+ {
+ return v.get();
+ }
+ static std::int64_t i64(V v)
+ {
+ return v.get();
+ }
+ static std::string_view str(V v)
+ {
+ const auto s = v.get_string();
+ return {s.data(), s.size()};
+ }
+ static std::size_t size(V v)
+ {
+ return v.size();
+ }
+ template static void each(V v, F&& f)
+ {
+ for (const json_view e : v)
+ {
+ f(e);
+ }
+ }
+};
+
+struct acc_yyjson
+{
+ using V = yyjson_val*;
+ static V get(V v, const char* k)
+ {
+ return yyjson_obj_get(v, k);
+ }
+ static V at(V v, std::size_t i)
+ {
+ return yyjson_arr_get(v, i);
+ }
+ static std::uint64_t u64(V v)
+ {
+ return yyjson_get_uint(v);
+ }
+ static std::int64_t i64(V v)
+ {
+ return yyjson_get_sint(v);
+ }
+ static std::string_view str(V v)
+ {
+ return {yyjson_get_str(v), yyjson_get_len(v)};
+ }
+ static std::size_t size(V v)
+ {
+ return yyjson_is_arr(v) ? yyjson_arr_size(v) : yyjson_obj_size(v);
+ }
+ template static void each(V v, F&& f)
+ {
+ std::size_t idx, max;
+ yyjson_val* e;
+ yyjson_arr_foreach(v, idx, max, e)
+ {
+ f(e);
+ }
+ }
+};
+
+struct acc_simdjson
+{
+ using V = simdjson::dom::element;
+ static V get(V v, const char* k)
+ {
+ return v[k].value();
+ }
+ static V at(V v, std::size_t i)
+ {
+ return v.at(i).value();
+ }
+ static std::uint64_t u64(V v)
+ {
+ return uint64_t(v);
+ }
+ static std::int64_t i64(V v)
+ {
+ return int64_t(v);
+ }
+ static std::string_view str(V v)
+ {
+ return std::string_view(v);
+ }
+ static std::size_t size(V v)
+ {
+ return v.is_array() ? simdjson::dom::array(v).size() : simdjson::dom::object(v).size();
+ }
+ template static void each(V v, F&& f)
+ {
+ for (V e : simdjson::dom::array(v))
+ {
+ f(e);
+ }
+ }
+};
+
+struct acc_rapidjson
+{
+ using V = const rapidjson::Value*;
+ static V get(V v, const char* k)
+ {
+ return &(*v)[k];
+ }
+ static V at(V v, std::size_t i)
+ {
+ return &(*v)[static_cast(i)];
+ }
+ static std::uint64_t u64(V v)
+ {
+ return v->GetUint64();
+ }
+ static std::int64_t i64(V v)
+ {
+ return v->GetInt64();
+ }
+ static std::string_view str(V v)
+ {
+ return {v->GetString(), v->GetStringLength()};
+ }
+ static std::size_t size(V v)
+ {
+ return v->IsArray() ? v->Size() : v->MemberCount();
+ }
+ template static void each(V v, F&& f)
+ {
+ for (const auto& e : v->GetArray())
+ {
+ f(&e);
+ }
+ }
+};
+
+struct acc_sonic
+{
+ using V = const sonic_json::Node*;
+ static V get(V v, const char* k)
+ {
+ return &(*v)[k];
+ }
+ static V at(V v, std::size_t i)
+ {
+ return &(*v)[i];
+ }
+ static std::uint64_t u64(V v)
+ {
+ return v->GetUint64();
+ }
+ static std::int64_t i64(V v)
+ {
+ return v->GetInt64();
+ }
+ static std::string_view str(V v)
+ {
+ const auto s = v->GetStringView();
+ return {s.data(), s.size()};
+ }
+ static std::size_t size(V v)
+ {
+ return v->Size();
+ }
+ template static void each(V v, F&& f)
+ {
+ for (auto e = v->Begin(); e != v->End(); ++e)
+ {
+ f(&*e);
+ }
+ }
+};
+
+struct acc_boost
+{
+ using V = const boost::json::value*;
+ static V get(V v, const char* k)
+ {
+ return &v->get_object().at(k);
+ }
+ static V at(V v, std::size_t i)
+ {
+ return &v->get_array().at(i);
+ }
+ static std::uint64_t u64(V v)
+ {
+ return v->to_number();
+ }
+ static std::int64_t i64(V v)
+ {
+ return v->to_number();
+ }
+ static std::string_view str(V v)
+ {
+ const auto& s = v->get_string();
+ return {s.data(), s.size()};
+ }
+ static std::size_t size(V v)
+ {
+ return v->is_array() ? v->get_array().size() : v->get_object().size();
+ }
+ template static void each(V v, F&& f)
+ {
+ for (const auto& e : v->get_array())
+ {
+ f(&e);
+ }
+ }
+};
+
+#if !BENCH_NO_GLAZE
+struct acc_glaze
+{
+ using V = const glz::generic_u64*;
+ static V get(V v, const char* k)
+ {
+ return &(*v)[k];
+ }
+ static V at(V v, std::size_t i)
+ {
+ return &v->get_array()[i];
+ }
+ static std::uint64_t u64(V v)
+ {
+ return std::get(v->data);
+ }
+ static std::int64_t i64(V v)
+ {
+ const auto* u = std::get_if(&v->data);
+ return u != nullptr ? static_cast(*u) : std::get(v->data);
+ }
+ static std::string_view str(V v)
+ {
+ return std::get(v->data);
+ }
+ static std::size_t size(V v)
+ {
+ return v->is_array() ? v->get_array().size() : v->get_object().size();
+ }
+ template static void each(V v, F&& f)
+ {
+ for (const auto& e : v->get_array())
+ {
+ f(&e);
+ }
+ }
+};
+#endif
+
+template
+static double pick(const std::string& name, typename A::V r)
+{
+ using V = typename A::V;
+ double acc = 0;
+ auto status = [&](V s)
+ {
+ acc += static_cast(A::u64(A::get(s, "id")));
+ acc += static_cast(A::str(A::get(A::get(s, "user"), "screen_name")).size());
+ acc += static_cast(A::i64(A::get(s, "retweet_count")));
};
- if (name == "twitter" || name == "status")
+ if (name == "twitter")
{
- auto one = [&](yyjson_val * s)
- {
- acc += static_cast(yyjson_get_uint(get(s, "id")));
- acc += static_cast(yyjson_get_len(get(get(s, "user"), "screen_name")));
- acc += static_cast(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);
- }
+ A::each(A::get(r, "statuses"), status);
+ }
+ else if (name == "status")
+ {
+ status(r);
}
else if (name == "citm_catalog")
{
- std::size_t idx, max;
- yyjson_val* p;
- yyjson_arr_foreach(get(r, "performances"), idx, max, p)
+ A::each(A::get(r, "performances"), [&](V p)
{
- acc += static_cast(yyjson_get_uint(get(p, "id")) + yyjson_get_uint(get(p, "eventId")) + yyjson_arr_size(get(p, "seatCategories")));
- }
- acc += static_cast(yyjson_obj_size(get(r, "events")));
+ acc += static_cast(A::u64(A::get(p, "id")) + A::u64(A::get(p, "eventId")) + A::size(A::get(p, "seatCategories")));
+ });
+ acc += static_cast(A::size(A::get(r, "events")));
}
else if (name == "canada")
{
- yyjson_val* g = get(yyjson_arr_get(get(r, "features"), 0), "geometry");
- acc += static_cast(yyjson_get_len(get(r, "type")) + yyjson_get_len(get(g, "type")) + yyjson_arr_size(get(g, "coordinates")));
+ const V g = A::get(A::at(A::get(r, "features"), 0), "geometry");
+ acc += static_cast(A::str(A::get(r, "type")).size() + A::str(A::get(g, "type")).size() + A::size(A::get(g, "coordinates")));
}
else if (name == "jeopardy")
{
- std::size_t idx, max;
- yyjson_val* q;
- yyjson_arr_foreach(r, idx, max, q)
+ A::each(r, [&](V q)
{
- acc += yyjson_equals_str(get(q, "round"), "Final Jeopardy!") ? 1 : 0;
- acc += static_cast(yyjson_get_len(get(q, "category")));
- }
- }
- else if (name == "rpc")
- {
- acc += static_cast(yyjson_get_len(get(r, "method")));
- acc += static_cast(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(uint64_t(s["id"]));
- acc += static_cast(std::string_view(s["user"]["screen_name"]).size());
- acc += static_cast(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(uint64_t(p["id"]) + uint64_t(p["eventId"]) + simdjson::dom::array(p["seatCategories"]).size());
- }
- acc += static_cast(simdjson::dom::object(r["events"]).size());
- }
- else if (name == "canada")
- {
- auto g = r["features"].at(0)["geometry"];
- acc += static_cast(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(std::string_view(q["category"]).size());
- }
- }
- else if (name == "rpc")
- {
- acc += static_cast(std::string_view(r["method"]).size());
- acc += static_cast(int64_t(r["params"]["minuend"]) + int64_t(r["id"]));
+ acc += A::str(A::get(q, "round")) == "Final Jeopardy!" ? 1 : 0;
+ acc += static_cast(A::str(A::get(q, "category")).size());
+ });
}
return acc;
}
@@ -668,11 +1244,120 @@ static double pick_od(const std::string& name, simdjson::ondemand::document& d)
acc += std::string_view(o["round"]) == "Final Jeopardy!" ? 1 : 0;
}
}
- else if (name == "rpc")
+ return acc;
+}
+
+// nlohmann/json reading into structs with from_json(): the NLOHMANN_DEFINE_TYPE_* macros, from a json value
+// (json::parse(s).get()) and from a view (json_document::parse(s).root().get()), into the fields that Glaze's
+// and serde's structs declare; members that are only counted read into count_only
+namespace nsel
+{
+struct count_only
+{
+ std::size_t n = 0;
+};
+template < typename J, typename std::enable_if < nlohmann::is_json_view::value || std::is_same::value, int >::type = 0 >
+void from_json(const J& j, count_only& c)
+{
+ c.n = j.size();
+}
+struct user
+{
+ std::string screen_name;
+};
+struct status
+{
+ std::uint64_t id{};
+ nsel::user user;
+ std::int64_t retweet_count{};
+};
+struct twitter
+{
+ std::vector statuses;
+};
+struct performance
+{
+ std::uint64_t id{};
+ std::uint64_t eventId{};
+ count_only seatCategories;
+};
+struct citm
+{
+ std::map events;
+ std::vector performances;
+};
+struct geometry
+{
+ std::string type;
+ count_only coordinates;
+};
+struct feature
+{
+ nsel::geometry geometry;
+};
+struct canada
+{
+ std::string type;
+ std::vector features;
+};
+struct question
+{
+ std::string category;
+ std::string round;
+};
+NLOHMANN_DEFINE_TYPE_NON_INTRUSIVE(user, screen_name)
+NLOHMANN_DEFINE_TYPE_NON_INTRUSIVE(status, id, user, retweet_count)
+NLOHMANN_DEFINE_TYPE_NON_INTRUSIVE(twitter, statuses)
+NLOHMANN_DEFINE_TYPE_NON_INTRUSIVE(performance, id, eventId, seatCategories)
+NLOHMANN_DEFINE_TYPE_NON_INTRUSIVE(citm, events, performances)
+NLOHMANN_DEFINE_TYPE_NON_INTRUSIVE(geometry, type, coordinates)
+NLOHMANN_DEFINE_TYPE_NON_INTRUSIVE(feature, geometry)
+NLOHMANN_DEFINE_TYPE_NON_INTRUSIVE(canada, type, features)
+NLOHMANN_DEFINE_TYPE_NON_INTRUSIVE(question, category, round)
+} // namespace nsel
+
+template
+static double pick_nlohmann_structs(const std::string& name, const J& j)
+{
+ double acc = 0;
+ auto one = [&](const nsel::status & x)
{
- acc += static_cast(std::string_view(d["method"]).size());
- acc += static_cast(int64_t(d["params"]["minuend"]));
- acc += static_cast(int64_t(d["id"]));
+ acc += static_cast(x.id);
+ acc += static_cast(x.user.screen_name.size());
+ acc += static_cast(x.retweet_count);
+ };
+ if (name == "twitter")
+ {
+ for (const auto& x : j.template get().statuses)
+ {
+ one(x);
+ }
+ }
+ else if (name == "status")
+ {
+ one(j.template get());
+ }
+ else if (name == "citm_catalog")
+ {
+ const auto c = j.template get();
+ for (const auto& p : c.performances)
+ {
+ acc += static_cast(p.id + p.eventId + p.seatCategories.n);
+ }
+ acc += static_cast(c.events.size());
+ }
+ else if (name == "canada")
+ {
+ const auto c = j.template get();
+ acc += static_cast(c.type.size() + c.features[0].geometry.type.size() + c.features[0].geometry.coordinates.n);
+ }
+ else if (name == "jeopardy")
+ {
+ for (const auto& x : j.template get>())
+ {
+ acc += x.round == "Final Jeopardy!" ? 1 : 0;
+ acc += static_cast(x.category.size());
+ }
}
return acc;
}
@@ -725,49 +1410,53 @@ struct question
std::string category;
std::string round;
};
-}
+} // namespace sel
-constexpr glz::opts sel_opts{.error_on_unknown_keys = false};
-
-static double pick_glaze_structs(const std::string& name, const std::string& s)
+// validate_skipped: Glaze's option to validate the values it skips (an extension of glz::opts, hence the struct)
+struct sel_opts_validated : glz::opts
{
- double acc = 0;
- if (name == "twitter" || name == "status")
+ bool validate_skipped = true;
+};
+constexpr glz::opts sel_opts_fast{.error_on_unknown_keys = false};
+constexpr sel_opts_validated sel_opts_full{{.error_on_unknown_keys = false}};
+
+template
+static bool pick_glaze_structs(const std::string& name, const std::string& s, double& acc)
+{
+ acc = 0;
+ auto one = [&](const sel::status & x)
{
- auto one = [&](const sel::status & x)
+ acc += static_cast(x.id);
+ acc += static_cast(x.user.screen_name.size());
+ acc += static_cast(x.retweet_count);
+ };
+ if (name == "twitter")
+ {
+ sel::twitter t;
+ if (glz::read(t, s))
{
- acc += static_cast(x.id);
- acc += static_cast(x.user.screen_name.size());
- acc += static_cast(x.retweet_count);
- };
- if (name == "twitter")
- {
- sel::twitter t;
- if (glz::read(t, s))
- {
- return -1;
- }
- for (const auto& x : t.statuses)
- {
- one(x);
- }
+ return false;
}
- else
+ for (const auto& x : t.statuses)
{
- sel::status x;
- if (glz::read(x, s))
- {
- return -1;
- }
one(x);
}
}
+ else if (name == "status")
+ {
+ sel::status x;
+ if (glz::read(x, s))
+ {
+ return false;
+ }
+ one(x);
+ }
else if (name == "citm_catalog")
{
sel::citm c;
if (glz::read(c, s))
{
- return -1;
+ return false;
}
for (const auto& p : c.performances)
{
@@ -780,7 +1469,7 @@ static double pick_glaze_structs(const std::string& name, const std::string& s)
sel::canada c;
if (glz::read(c, s))
{
- return -1;
+ return false;
}
acc += static_cast(c.type.size() + c.features[0].geometry.type.size() + c.features[0].geometry.coordinates.size());
}
@@ -789,7 +1478,7 @@ static double pick_glaze_structs(const std::string& name, const std::string& s)
std::vector q;
if (glz::read(q, s))
{
- return -1;
+ return false;
}
for (const auto& x : q)
{
@@ -797,7 +1486,7 @@ static double pick_glaze_structs(const std::string& name, const std::string& s)
acc += static_cast(x.category.size());
}
}
- return acc;
+ return true;
}
template
@@ -846,6 +1535,649 @@ static double pick_glaze_lazy(const std::string& name, const D& d)
}
#endif
+///////////////////////////////////////////////////////////////////////////////
+// edit: change some values of every record and write the document as compact JSON
+// twitter (and status): for every status: retweet_count + 1, erase "entities", add "seen": true
+// citm_catalog: for every performance: start + 1, erase "seatMapImage", add "seen": true
+// canada: features[0].properties.name = "Kanada"; append [0.5,0.25] to coordinates[0] of every feature
+// jeopardy: for every record: value = null, erase "air_date", add "seen": true
+///////////////////////////////////////////////////////////////////////////////
+
+// basic_json: the reference for every other library, and the json/ordered_json variants
+template
+static void edit(const std::string& name, J& r)
+{
+ auto status = [](J & s)
+ {
+ s["retweet_count"] = s["retweet_count"].template get() + 1;
+ s.erase("entities");
+ s["seen"] = true;
+ };
+ if (name == "twitter")
+ {
+ for (J& s : r["statuses"])
+ {
+ status(s);
+ }
+ }
+ else if (name == "status")
+ {
+ status(r);
+ }
+ else if (name == "citm_catalog")
+ {
+ for (J& p : r["performances"])
+ {
+ p["start"] = p["start"].template get() + 1;
+ p.erase("seatMapImage");
+ p["seen"] = true;
+ }
+ }
+ else if (name == "canada")
+ {
+ r["features"][0]["properties"]["name"] = "Kanada";
+ for (J& f : r["features"])
+ {
+ f["geometry"]["coordinates"][0].push_back(J::array({0.5, 0.25}));
+ }
+ }
+ else if (name == "jeopardy")
+ {
+ for (J& q : r)
+ {
+ q["value"] = nullptr;
+ q.erase("air_date");
+ q["seen"] = true;
+ }
+ }
+}
+
+static bool edit_document(const std::string& name, const std::string& s, result& res, bool source_numbers)
+{
+ json_editable_document d = json_editable_document::parse(s);
+ const json_editable_view r = d.root();
+ auto status = [&](json_editable_view st)
+ {
+ d.set(st, "retweet_count", st["retweet_count"].get() + 1);
+ d.erase(st, "entities");
+ d.set(st, "seen", true);
+ };
+ if (name == "twitter")
+ {
+ for (const json_editable_view st : r["statuses"])
+ {
+ status(st);
+ }
+ }
+ else if (name == "status")
+ {
+ status(r);
+ }
+ else if (name == "citm_catalog")
+ {
+ for (const json_editable_view p : r["performances"])
+ {
+ d.set(p, "start", p["start"].get() + 1);
+ d.erase(p, "seatMapImage");
+ d.set(p, "seen", true);
+ }
+ }
+ else if (name == "canada")
+ {
+ d.set(r["features"][0]["properties"], "name", "Kanada");
+ for (const json_editable_view f : r["features"])
+ {
+ d.push_back(f["geometry"]["coordinates"][0], json::array({0.5, 0.25}));
+ }
+ }
+ else if (name == "jeopardy")
+ {
+ for (const json_editable_view q : r)
+ {
+ d.set(q, "value", nullptr);
+ d.erase(q, "air_date");
+ d.set(q, "seen", true);
+ }
+ }
+ return emit(res, source_numbers ? r.dump(-1, ' ', false, json_editable_view::number_format::source) : r.dump());
+}
+
+static std::int64_t yy_int(const yyjson_mut_val* v)
+{
+ return yyjson_mut_is_uint(v) ? static_cast(yyjson_mut_get_uint(v)) : yyjson_mut_get_sint(v);
+}
+
+static bool edit_yyjson(const std::string& name, yyjson_doc* idoc, result& res)
+{
+ if (idoc == nullptr)
+ {
+ return false;
+ }
+ yyjson_mut_doc* d = yyjson_doc_mut_copy(idoc, nullptr);
+ yyjson_mut_val* r = yyjson_mut_doc_get_root(d);
+ auto get = [](yyjson_mut_val * o, const char* k)
+ {
+ return yyjson_mut_obj_get(o, k);
+ };
+ auto status = [&](yyjson_mut_val * st)
+ {
+ yyjson_mut_val* rc = get(st, "retweet_count");
+ yyjson_mut_set_sint(rc, yy_int(rc) + 1);
+ yyjson_mut_obj_remove_key(st, "entities");
+ yyjson_mut_obj_add_bool(d, st, "seen", true);
+ };
+ std::size_t idx, max;
+ yyjson_mut_val* e;
+ if (name == "twitter")
+ {
+ yyjson_mut_arr_foreach(get(r, "statuses"), idx, max, e)
+ {
+ status(e);
+ }
+ }
+ else if (name == "status")
+ {
+ status(r);
+ }
+ else if (name == "citm_catalog")
+ {
+ yyjson_mut_arr_foreach(get(r, "performances"), idx, max, e)
+ {
+ yyjson_mut_val* start = get(e, "start");
+ yyjson_mut_set_sint(start, yy_int(start) + 1);
+ yyjson_mut_obj_remove_key(e, "seatMapImage");
+ yyjson_mut_obj_add_bool(d, e, "seen", true);
+ }
+ }
+ else if (name == "canada")
+ {
+ static const double point[2] = {0.5, 0.25};
+ yyjson_mut_val* features = get(r, "features");
+ yyjson_mut_set_str(get(get(yyjson_mut_arr_get(features, 0), "properties"), "name"), "Kanada");
+ yyjson_mut_arr_foreach(features, idx, max, e)
+ {
+ yyjson_mut_arr_append(yyjson_mut_arr_get(get(get(e, "geometry"), "coordinates"), 0), yyjson_mut_arr_with_real(d, point, 2));
+ }
+ }
+ else if (name == "jeopardy")
+ {
+ yyjson_mut_arr_foreach(r, idx, max, e)
+ {
+ yyjson_mut_set_null(get(e, "value"));
+ yyjson_mut_obj_remove_key(e, "air_date");
+ yyjson_mut_obj_add_bool(d, e, "seen", true);
+ }
+ }
+ std::size_t n = 0;
+ char* o = yyjson_mut_write(d, 0, &n);
+ const bool ok = o != nullptr && emit(res, o, n);
+ std::free(o);
+ yyjson_mut_doc_free(d);
+ return ok;
+}
+
+static bool edit_rapidjson(const std::string& name, rapidjson::Document& d, result& res)
+{
+ auto& a = d.GetAllocator();
+ auto status = [&](rapidjson::Value & st)
+ {
+ auto& rc = st["retweet_count"];
+ rc.SetInt64(rc.GetInt64() + 1);
+ st.RemoveMember("entities");
+ st.AddMember("seen", true, a);
+ };
+ if (name == "twitter")
+ {
+ for (auto& st : d["statuses"].GetArray())
+ {
+ status(st);
+ }
+ }
+ else if (name == "status")
+ {
+ status(d);
+ }
+ else if (name == "citm_catalog")
+ {
+ for (auto& p : d["performances"].GetArray())
+ {
+ auto& start = p["start"];
+ start.SetInt64(start.GetInt64() + 1);
+ p.RemoveMember("seatMapImage");
+ p.AddMember("seen", true, a);
+ }
+ }
+ else if (name == "canada")
+ {
+ d["features"][0u]["properties"]["name"].SetString("Kanada");
+ for (auto& f : d["features"].GetArray())
+ {
+ rapidjson::Value point(rapidjson::kArrayType);
+ point.PushBack(0.5, a).PushBack(0.25, a);
+ f["geometry"]["coordinates"][0u].PushBack(point, a);
+ }
+ }
+ else if (name == "jeopardy")
+ {
+ for (auto& q : d.GetArray())
+ {
+ q["value"].SetNull();
+ q.RemoveMember("air_date");
+ q.AddMember("seen", true, a);
+ }
+ }
+ rapidjson::StringBuffer sb;
+ rapidjson::Writer w(sb);
+ return d.Accept(w) && emit(res, sb.GetString(), sb.GetSize());
+}
+
+static bool edit_sonic(const std::string& name, const std::string& s, result& res)
+{
+ sonic_json::Document d;
+ d.Parse(s.data(), s.size());
+ if (d.HasParseError())
+ {
+ return false;
+ }
+ auto& a = d.GetAllocator();
+ auto status = [&](sonic_json::Node & st)
+ {
+ auto& rc = st["retweet_count"];
+ rc.SetInt64(rc.GetInt64() + 1);
+ st.RemoveMember("entities");
+ st.AddMember("seen", sonic_json::Node(true), a);
+ };
+ if (name == "twitter")
+ {
+ auto& sts = d["statuses"];
+ for (auto it = sts.Begin(); it != sts.End(); ++it)
+ {
+ status(*it);
+ }
+ }
+ else if (name == "status")
+ {
+ status(d);
+ }
+ else if (name == "citm_catalog")
+ {
+ auto& ps = d["performances"];
+ for (auto it = ps.Begin(); it != ps.End(); ++it)
+ {
+ auto& start = (*it)["start"];
+ start.SetInt64(start.GetInt64() + 1);
+ it->RemoveMember("seatMapImage");
+ it->AddMember("seen", sonic_json::Node(true), a);
+ }
+ }
+ else if (name == "canada")
+ {
+ auto& fs = d["features"];
+ fs[std::size_t{0}]["properties"]["name"].SetString("Kanada", 6, a);
+ for (auto it = fs.Begin(); it != fs.End(); ++it)
+ {
+ sonic_json::Node point(sonic_json::kArray);
+ point.PushBack(sonic_json::Node(0.5), a);
+ point.PushBack(sonic_json::Node(0.25), a);
+ (*it)["geometry"]["coordinates"][std::size_t{0}].PushBack(std::move(point), a);
+ }
+ }
+ else if (name == "jeopardy")
+ {
+ for (auto it = d.Begin(); it != d.End(); ++it)
+ {
+ (*it)["value"].SetNull();
+ it->RemoveMember("air_date");
+ it->AddMember("seen", sonic_json::Node(true), a);
+ }
+ }
+ sonic_json::WriteBuffer wb;
+ return d.Serialize(wb) == sonic_json::kErrorNone && emit(res, wb.ToString(), wb.Size());
+}
+
+#if !BENCH_NO_GLAZE
+static void glaze_inc(glz::generic_u64& v)
+{
+ if (auto* u = std::get_if(&v.data))
+ {
+ ++*u;
+ }
+ else
+ {
+ ++std::get(v.data);
+ }
+}
+
+static bool edit_glaze(const std::string& name, const std::string& s, result& res)
+{
+ glz::generic_u64 j;
+ if (glz::read_json(j, s))
+ {
+ return false;
+ }
+ auto status = [](glz::generic_u64 & st)
+ {
+ glaze_inc(st["retweet_count"]);
+ st.get_object().erase("entities");
+ st["seen"].data = true;
+ };
+ if (name == "twitter")
+ {
+ for (auto& st : j["statuses"].get_array())
+ {
+ status(st);
+ }
+ }
+ else if (name == "status")
+ {
+ status(j);
+ }
+ else if (name == "citm_catalog")
+ {
+ for (auto& p : j["performances"].get_array())
+ {
+ glaze_inc(p["start"]);
+ p.get_object().erase("seatMapImage");
+ p["seen"].data = true;
+ }
+ }
+ else if (name == "canada")
+ {
+ j["features"][0]["properties"]["name"].data = std::string("Kanada");
+ for (auto& f : j["features"].get_array())
+ {
+ glz::generic_u64 point;
+ point.data = glz::generic_u64::array_t(2);
+ point.get_array()[0].data = 0.5;
+ point.get_array()[1].data = 0.25;
+ f["geometry"]["coordinates"][0].get_array().push_back(std::move(point));
+ }
+ }
+ else if (name == "jeopardy")
+ {
+ for (auto& q : j.get_array())
+ {
+ q["value"].data = nullptr;
+ q.get_object().erase("air_date");
+ q["seen"].data = true;
+ }
+ }
+ std::string o;
+ return !glz::write_json(j, o) && emit(res, std::move(o));
+}
+#endif
+
+static bool edit_boost(const std::string& name, const std::string& s, result& res)
+{
+ boost::json::monotonic_resource mr;
+ boost::system::error_code ec;
+ boost::json::value v = boost::json::parse(s, ec, &mr, bj_precise);
+ if (ec)
+ {
+ return false;
+ }
+ auto status = [](boost::json::object & o)
+ {
+ boost::json::value& rc = o["retweet_count"];
+ rc = rc.to_number() + 1;
+ o.erase("entities");
+ o["seen"] = true;
+ };
+ if (name == "twitter")
+ {
+ for (auto& st : v.as_object()["statuses"].as_array())
+ {
+ status(st.as_object());
+ }
+ }
+ else if (name == "status")
+ {
+ status(v.as_object());
+ }
+ else if (name == "citm_catalog")
+ {
+ for (auto& p : v.as_object()["performances"].as_array())
+ {
+ auto& o = p.as_object();
+ boost::json::value& start = o["start"];
+ start = start.to_number() + 1;
+ o.erase("seatMapImage");
+ o["seen"] = true;
+ }
+ }
+ else if (name == "canada")
+ {
+ auto& fs = v.as_object()["features"].as_array();
+ fs.at(0).as_object()["properties"].as_object()["name"] = "Kanada";
+ for (auto& f : fs)
+ {
+ auto& ring = f.as_object()["geometry"].as_object()["coordinates"].as_array().at(0).as_array();
+ ring.push_back(boost::json::array({0.5, 0.25}, ring.storage()));
+ }
+ }
+ else if (name == "jeopardy")
+ {
+ for (auto& q : v.as_array())
+ {
+ auto& o = q.as_object();
+ o["value"] = nullptr;
+ o.erase("air_date");
+ o["seen"] = true;
+ }
+ }
+ return emit(res, boost::json::serialize(v));
+}
+
+///////////////////////////////////////////////////////////////////////////////
+// variants
+///////////////////////////////////////////////////////////////////////////////
+
+struct variant
+{
+ std::string name; // ": "
+ std::function fn;
+ bool any_order = false; // sum: visits objects in key order, so its sum may differ in the last bits
+};
+
+struct context
+{
+ std::string name; // document
+ json_document& reused;
+ simdjson::dom::parser& sj;
+ simdjson::ondemand::parser& od;
+};
+
+static std::vector validate_variants(context& c)
+{
+ return
+ {
+ {"nlohmann json_document: accept", [](const std::string & s, result&) { return json_document::accept(s); }},
+ {"nlohmann json_document: parse", [](const std::string & s, result & r) { auto d = json_document::parse(s); r.acc = static_cast(d.node_count()); return true; }},
+ {"nlohmann json_document: read (reused)", [&c](const std::string & s, result & r) { c.reused.read(s); r.acc = static_cast(c.reused.node_count()); return true; }},
+ {"nlohmann json: accept", [](const std::string & s, result&) { return json::accept(s); }},
+ {"nlohmann json: parse", [](const std::string & s, result & r) { json j = json::parse(s); r.acc = static_cast(j.size()); return true; }},
+ {"yyjson: read", [](const std::string & s, result&) { yyjson_doc* d = yyjson_read(s.data(), s.size(), 0); const bool ok = d != nullptr; yyjson_doc_free(d); return ok; }},
+ {"yyjson: insitu (copy)", [](const std::string & s, result&) { yy_insitu d(s); return d.doc != nullptr; }},
+ {"simdjson: DOM", [](const std::string & s, result&) { simdjson::dom::parser p; return !p.parse(s).error(); }},
+ {"simdjson: DOM (reused)", [&c](const std::string & s, result&) { return !c.sj.parse(s).error(); }},
+ {"sonic-cpp: Document", [](const std::string & s, result&) { sonic_json::Document d; d.Parse(s.data(), s.size()); return !d.HasParseError(); }},
+#if !BENCH_NO_GLAZE
+ {"Glaze: validate_json", [](const std::string & s, result&) { return !glz::validate_json(s); }},
+ {"Glaze: generic", [](const std::string & s, result&) { glz::generic_u64 j; return !glz::read_json(j, s); }},
+#endif
+ {"Boost.JSON: SAX", [](const std::string & s, result&) { return bj_sax(s); }},
+ {"Boost.JSON: parse", [](const std::string & s, result&) { boost::json::monotonic_resource mr; boost::system::error_code ec; auto v = boost::json::parse(s, ec, &mr, bj_precise); return !ec; }},
+ };
+}
+
+static std::vector sum_variants(context& c)
+{
+ return
+ {
+ {"nlohmann json_document: parse", [](const std::string & s, result & r) { auto d = json_document::parse(s); walk(d.root(), r.st); return true; }},
+ {"nlohmann json_document: read (reused)", [&c](const std::string & s, result & r) { c.reused.read(s); walk(c.reused.root(), r.st); return true; }},
+ {"nlohmann json: parse", [](const std::string & s, result & r) { json j = json::parse(s); walk(j, r.st); return true; }, true},
+ {"nlohmann json: sax", [](const std::string & s, result & r) { json_sum_sax h{&r.st}; return json::sax_parse(s, &h); }},
+ {"yyjson: read", [](const std::string & s, result & r) { yyjson_doc* d = yyjson_read(s.data(), s.size(), 0); if (d == nullptr) { return false; } walk(yyjson_doc_get_root(d), r.st); yyjson_doc_free(d); return true; }},
+ {"yyjson: insitu (copy)", [](const std::string & s, result & r) { yy_insitu d(s); if (d.doc == nullptr) { return false; } walk(yyjson_doc_get_root(d.doc), r.st); return true; }},
+ {"simdjson: DOM", [](const std::string & s, result & r) { simdjson::dom::parser p; auto e = p.parse(s); if (e.error()) { return false; } walk(e.value_unsafe(), r.st); return true; }},
+ {"simdjson: DOM (reused)", [&c](const std::string & s, result & r) { auto e = c.sj.parse(s); if (e.error()) { return false; } walk(e.value_unsafe(), r.st); return true; }},
+ {"simdjson: On-Demand", [](const std::string & s, result & r) { simdjson::ondemand::parser p; auto d = p.iterate(s).value(); walk_od(d.get_value().value(), r.st); return d.at_end(); }},
+ {"simdjson: On-Demand (reused)", [&c](const std::string & s, result & r) { auto d = c.od.iterate(s).value(); walk_od(d.get_value().value(), r.st); return d.at_end(); }},
+ {"sonic-cpp: Document", [](const std::string & s, result & r) { sonic_json::Document d; d.Parse(s.data(), s.size()); if (d.HasParseError()) { return false; } walk_sonic(d, r.st); return true; }},
+#if !BENCH_NO_GLAZE
+ {"Glaze: generic", [](const std::string & s, result & r) { glz::generic_u64 j; if (glz::read_json(j, s)) { return false; } walk(j, r.st); return true; }},
+ {"Glaze: lazy_json", [](const std::string & s, result & r) { auto l = glz::lazy_json(s); if (!l) { return false; } walk_lazy(l->root(), r.st); return true; }},
+#endif
+ {"Boost.JSON: parse", [](const std::string & s, result & r) { boost::json::monotonic_resource mr; boost::system::error_code ec; auto v = boost::json::parse(s, ec, &mr, bj_precise); if (ec) { return false; } walk(v, r.st); return true; }},
+ {"Boost.JSON: SAX", [](const std::string & s, result & r) { return bj_sax(s, &r.st); }},
+ };
+}
+
+static std::vector select_variants(context& c)
+{
+ const std::string n = c.name;
+ return
+ {
+ {"nlohmann json_document: parse", [n](const std::string & s, result & r) { auto d = json_document::parse(s); r.acc = pick(n, d.root()); return true; }},
+ {"nlohmann json_document: read (reused)", [&c, n](const std::string & s, result & r) { c.reused.read(s); r.acc = pick(n, c.reused.root()); return true; }},
+ {"nlohmann json: parse", [n](const std::string & s, result & r) { json j = json::parse(s); r.acc = pick(n, &j); return true; }},
+ {"nlohmann json_document: structs (from_json)", [n](const std::string & s, result & r) { auto d = json_document::parse(s); r.acc = pick_nlohmann_structs(n, d.root()); return true; }},
+ {"nlohmann json: structs (from_json)", [n](const std::string & s, result & r) { r.acc = pick_nlohmann_structs(n, json::parse(s)); return true; }},
+ {"yyjson: read", [n](const std::string & s, result & r) { yyjson_doc* d = yyjson_read(s.data(), s.size(), 0); if (d == nullptr) { return false; } r.acc = pick(n, yyjson_doc_get_root(d)); yyjson_doc_free(d); return true; }},
+ {"yyjson: insitu (copy)", [n](const std::string & s, result & r) { yy_insitu d(s); if (d.doc == nullptr) { return false; } r.acc = pick(n, yyjson_doc_get_root(d.doc)); return true; }},
+ {"simdjson: DOM", [n](const std::string & s, result & r) { simdjson::dom::parser p; auto e = p.parse(s); if (e.error()) { return false; } r.acc = pick(n, e.value_unsafe()); return true; }},
+ {"simdjson: DOM (reused)", [&c, n](const std::string & s, result & r) { auto e = c.sj.parse(s); if (e.error()) { return false; } r.acc = pick(n, e.value_unsafe()); return true; }},
+ {"simdjson: On-Demand", [n](const std::string & s, result & r) { simdjson::ondemand::parser p; auto d = p.iterate(s).value(); r.acc = pick_od(n, d); return true; }},
+ {"simdjson: On-Demand (reused)", [&c, n](const std::string & s, result & r) { auto d = c.od.iterate(s).value(); r.acc = pick_od(n, d); return true; }},
+ {"sonic-cpp: Document", [n](const std::string & s, result & r) { sonic_json::Document d; d.Parse(s.data(), s.size()); if (d.HasParseError()) { return false; } r.acc = pick(n, &d); return true; }},
+#if !BENCH_NO_GLAZE
+ {"Glaze: structs", [n](const std::string & s, result & r) { return pick_glaze_structs(n, s, r.acc); }},
+ {"Glaze: structs (validate_skipped)", [n](const std::string & s, result & r) { return pick_glaze_structs(n, s, r.acc); }},
+ {"Glaze: generic", [n](const std::string & s, result & r) { glz::generic_u64 j; if (glz::read_json(j, s)) { return false; } r.acc = pick(n, &j); return true; }},
+ {"Glaze: lazy_json", [n](const std::string & s, result & r) { auto l = glz::lazy_json(s); if (!l) { return false; } r.acc = pick_glaze_lazy(n, *l); return true; }},
+#endif
+ {"Boost.JSON: parse", [n](const std::string & s, result & r) { boost::json::monotonic_resource mr; boost::system::error_code ec; auto v = boost::json::parse(s, ec, &mr, bj_precise); if (ec) { return false; } r.acc = pick(n, &v); return true; }},
+ };
+}
+
+static std::vector edit_variants(context& c)
+{
+ const std::string n = c.name;
+ return
+ {
+ {"nlohmann json_document: editable", [n](const std::string & s, result & r) { return edit_document(n, s, r, false); }},
+ {"nlohmann json_document: editable (source numbers)", [n](const std::string & s, result & r) { return edit_document(n, s, r, true); }},
+ {"nlohmann json: parse", [n](const std::string & s, result & r) { json j = json::parse(s); edit(n, j); return emit(r, j.dump()); }},
+ {"nlohmann json: ordered_json", [n](const std::string & s, result & r) { ordered_json j = ordered_json::parse(s); edit(n, j); return emit(r, j.dump()); }},
+ {"yyjson: mut copy", [n](const std::string & s, result & r) { yyjson_doc* d = yyjson_read(s.data(), s.size(), 0); const bool ok = edit_yyjson(n, d, r); yyjson_doc_free(d); return ok; }},
+ {"yyjson: insitu (copy) + mut copy", [n](const std::string & s, result & r) { yy_insitu d(s); return edit_yyjson(n, d.doc, r); }},
+ {"sonic-cpp: Document", [n](const std::string & s, result & r) { return edit_sonic(n, s, r); }},
+#if !BENCH_NO_GLAZE
+ {"Glaze: generic", [n](const std::string & s, result & r) { return edit_glaze(n, s, r); }},
+#endif
+ {"Boost.JSON: parse", [n](const std::string & s, result & r) { return edit_boost(n, s, r); }},
+ };
+}
+
+static std::vector minify_variants(context& c)
+{
+ return
+ {
+ {"nlohmann json_document: parse + dump", [](const std::string & s, result & r) { auto d = json_document::parse(s); return emit(r, d.root().dump()); }},
+ {"nlohmann json_document: parse + dump (source numbers)", [](const std::string & s, result & r) { auto d = json_document::parse(s); return emit(r, d.root().dump(-1, ' ', false, json_view::number_format::source)); }},
+ {"nlohmann json_document: read + dump (reused)", [&c](const std::string & s, result & r) { c.reused.read(s); return emit(r, c.reused.root().dump()); }},
+ {"nlohmann json: parse + dump", [](const std::string & s, result & r) { json j = json::parse(s); return emit(r, j.dump()); }},
+ {"yyjson: read + write", [](const std::string & s, result & r) { yyjson_doc* d = yyjson_read(s.data(), s.size(), 0); if (d == nullptr) { return false; } std::size_t n = 0; char* o = yyjson_write(d, 0, &n); const bool ok = o != nullptr && emit(r, o, n); std::free(o); yyjson_doc_free(d); return ok; }},
+ {"yyjson: insitu (copy) + write", [](const std::string & s, result & r) { yy_insitu d(s); if (d.doc == nullptr) { return false; } std::size_t n = 0; char* o = yyjson_write(d.doc, 0, &n); const bool ok = o != nullptr && emit(r, o, n); std::free(o); return ok; }},
+ {"simdjson: DOM + to_string", [](const std::string & s, result & r) { simdjson::dom::parser p; auto e = p.parse(s); if (e.error()) { return false; } return emit(r, simdjson::to_string(e.value_unsafe())); }},
+ {"simdjson: DOM (reused) + to_string", [&c](const std::string & s, result & r) { auto e = c.sj.parse(s); if (e.error()) { return false; } return emit(r, simdjson::to_string(e.value_unsafe())); }},
+ {"simdjson: minify", [](const std::string & s, result & r) { std::unique_ptr o(new char[s.size() + simdjson::SIMDJSON_PADDING]); std::size_t n = 0; if (simdjson::minify(s.data(), s.size(), o.get(), n)) { return false; } return emit(r, o.get(), n); }},
+ {"sonic-cpp: Document + Serialize", [](const std::string & s, result & r) { sonic_json::Document d; d.Parse(s.data(), s.size()); if (d.HasParseError()) { return false; } sonic_json::WriteBuffer wb; return d.Serialize(wb) == sonic_json::kErrorNone && emit(r, wb.ToString(), wb.Size()); }},
+#if !BENCH_NO_GLAZE
+ {"Glaze: generic", [](const std::string & s, result & r) { glz::generic_u64 j; if (glz::read_json(j, s)) { return false; } std::string o; return !glz::write_json(j, o) && emit(r, std::move(o)); }},
+ // minify_json takes a resizable input but only reads it
+ {"Glaze: minify_json", [](const std::string & s, result & r) { std::string o; return !glz::minify_json(const_cast(s), o) && emit(r, std::move(o)); }},
+#endif
+ {"Boost.JSON: parse + serialize", [](const std::string & s, result & r) { boost::json::monotonic_resource mr; boost::system::error_code ec; auto v = boost::json::parse(s, ec, &mr, bj_precise); if (ec) { return false; } return emit(r, boost::json::serialize(v)); }},
+ };
+}
+
+// RapidJSON, once with UTF-8 validation (kParseValidateEncodingFlag) and once without; both with
+// kParseFullPrecisionFlag, without which floats are not correctly rounded and sum/edit/minify give wrong results
+constexpr unsigned rj_fast = rapidjson::kParseFullPrecisionFlag;
+
+template
+static void add_rapidjson(std::vector& vs, const std::string& uc, const std::string& n, const char* sfx)
+{
+ if (uc == "validate")
+ {
+ vs.push_back({std::string("RapidJSON: SAX") + sfx, [](const std::string & s, result&) { rapidjson::BaseReaderHandler<> h; return rj_read(s, h); }});
+ vs.push_back({std::string("RapidJSON: Document") + sfx, [](const std::string & s, result&) { rapidjson::Document d; d.Parse(s.data(), s.size()); return !d.HasParseError(); }});
+ vs.push_back({std::string("RapidJSON: insitu (copy)") + sfx, [](const std::string & s, result&) { std::string b = s; rapidjson::Document d; d.ParseInsitu(&b[0]); return !d.HasParseError(); }});
+ }
+ else if (uc == "sum")
+ {
+ vs.push_back({std::string("RapidJSON: Document") + sfx, [](const std::string & s, result & r) { rapidjson::Document d; d.Parse(s.data(), s.size()); if (d.HasParseError()) { return false; } walk(d, r.st); return true; }});
+ vs.push_back({std::string("RapidJSON: SAX") + sfx, [](const std::string & s, result & r) { rj_sum h(&r.st); return rj_read(s, h); }});
+ vs.push_back({std::string("RapidJSON: insitu (copy)") + sfx, [](const std::string & s, result & r) { std::string b = s; rapidjson::Document d; d.ParseInsitu(&b[0]); if (d.HasParseError()) { return false; } walk(d, r.st); return true; }});
+ }
+ else if (uc == "select")
+ {
+ vs.push_back({std::string("RapidJSON: Document") + sfx, [n](const std::string & s, result & r) { rapidjson::Document d; d.Parse(s.data(), s.size()); if (d.HasParseError()) { return false; } r.acc = pick(n, &d); return true; }});
+ vs.push_back({std::string("RapidJSON: insitu (copy)") + sfx, [n](const std::string & s, result & r) { std::string b = s; rapidjson::Document d; d.ParseInsitu(&b[0]); if (d.HasParseError()) { return false; } r.acc = pick(n, &d); return true; }});
+ }
+ else if (uc == "edit")
+ {
+ vs.push_back({std::string("RapidJSON: Document") + sfx, [n](const std::string & s, result & r) { rapidjson::Document d; d.Parse(s.data(), s.size()); return !d.HasParseError() && edit_rapidjson(n, d, r); }});
+ vs.push_back({std::string("RapidJSON: insitu (copy)") + sfx, [n](const std::string & s, result & r) { std::string b = s; rapidjson::Document d; d.ParseInsitu(&b[0]); return !d.HasParseError() && edit_rapidjson(n, d, r); }});
+ }
+ else if (uc == "minify")
+ {
+ vs.push_back({std::string("RapidJSON: Document + Writer") + sfx, [](const std::string & s, result & r) { rapidjson::Document d; d.Parse(s.data(), s.size()); if (d.HasParseError()) { return false; } rapidjson::StringBuffer sb; rapidjson::Writer w(sb); return d.Accept(w) && emit(r, sb.GetString(), sb.GetSize()); }});
+ vs.push_back({std::string("RapidJSON: SAX to Writer") + sfx, [](const std::string & s, result & r) { rapidjson::StringBuffer sb; rapidjson::Writer w(sb); return rj_read(s, w) && emit(r, sb.GetString(), sb.GetSize()); }});
+ vs.push_back({std::string("RapidJSON: SAX to Writer (raw numbers)") + sfx, [](const std::string & s, result & r) { rapidjson::StringBuffer sb; rj_raw_writer w(sb); return rj_read(s, w) && emit(r, sb.GetString(), sb.GetSize()); }});
+ }
+ (void)n;
+}
+
+// write: each library's document is parsed once, outside the timer; the input argument is ignored
+static std::vector write_variants(const std::string& s)
+{
+ auto view = std::make_shared(json_document::parse(s));
+ auto j = std::make_shared(json::parse(s));
+ std::shared_ptr yy(yyjson_read(s.data(), s.size(), 0), yyjson_doc_free);
+ auto sjp = std::make_shared();
+ const simdjson::dom::element sje = sjp->parse(s).value();
+ auto rj = std::make_shared();
+ rj->Parse(s.data(), s.size());
+ auto so = std::make_shared();
+ so->Parse(s.data(), s.size());
+#if !BENCH_NO_GLAZE
+ auto gl = std::make_shared();
+ (void)glz::read_json(*gl, s);
+#endif
+ auto bj = std::make_shared(boost::json::parse(s, boost::json::make_shared_resource(), bj_precise));
+ return
+ {
+ {"nlohmann json_document: dump", [view](const std::string&, result & r) { return emit(r, view->root().dump()); }},
+ {"nlohmann json_document: dump (source numbers)", [view](const std::string&, result & r) { return emit(r, view->root().dump(-1, ' ', false, json_view::number_format::source)); }},
+ {"nlohmann json: dump", [j](const std::string&, result & r) { return emit(r, j->dump()); }},
+ {"yyjson: write", [yy](const std::string&, result & r) { std::size_t n = 0; char* o = yyjson_write(yy.get(), 0, &n); const bool ok = o != nullptr && emit(r, o, n); std::free(o); return ok; }},
+ {"simdjson: to_string", [sjp, sje](const std::string&, result & r) { return emit(r, simdjson::to_string(sje)); }},
+ {"RapidJSON: Writer", [rj](const std::string&, result & r) { rapidjson::StringBuffer sb; rapidjson::Writer w(sb); return rj->Accept(w) && emit(r, sb.GetString(), sb.GetSize()); }},
+ {"sonic-cpp: Serialize", [so](const std::string&, result & r) { sonic_json::WriteBuffer wb; return so->Serialize(wb) == sonic_json::kErrorNone && emit(r, wb.ToString(), wb.Size()); }},
+#if !BENCH_NO_GLAZE
+ {"Glaze: generic", [gl](const std::string&, result & r) { std::string o; return !glz::write_json(*gl, o) && emit(r, std::move(o)); }},
+#endif
+ {"Boost.JSON: serialize", [bj](const std::string&, result & r) { return emit(r, boost::json::serialize(*bj)); }},
+ };
+}
+
+///////////////////////////////////////////////////////////////////////////////
+// driver
+///////////////////////////////////////////////////////////////////////////////
+
static std::string slurp(const std::string& p)
{
std::ifstream f(p, std::ios::binary);
@@ -854,25 +2186,156 @@ static std::string slurp(const std::string& p)
return ss.str();
}
-struct engine
+// the input contract: at least 64 bytes of spare capacity
+static std::string padded(const std::string& s)
{
- std::string name;
- std::function 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);
+ std::string r;
+ r.reserve(s.size() + 64);
+ r.assign(s);
+ return r;
}
-constexpr auto rj_flags = rapidjson::kParseFullPrecisionFlag; // correctly rounded floats, as every other engine
+struct bad_input
+{
+ std::string what, text;
+ bool utf8; // the 0xFF probe; the others are structural errors
+};
+
+// malformed inputs derived from a valid document: truncated by one byte (after trailing whitespace), a stray comma
+// before the first '}' outside a string, and a 0xFF byte right after the opening quote of the first string value (not
+// a key, so that a library that accepts the byte still finds the keys the use cases look up) in the second half of the
+// document, or of the last string value if the second half has none (canada.json)
+static std::vector