Merge branch develop into claude/perf-contiguous-parser

Resolve the conflict in docs/mkdocs/docs/features/files.md (keep the
measured ifstream ratio of this PR, use develop's json_view/index.md link)
and regenerate the single include headers.

Signed-off-by: Niels Lohmann <mail@nlohmann.me>
This commit is contained in:
Niels Lohmann committed 2026-11-09 09:07:53 +01:00
commit b2d379fe84
126 files changed
+74739 -763

No files matched your search

+3
View File
@@ -138,6 +138,9 @@ the `View*` benchmarks (in `src/benchmarks_view.cpp`) are only built if the dire
Only benchmarks present in both result files are compared, so for older releases, either filter the benchmarks or
build that release's own `tests/benchmarks` against its own header.
To find out *which commit* changed a number, [`history/`](history/README.md) times one fixed program against the
header of every commit that changed it.
## Getting stable numbers
- Build and run both versions on the same machine with the same compiler and Google Benchmark, one right after the
+43
View File
@@ -0,0 +1,43 @@
# Performance history
The benchmarks in [`tests/benchmarks`](../README.md) compare two versions. The program here answers a different
question: *which commit* made something faster or slower. `history.cpp` is one fixed benchmark that uses only API
3.12.0 already has. `history.py` compiles it against the `single_include` of every first-parent commit that changed
`json.hpp` or `json_view.hpp`, runs all of them interleaved, and writes one CSV row per commit with its pull request.
The timeline in the post [How 3.13.0 got faster](https://json.nlohmann.me/home/blog/how_3_13_0_got_faster/) comes from
it. It is not built by CMake or run by CI.
## Workloads
| name | what it does |
|---|---|
| `parse_twitter`, `parse_citm`, `parse_canada` | `json::parse()` of a `std::string` (`citm_catalog.json` as distributed is indented by four spaces) |
| `parse_citm_compact` | the same with a minified copy of `citm_catalog.json` |
| `parse_twitter_ifstream` | `json::parse()` of an `std::ifstream`, opening the file included |
| `accept_twitter` | `json::accept()` |
| `view_parse_*` | `json_document::parse()`; only where the header has `json_view.hpp` |
| `dump_twitter`, `dump_citm`, `dump_canada`, `dump4_citm` | `dump()` and `dump(4)` |
| `from_cbor_twitter`, `to_cbor_twitter` | read and write CBOR |
| `copy_destroy_citm` | copy a parsed document and destroy the copy |
| `equal_citm` | copy a parsed document and compare it with `==` |
Each workload runs in batches of at least 20 ms; the program prints the median of 9 batches in microseconds per call.
## Running it
```sh
mkdir -p ~/history/data
cp <json_test_data>/nativejson-benchmark/{twitter,citm_catalog,canada}.json ~/history/data/
python3 history.py build --repo ../../.. --work ~/history # about 300 builds, a few minutes in parallel
caffeinate -i python3 history.py run --work ~/history --rounds 4 # about 1 h 45 min on an M1 Max (about 4 s per binary and round)
python3 history.py summarize --work ~/history > history.csv
```
`--extra DIR` adds one more header directory, such as an amalgamation of `develop` with open pull requests merged
in. `run` waits whenever the 1-minute load average is 4 or more.
## Reading the results
A single commit can move a workload by a few percent through code layout alone: the functions it did not touch
move to other addresses. Read a change as a step only if it is larger than that and stays. To check one, time the
two commits against each other with more rounds, or compare instruction counts.
+98
View File
@@ -0,0 +1,98 @@
// __ _____ _____ _____
// __| | __| | | | 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
// One fixed benchmark, compiled against the json.hpp of every commit that changed it (see README.md).
// It uses only API that 3.12.0 has; the json_document rows exist only where the header has json_view.hpp.
// Usage: history <dir with twitter.json, citm_catalog.json, canada.json and citm_compact.json>
// Prints one line per workload: the name and the median of 9 batches in microseconds per call.
#include <nlohmann/json.hpp>
#if __has_include(<nlohmann/json_view.hpp>) && !defined(HISTORY_NO_VIEW)
#include <nlohmann/json_view.hpp>
#define HISTORY_VIEW 1
#endif
#include <algorithm>
#include <chrono>
#include <fstream>
#include <functional>
#include <iomanip>
#include <iostream>
#include <sstream>
#include <string>
#include <vector>
using nlohmann::json;
static std::string slurp(const std::string& p)
{
std::ifstream f(p, std::ios::binary);
std::stringstream s;
s << f.rdbuf();
return s.str();
}
static volatile std::size_t sink;
static void bench(const char* name, const std::function<std::size_t()>& f)
{
using clk = std::chrono::steady_clock;
sink = f(); // warm up
std::size_t n = 1;
for (;;)
{
auto t0 = clk::now();
for (std::size_t i = 0; i < n; ++i)
{
sink = f();
}
double dt = std::chrono::duration<double>(clk::now() - t0).count();
if (dt > 0.02)
{
break;
}
n *= 2;
}
std::vector<double> v;
for (int b = 0; b < 9; ++b)
{
auto t0 = clk::now();
for (std::size_t i = 0; i < n; ++i)
{
sink = f();
}
v.push_back(std::chrono::duration<double, std::micro>(clk::now() - t0).count() / n);
}
std::sort(v.begin(), v.end());
std::cout << name << ' ' << std::fixed << std::setprecision(3) << v[4] << '\n';
}
int main(int, char** argv)
{
std::string d = argv[1];
const std::string tw = slurp(d + "/twitter.json"), ci = slurp(d + "/citm_catalog.json"),
ca = slurp(d + "/canada.json"), cii = slurp(d + "/citm_compact.json");
const json jtw = json::parse(tw), jci = json::parse(ci), jca = json::parse(ca);
const std::vector<std::uint8_t> cbtw = json::to_cbor(jtw);
bench("parse_twitter", [&] { return json::parse(tw).size(); });
bench("parse_citm", [&] { return json::parse(ci).size(); });
bench("parse_canada", [&] { return json::parse(ca).size(); });
bench("parse_citm_compact", [&] { return json::parse(cii).size(); });
bench("parse_twitter_ifstream", [&] { std::ifstream f(d + "/twitter.json"); return json::parse(f).size(); });
bench("accept_twitter", [&] { return static_cast<std::size_t>(json::accept(tw)); });
#ifdef HISTORY_VIEW
bench("view_parse_twitter", [&] { const auto doc = nlohmann::json_document::parse(tw); return doc.root().size(); });
bench("view_parse_citm", [&] { const auto doc = nlohmann::json_document::parse(ci); return doc.root().size(); });
bench("view_parse_canada", [&] { const auto doc = nlohmann::json_document::parse(ca); return doc.root().size(); });
#endif
bench("dump_twitter", [&] { return jtw.dump().size(); });
bench("dump_citm", [&] { return jci.dump().size(); });
bench("dump_canada", [&] { return jca.dump().size(); });
bench("dump4_citm", [&] { return jci.dump(4).size(); });
bench("from_cbor_twitter", [&] { return json::from_cbor(cbtw).size(); });
bench("to_cbor_twitter", [&] { return json::to_cbor(jtw).size(); });
bench("copy_destroy_citm", [&] { json c = jci; return c.size(); });
bench("equal_citm", [&] { json c = jci; return static_cast<std::size_t>(c == jci); });
}
+151
View File
@@ -0,0 +1,151 @@
#!/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
"""Time one fixed benchmark against the json.hpp of every commit that changed it.
Usage:
history.py build --repo REPO --work DIR [--from v3.12.0] [--to origin/develop] [--extra INCLUDE_DIR]
history.py run --work DIR [--rounds 4]
history.py summarize --work DIR [--repo REPO] > history.csv
build: lists the first-parent commits between --from and --to that changed single_include/nlohmann/json.hpp or
json_view.hpp, extracts single_include/ of each (and of --from itself), and compiles history.cpp against it into
DIR/bin/NNN, NNN being the line of the commit in DIR/commits.txt. --extra adds one more header directory at the end,
for example an amalgamation of develop with open pull requests merged in. It also writes the input files to DIR/data
(the nativejson-benchmark files from the test data, which must be in DIR/data already, plus a minified copy of
citm_catalog.json).
run: runs every binary once per round, in a new random order each round, and only while the 1-minute load average is
below 4. Output: DIR/res/NNN-R.txt.
summarize: the median over the rounds per binary and workload, as CSV with the commit, its date and its pull request.
Only the Python 3 standard library is used; a C++17 compiler (c++) and git are needed.
"""
import argparse
import concurrent.futures
import csv
import json
import os
import random
import re
import statistics
import subprocess
import sys
import time
from pathlib import Path
HERE = Path(__file__).resolve().parent
FLAGS = ["-std=c++17", "-O3", "-DNDEBUG", "-w"]
def git(repo, *args):
return subprocess.run(["git", "-C", str(repo), *args], check=True, capture_output=True, text=True).stdout
def build_one(work, idx, rev, repo):
hdr = work / "hdr" / idx
hdr.mkdir(parents=True, exist_ok=True)
if rev.startswith("dir:"):
subprocess.run(["cp", "-R", rev[4:] + "/nlohmann", str(hdr)], check=True)
else:
archive = subprocess.run(["git", "-C", str(repo), "archive", rev, "single_include/nlohmann"],
check=True, capture_output=True).stdout
subprocess.run(["tar", "-x", "-C", str(hdr), "--strip-components=1"], input=archive, check=True)
out = work / "bin" / idx
cmd = ["c++", *FLAGS, "-I", str(hdr), str(HERE / "history.cpp"), "-o", str(out)]
# json_view.hpp of the first commits that had it may not compile with history.cpp: time json.hpp alone then
if subprocess.run(cmd, capture_output=True).returncode != 0:
if subprocess.run(cmd[:-4] + ["-DHISTORY_NO_VIEW"] + cmd[-4:], capture_output=True).returncode != 0:
return "FAIL %s %s" % (idx, rev)
return None
def build(a):
work, repo = Path(a.work), Path(a.repo)
(work / "bin").mkdir(parents=True, exist_ok=True)
revs = [a.__dict__["from"]] + git(repo, "log", "--first-parent", "--reverse", "--format=%H",
"%s..%s" % (a.__dict__["from"], a.to), "--",
"single_include/nlohmann/json.hpp", "single_include/nlohmann/json_view.hpp").split()
if a.extra:
revs.append("dir:" + str(Path(a.extra).resolve()))
(work / "commits.txt").write_text("\n".join(revs) + "\n")
data = work / "data"
citm = json.loads((data / "citm_catalog.json").read_text(encoding="utf-8"))
(data / "citm_compact.json").write_text(json.dumps(citm, separators=(",", ":"), ensure_ascii=False), encoding="utf-8")
with concurrent.futures.ThreadPoolExecutor(os.cpu_count()) as ex:
for r in ex.map(lambda p: build_one(work, "%03d" % p[0], p[1], repo), enumerate(revs)):
if r:
print(r, file=sys.stderr)
def load():
out = subprocess.run(["sysctl", "-n", "vm.loadavg"], capture_output=True, text=True).stdout
return float(out.split()[1]) if out else os.getloadavg()[0]
def run(a):
work = Path(a.work)
(work / "res").mkdir(exist_ok=True)
bins = sorted(p.name for p in (work / "bin").iterdir())
for r in range(1, a.rounds + 1):
random.shuffle(bins)
for b in bins:
while load() >= 4:
time.sleep(15)
out = subprocess.run([str(work / "bin" / b), str(work / "data")], check=True, capture_output=True, text=True)
(work / "res" / ("%s-%d.txt" % (b, r))).write_text(out.stdout)
print("round %d done" % r, file=sys.stderr)
def summarize(a):
work = Path(a.work)
revs = (work / "commits.txt").read_text().split()
times, names = {}, []
for f in sorted((work / "res").glob("*.txt")):
idx = int(f.name.split("-")[0])
for line in f.read_text().splitlines():
name, us = line.split()
if name not in names:
names.append(name)
times.setdefault((idx, name), []).append(float(us))
w = csv.writer(sys.stdout, lineterminator="\n")
w.writerow(["index", "commit", "date", "pr", "subject"] + names)
for idx, rev in enumerate(revs):
if rev.startswith("dir:"):
sha, date, pr, subject = "candidate", "", "", "develop + open pull requests"
else:
sha, date, subject = git(a.repo, "log", "-1", "--format=%h%x09%cs%x09%s", rev).strip().split("\t")
m = re.search(r"\(#(\d+)\)$", subject)
pr = m.group(1) if m else ""
w.writerow([idx, sha, date, pr, subject] +
["%.3f" % statistics.median(times[(idx, n)]) if (idx, n) in times else "" for n in names])
def main():
p = argparse.ArgumentParser(description=__doc__.splitlines()[0])
sub = p.add_subparsers(dest="cmd", required=True)
b = sub.add_parser("build")
b.add_argument("--repo", required=True)
b.add_argument("--work", required=True)
b.add_argument("--from", default="v3.12.0")
b.add_argument("--to", default="origin/develop")
b.add_argument("--extra")
r = sub.add_parser("run")
r.add_argument("--work", required=True)
r.add_argument("--rounds", type=int, default=4)
s = sub.add_parser("summarize")
s.add_argument("--work", required=True)
s.add_argument("--repo", default=str(HERE.parents[2]))
a = p.parse_args()
{"build": build, "run": run, "summarize": summarize}[a.cmd](a)
if __name__ == "__main__":
main()
+249
View File
@@ -0,0 +1,249 @@
# 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 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
`<library>: <variant>` (`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.
| use case | what it does |
|---|---|
| `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 |
The edits:
| 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`, the first
status of `twitter.json` (`status`; written by `json::dump()`, timed in batches of 200), and `twitter_min` and
`citm_catalog_min`: `twitter.json` and `citm_catalog.json` with all whitespace outside strings removed, built in memory at
startup by the same small function in both programs (`strip_ws`; the bytes inside strings are untouched). The minified
files use the select and edit of their originals.
### 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)`.
The C++ in-situ variants also run as `(copy untimed)`: the copy is made before the timer starts and freed after it
stops, as for a program that reads a file into a buffer it owns and no longer needs. They are reported but never
chosen as a library's best. `json_document`'s `parse_copy` is the opposite case: a document that owns a copy of its
text.
Each timed call starts from the text and frees everything it built, except variants named `(reused)` or
`(reused allocator)`, which keep a parser, document or allocator across calls (simdjson's reused parsers,
`json_document::read()`, Boost.JSON's reset `parser` and `basic_parser`, yyjson's dynamic allocator, RapidJSON's
user-buffer allocators, simd-json's buffers). These are reported but never chosen as a library's best;
`summarize.py` reports each library's best reused variant separately. 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`. A variant that does not convert numbers (`(raw numbers)`,
`(numbers: none)`) has to convert the numbers it selects exactly itself.
- `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: <file> <use case> | <variant> | <reason>` and not timed.
### Validation levels
Except for `write`, every variant is also probed with four malformed inputs derived from each file: the file
truncated by one byte (after trailing whitespace), the file followed by ` ]x` (trailing garbage), a stray `,` before
the first `}` (before the last `]` in a file without objects), 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; a file without string values gets no `0xFF` probe). 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 four probes |
| `no UTF-8` | rejects the structural probes, accepts the `0xFF` byte |
| `partial` | accepts the truncated input, the trailing garbage 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: <file> <use case> | <variant> | <level> | <what>`. 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.
The four probes do not cover everything a parser can get wrong. `--conformance <dir>` runs every `validate` variant
on every file of [JSONTestSuite](https://github.com/nst/JSONTestSuite)'s `test_parsing` (json_test_data has it as
`nst_json_testsuite2/test_parsing`) and prints `conformance: <file> | <variant> | accepted|rejected|crashed`; a `y_`
file must be accepted, an `n_` file rejected, and an `i_` file may go either way. bench_all runs each call in a child
process (a call that hangs for 60 s counts as crashed); a stack overflow aborts bench_rs, so `conformance-rs.sh`
runs it one file per process and, for a file that aborts, one variant per process (`CONF_VARIANT`). The trailing
garbage probe was added after this check: Glaze reads anything after the value unless `validate_trailing_whitespace`
is set.
### 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`, `parse_copy`, `read (reused)` | `parse`, `parse_copy`, `structs (from_json)`, `read (reused)` | `editable`, `editable (source numbers)` (`json_editable_document`: set/erase, `dump` with `number_format::source`) | `parse + dump`, `parse_copy + 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)`, `insitu (copy untimed)`, `read (raw numbers)`, `read (reused allocator)` | `read`, `insitu (copy)`, `insitu (copy untimed)`, `read (reused allocator)` | `read`, `insitu (copy)`, `insitu (copy untimed)`, `read (raw numbers)`, `read (reused allocator)` | `mut copy`, `insitu (copy) + mut copy` (`yyjson_doc_mut_copy`), `insitu (copy untimed) + mut copy` | `read + write`, `insitu (copy) + write`, `insitu (copy untimed) + write`, `read (raw numbers) + write`, `read + write (reused allocator)` | `write` |
| simdjson | `DOM`, `DOM (reused)` | `DOM`, `DOM (reused)`, `On-Demand`, `On-Demand (reused)` | `DOM`, `DOM (reused)`, `On-Demand` [P], `On-Demand (reused)` [P], `On-Demand structs` [P], `On-Demand (key selectors)` [P] | n/a (no mutable document) | `DOM + to_string`, `DOM (reused) + to_string`, `minify` [P] | `to_string` |
| RapidJSON | `SAX` (null handler), `Document`, `insitu (copy)`, `insitu (copy untimed)`, `SAX (string stream)`, `SAX (default precision)`, `SAX (string stream, default precision)`, `SAX (raw numbers)`, `Document (reused allocator)` | `Document`, `SAX`, `insitu (copy)`, `insitu (copy untimed)`, `SAX (string stream)`, `Document (reused allocator)` | `Document`, `insitu (copy)`, `insitu (copy untimed)`, `Document (string stream)`, `Document (raw numbers)`, `Document (reused allocator)` | `Document`, `insitu (copy)`, `insitu (copy untimed)`, `Document (string stream)` | `Document + Writer`, `SAX to Writer`, `SAX to Writer (string stream)`, `SAX to Writer (raw numbers)`, `Document + Writer (reused allocator)` | `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` [P], `generic (double)` [P], `generic (strict)`, `generic (double, strict)` | `generic` [P], `generic (double)` [P], `generic (strict)`, `generic (double, strict)`, `lazy_json` [P], `lazy_json (string_view)` (x) | `structs` [P], `structs (validate_skipped)`, `generic` [P], `generic (double)` [P], `generic (strict)`, `generic (double, strict)`, `lazy_json` [P], `lazy_json (string_view)` (x) | `generic` [P], `generic (double)` (x), `generic (strict)`, `generic (double, strict)` (x) | `generic` [P], `generic (double)` (x), `generic (strict)`, `generic (double, strict)` (x), `minify_json` [P] | `generic`, `generic (double)` (x), `generic (strict)`, `generic (double, strict)` (x) |
| Boost.JSON | `SAX` (`basic_parser`), `parse`, `SAX (numbers: none)`, `parse (numbers: none)`, `SAX (reused)`, `parser (reused)` | `parse`, `SAX`, `SAX (reused)`, `parser (reused)` | `parse`, `parse (numbers: none)` (x), `parser (reused)`, `parse_into structs` | `parse` | `parse + serialize`, `parser (reused) + 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` |
(x): excluded for a wrong result on some file, and so in the summary for every file: `generic (double)` loses the
64-bit ids of `twitter.json` when it writes them; `lazy_json (string_view)` reads the raw bytes of a string, so escaped
strings have the wrong length (`sum`, and the `"` in 19,077 `jeopardy.json` categories in `select`);
`numbers: none` reads every number as 0.
`structs` read into types that declare only the fields `select` uses and hold the same data: nlohmann/json's
`from_json()` of the `NLOHMANN_DEFINE_TYPE_NON_INTRUSIVE` macro (from a `json` value or from a `json_document`'s view)
reads into `nsel`, where the arrays and objects that are only counted (`seatCategories`, the values of `events`,
`coordinates`) are a `count_only` holding their size. Glaze reads into the same types (reflected; `count_only` has a
reader that skips and counts the elements; `error_on_unknown_keys = false`, and with `validate_skipped` to validate
what it skips), and so does simdjson On-Demand (`get<T>()` with `tag_invoke`, fields in document order,
`count_elements()`/`count_fields()`). serde derive reads into the same fields with a `Count` that counts with
`IgnoredAny` (and a `BTreeMap`, as `std::map`). Boost.JSON's `parse_into` has no type that skips any value, so its
counted members only count what it converts into them: `seatCategories` into a counting sequence of an empty described
struct, `coordinates` into three levels of counting sequences down to `double`, and the `events` values into an empty
described struct, so their members are not counted. `key selectors` are simdjson 5.0's `object::for_each<"k1",
...>`, which walks each object once and matches keys with a compile-time perfect hash. `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; only `validate`, which reads no number, also has RapidJSON's `(default
precision)`. RapidJSON's `Document::Parse(data, size)` and our `SAX` read through `EncodedInputStream<MemoryStream>`;
`(string stream)` reads the NUL-terminated string through `StringStream`, as `Document::Parse(const char*)` does,
where RapidJSON has a SIMD string scanner (used only without `kParseValidateEncodingFlag`).
Variants named `(raw numbers)` or `(numbers: none)` use a library's mode that validates numbers without converting
them (yyjson `YYJSON_READ_NUMBER_AS_RAW`, RapidJSON `kParseNumbersAsStringsFlag`, Boost.JSON
`number_precision::none`), as `json_document` converts a number only when it is read. In `select` they convert the
selected numbers themselves (`std::from_chars`); Boost.JSON's `none` leaves no number text in its DOM, so it cannot.
yyjson writes a raw number verbatim, so `read (raw numbers) + write` is exact. sonic-cpp 1.0.2 has no parse flags,
and Glaze's generic DOM has no raw number mode (`validate_json` converts no number anyway). `(reused allocator)`:
yyjson's `yyjson_alc_dyn_new()`, RapidJSON's `MemoryPoolAllocator`s over user buffers for the values and the parse
stack; Boost.JSON's `parser (reused)` builds its value in a `monotonic_resource` over a buffer kept across calls. These
buffers grow after a call that needed more, so the timed calls do not allocate. `generic (double)` is `glz::generic`,
which stores every number as a double. The Glaze `(strict)` variants read with `validate_trailing_whitespace` (else
Glaze accepts anything after the value) and write with `escape_control_characters` (else Glaze writes a string's
control characters, e.g. U+0001, unescaped, which is not JSON; `gsoc-2018.json` has some).
| library | version | notes |
|---|---|---|
| `json_document`, `json` | this checkout | |
| yyjson | 0.13.0 | |
| simdjson | 5.1.1 | the single-header files of the tag |
| sonic-cpp | 1.0.2 | |
| RapidJSON | master 24b5e7a8b27f (2025-02-05) | |
| Glaze | 9.1.0 | `generic_u64` and `generic` (owning DOMs, 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:
```
55f8a675397c505f9bee6c160e1c26b0085868f185d04d7a7727531fdbff4bd4 glaze-v9.1.0.tar.gz
30edf9e47c417f653546c39295fdb66ba5f89fb37eb3780122c6ee66605e5837 sonic-cpp v1.0.2.tar.gz
2d2601a82d2d3b7e143a3c8d43ef616671391034bc46891a9816b79cf2d3e7a8 rapidjson 24b5e7a8b27f42fa16b96fc70aade9106cf7102f.tar.gz
34e0f62a2bc11ab20d601e8ca1cc2b2079503aa45119a19133d89d19b94a0fae yyjson 0.13.0.tar.gz
f3e5ee5f39051960b5ce65762ee4b6919caf270675eb52240ebd92f21a5bcefd simdjson v5.1.1 singleheader/simdjson.h
3ea5f501cef6c2e16e28095b566feeb56021ca473aaf50aad32102f45d734195 simdjson v5.1.1 singleheader/simdjson.cpp
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`). The order of the variants changes in every round (a shuffle with a
fixed seed; the first round keeps the listed order): on the Mac, a variant's time can depend on which variant ran
before it, by up to 15 %, and a fixed order would give each variant the same neighbour in every round. `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`
(`compare.py --download` fetches yyjson and Boost into its build directory, and simdjson 4.6.11, so take simdjson
5.1.1's `singleheader` files from its tag):
```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-5.1.1/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-5.1.1/singleheader -I $DL/boost_1_92_0 \
-I $DL/glaze-9.1.0/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 --locked && ./target/release/bench_rs <json_test_data> 30 > ../mac-rs-1.log
python3 summarize.py mac-cpp-*.log --rust mac-rs-*.log --json mac.json
```
With GCC 16 and Homebrew Clang on macOS, build the three steps with the same flags plus
`--sysroot=$(xcrun --show-sdk-path)` (`gcc-16`, `g++-16`) or `-isysroot $(xcrun --show-sdk-path)`
(`/opt/homebrew/opt/llvm/bin/clang`, `clang++`). The key selectors and simdjson's `tag_invoke` need C++20, so
`bench_all.cpp` needs `-std=c++20` or newer.
Both programs take `<json_test_data> [rounds] [file] [use case]`: `./bench_all <json_test_data> 30 canada edit` times
only the edit of `canada.json`; `""` stands for all files; the file names are `twitter`, `citm_catalog`, `canada`,
`jeopardy`, `status`, `twitter_min` and `citm_catalog_min`. The checks cover only the selected files and use cases.
bench_all uses `fork()` for the probes and the crash check (macOS, Linux).
`--files <dir> [rounds] [file] [use case]` runs `validate`, `sum`, `minify` and `write` (`select` and `edit` are
defined only for the files above) on every `*.json` file of a directory, named by their stem; files under 500 KB are
timed in batches of about 500 KB of input. It is meant for files the libraries were not tuned on, such as the
`jsonexamples` of [simdjson-data](https://github.com/simdjson/simdjson-data) without `twitter.json`,
`citm_catalog.json` and `canada.json`. `summarize.py` takes such logs as well.
On x86-64, use `-march=haswell -DRAPIDJSON_SSE42` instead of `-DRAPIDJSON_NEON` (sonic-cpp needs AVX2), pin the
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
A log line holds the file, the use case, and one cell per variant: `| <library>: <variant> [<level>] <time>us
(<ratio>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)` and `(reused allocator)` variants are never a library's best; a
`reused:` line below the library shows its best reused variant in the same way), (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": {...}, "best_reused": {...} or
null, "best_reused_any": {...} or null, "variants": {variant: {"us", "level"}}, "excluded": {...}}}}}` for the chart
scripts (`best_reused`: the best full reused variant; `best_reused_any`: at any level). 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.
File diff suppressed because it is too large. Load diff
+14
View File
@@ -0,0 +1,14 @@
#!/bin/bash
# the Rust conformance run, one file per process; a file that aborts the process (stack overflow) is rerun one
# variant per process, and a variant that aborts is printed as crashed
B=$1; D=$2
VARS=$($B --conformance $D 1 y_structure_lonely_int.json | sed -n 's/^conformance: [^|]* | \(.*\) | [a-z]*$/\1/p')
for f in $(ls $D | grep '\.json$'); do
out=$($B --conformance $D 1 "$f" 2>/dev/null)
if [ $? = 0 ]; then echo "$out" | grep '^conformance:'; continue; fi
while IFS= read -r v; do
o=$(CONF_VARIANT="$v" $B --conformance $D 1 "$f" 2>/dev/null)
if [ $? = 0 ]; then echo "$o" | grep '^conformance:'; else echo "conformance: $f | $v | crashed"; fi
done <<< "$VARS"
done
echo "done: conformance"
+627
View File
@@ -0,0 +1,627 @@
# 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",
"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,15 @@
[package]
name = "bench_rs"
version = "0.1.0"
edition = "2021"
[dependencies]
simd-json = "*"
sonic-rs = "*"
serde = { version = "1", features = ["derive"] }
serde_json = { version = "*", features = ["float_roundtrip"] }
[profile.release]
opt-level = 3
lto = "fat"
codegen-units = 1
@@ -0,0 +1,968 @@
// __ _____ _____ _____
// __| | __| | | | 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
// The Rust counterpart of bench_all.cpp: the same use cases (validate, sum, select, edit, minify, write), files,
// checks (right result; validation level from malformed-input probes) and timing method (every variant interleaved in every round, each
// timed call after an untimed one, best round); see README.md. The input is the file as bytes (&[u8]); simd-json
// parses in place, so its variants copy the input inside the timed call.
//
// Usage: bench_rs <json_test_data dir> [rounds] [document] [use case]
use serde::de::{DeserializeOwned, DeserializeSeed, IgnoredAny, MapAccess, SeqAccess, Visitor};
use simd_json::prelude::*;
use sonic_rs::{JsonContainerTrait, JsonValueMutTrait, JsonValueTrait};
use std::cell::RefCell;
use std::panic::{catch_unwind, AssertUnwindSafe};
use std::time::Instant;
#[derive(Default, Debug, Clone, Copy)]
struct Stats {
num: f64,
str_: usize,
nodes: usize,
}
// ---- sum: walk a DOM, visit every value, convert every number, touch every string and key
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) {
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();
}
}
_ => {}
}
}
// the same visit without a DOM: a serde visitor, driven by any serde deserializer, in document order
struct Sum<'a>(&'a mut Stats);
struct Key<'a>(&'a mut Stats);
impl<'de> DeserializeSeed<'de> for Sum<'_> {
type Value = ();
fn deserialize<D: serde::Deserializer<'de>>(self, d: D) -> Result<(), D::Error> {
d.deserialize_any(self)
}
}
impl<'de> Visitor<'de> for Sum<'_> {
type Value = ();
fn expecting(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
f.write_str("a JSON value")
}
fn visit_unit<E: serde::de::Error>(self) -> Result<(), E> {
self.0.nodes += 1;
Ok(())
}
fn visit_bool<E: serde::de::Error>(self, _: bool) -> Result<(), E> {
self.0.nodes += 1;
Ok(())
}
fn visit_i64<E: serde::de::Error>(self, v: i64) -> Result<(), E> {
self.0.nodes += 1;
self.0.num += v as f64;
Ok(())
}
fn visit_u64<E: serde::de::Error>(self, v: u64) -> Result<(), E> {
self.0.nodes += 1;
self.0.num += v as f64;
Ok(())
}
fn visit_f64<E: serde::de::Error>(self, v: f64) -> Result<(), E> {
self.0.nodes += 1;
self.0.num += v;
Ok(())
}
fn visit_str<E: serde::de::Error>(self, v: &str) -> Result<(), E> {
self.0.nodes += 1;
self.0.str_ += v.len();
Ok(())
}
fn visit_seq<A: SeqAccess<'de>>(self, mut a: A) -> Result<(), A::Error> {
let st = self.0;
st.nodes += 1;
while a.next_element_seed(Sum(&mut *st))?.is_some() {}
Ok(())
}
fn visit_map<A: MapAccess<'de>>(self, mut a: A) -> Result<(), A::Error> {
let st = self.0;
st.nodes += 1;
while a.next_key_seed(Key(&mut *st))?.is_some() {
a.next_value_seed(Sum(&mut *st))?;
}
Ok(())
}
}
impl<'de> DeserializeSeed<'de> for Key<'_> {
type Value = ();
fn deserialize<D: serde::Deserializer<'de>>(self, d: D) -> Result<(), D::Error> {
d.deserialize_str(self)
}
}
impl<'de> Visitor<'de> for Key<'_> {
type Value = ();
fn expecting(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
f.write_str("a key")
}
fn visit_str<E: serde::de::Error>(self, v: &str) -> Result<(), E> {
self.0.str_ += v.len();
Ok(())
}
}
fn sum_serde(s: &[u8], st: &mut Stats) -> bool {
let mut de = serde_json::Deserializer::from_slice(s);
Sum(st).deserialize(&mut de).is_ok() && de.end().is_ok()
}
fn sum_sonic(s: &[u8], st: &mut Stats) -> bool {
let mut de = sonic_rs::Deserializer::from_slice(s);
Sum(st).deserialize(&mut de).is_ok() && de.end().is_ok()
}
fn sum_simd(s: &[u8], st: &mut Stats) -> bool {
let mut b = s.to_vec();
match simd_json::Deserializer::from_slice(&mut b) {
Ok(mut de) => Sum(st).deserialize(&mut de).is_ok(),
Err(_) => false,
}
}
// ---- select: read a few fields of every record (the fields of bench_view.cpp)
// on a DOM; serde_json's is the reference
macro_rules! pick_value {
($fname:ident, $t:ty) => {
fn $fname(name: &str, r: &$t) -> f64 {
fn size(v: &$t) -> usize {
if let Some(a) = v.as_array() {
a.len()
} else {
v.as_object().unwrap().len()
}
}
fn status(acc: &mut f64, s: &$t) {
*acc += s["id"].as_u64().unwrap() as f64;
*acc += s["user"]["screen_name"].as_str().unwrap().len() as f64;
*acc += s["retweet_count"].as_i64().unwrap() as f64;
}
let mut acc = 0.0;
match name {
"twitter" => {
for s in r["statuses"].as_array().unwrap().iter() {
status(&mut acc, s);
}
}
"status" => status(&mut acc, r),
"citm_catalog" => {
for p in r["performances"].as_array().unwrap().iter() {
acc += (p["id"].as_u64().unwrap() + p["eventId"].as_u64().unwrap() + size(&p["seatCategories"]) as u64) as f64;
}
acc += size(&r["events"]) as f64;
}
"canada" => {
let g = &r["features"][0]["geometry"];
acc += (r["type"].as_str().unwrap().len() + g["type"].as_str().unwrap().len() + size(&g["coordinates"])) as f64;
}
"jeopardy" => {
for q in r.as_array().unwrap().iter() {
acc += if q["round"].as_str().unwrap() == "Final Jeopardy!" { 1.0 } else { 0.0 };
acc += q["category"].as_str().unwrap().len() as f64;
}
}
_ => {}
}
acc
}
};
}
pick_value!(pick_serde, serde_json::Value);
pick_value!(pick_sonic, sonic_rs::Value);
pick_value!(pick_simd, simd_json::BorrowedValue<'_>);
// into structs that declare only the fields the selection uses, the same data as nlohmann/json's from_json() structs in
// bench_all.cpp (nsel); unknown keys are skipped; Count counts the elements of an array or the members of an object
// (nlohmann's count_only)
mod sel {
use serde::de::{Deserializer, IgnoredAny, MapAccess, SeqAccess, Visitor};
use serde::Deserialize;
use std::collections::BTreeMap;
pub struct Count(pub usize);
impl<'de> Deserialize<'de> for Count {
fn deserialize<D: Deserializer<'de>>(d: D) -> Result<Count, D::Error> {
struct V;
impl<'de> Visitor<'de> for V {
type Value = Count;
fn expecting(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
f.write_str("an array or an object")
}
fn visit_seq<A: SeqAccess<'de>>(self, mut a: A) -> Result<Count, A::Error> {
let mut n = 0;
while a.next_element::<IgnoredAny>()?.is_some() {
n += 1;
}
Ok(Count(n))
}
fn visit_map<A: MapAccess<'de>>(self, mut a: A) -> Result<Count, A::Error> {
let mut n = 0;
while a.next_entry::<IgnoredAny, IgnoredAny>()?.is_some() {
n += 1;
}
Ok(Count(n))
}
}
d.deserialize_any(V)
}
}
#[derive(Deserialize)]
pub struct User {
pub screen_name: String,
}
#[derive(Deserialize)]
pub struct Status {
pub id: u64,
pub user: User,
pub retweet_count: i64,
}
#[derive(Deserialize)]
pub struct Twitter {
pub statuses: Vec<Status>,
}
#[derive(Deserialize)]
pub struct Performance {
pub id: u64,
#[serde(rename = "eventId")]
pub event_id: u64,
#[serde(rename = "seatCategories")]
pub seat_categories: Count,
}
#[derive(Deserialize)]
pub struct Citm {
pub events: BTreeMap<String, Count>,
pub performances: Vec<Performance>,
}
#[derive(Deserialize)]
pub struct Geometry {
#[serde(rename = "type")]
pub type_: String,
pub coordinates: Count,
}
#[derive(Deserialize)]
pub struct Feature {
pub geometry: Geometry,
}
#[derive(Deserialize)]
pub struct Canada {
#[serde(rename = "type")]
pub type_: String,
pub features: Vec<Feature>,
}
#[derive(Deserialize)]
pub struct Question {
pub category: String,
pub round: String,
}
}
trait De {
fn de<T: DeserializeOwned>(s: &[u8]) -> Option<T>;
}
struct SerdeJson;
struct SerdeJsonStr; // checks UTF-8 with std::str::from_utf8 first, then reads from the &str
struct SonicRs;
struct SimdJson;
impl De for SerdeJson {
fn de<T: DeserializeOwned>(s: &[u8]) -> Option<T> {
serde_json::from_slice(s).ok()
}
}
impl De for SerdeJsonStr {
fn de<T: DeserializeOwned>(s: &[u8]) -> Option<T> {
serde_json::from_str(std::str::from_utf8(s).ok()?).ok()
}
}
impl De for SonicRs {
fn de<T: DeserializeOwned>(s: &[u8]) -> Option<T> {
sonic_rs::from_slice(s).ok()
}
}
impl De for SimdJson {
fn de<T: DeserializeOwned>(s: &[u8]) -> Option<T> {
let mut b = s.to_vec();
simd_json::serde::from_slice(&mut b).ok()
}
}
fn pick_structs<D: De>(name: &str, s: &[u8]) -> Option<f64> {
fn status(acc: &mut f64, x: &sel::Status) {
*acc += x.id as f64;
*acc += x.user.screen_name.len() as f64;
*acc += x.retweet_count as f64;
}
let mut acc = 0.0;
match name {
"twitter" => {
let t: sel::Twitter = D::de(s)?;
for x in &t.statuses {
status(&mut acc, x);
}
}
"status" => {
let x: sel::Status = D::de(s)?;
status(&mut acc, &x);
}
"citm_catalog" => {
let c: sel::Citm = D::de(s)?;
for p in &c.performances {
acc += (p.id + p.event_id + p.seat_categories.0 as u64) as f64;
}
acc += c.events.len() as f64;
}
"canada" => {
let c: sel::Canada = D::de(s)?;
let g = &c.features[0].geometry;
acc += (c.type_.len() + g.type_.len() + g.coordinates.0) as f64;
}
"jeopardy" => {
let q: Vec<sel::Question> = D::de(s)?;
for x in &q {
acc += if x.round == "Final Jeopardy!" { 1.0 } else { 0.0 };
acc += x.category.len() as f64;
}
}
_ => {}
}
Some(acc)
}
// ---- edit: the same edits as bench_all.cpp, then write compact JSON
fn edit_serde(name: &str, r: &mut serde_json::Value) {
fn status(s: &mut serde_json::Value) {
let o = s.as_object_mut().unwrap();
let rc = o["retweet_count"].as_i64().unwrap();
o.insert("retweet_count".to_string(), (rc + 1).into());
o.remove("entities");
o.insert("seen".to_string(), true.into());
}
match name {
"twitter" => {
for s in r["statuses"].as_array_mut().unwrap() {
status(s);
}
}
"status" => status(r),
"citm_catalog" => {
for p in r["performances"].as_array_mut().unwrap() {
let o = p.as_object_mut().unwrap();
let start = o["start"].as_i64().unwrap();
o.insert("start".to_string(), (start + 1).into());
o.remove("seatMapImage");
o.insert("seen".to_string(), true.into());
}
}
"canada" => {
r["features"][0]["properties"]["name"] = "Kanada".into();
for f in r["features"].as_array_mut().unwrap() {
f["geometry"]["coordinates"][0].as_array_mut().unwrap().push(serde_json::json!([0.5, 0.25]));
}
}
"jeopardy" => {
for q in r.as_array_mut().unwrap() {
let o = q.as_object_mut().unwrap();
o.insert("value".to_string(), serde_json::Value::Null);
o.remove("air_date");
o.insert("seen".to_string(), true.into());
}
}
_ => {}
}
}
fn edit_sonic(name: &str, r: &mut sonic_rs::Value) {
fn status(s: &mut sonic_rs::Value) {
let o = s.as_object_mut().unwrap();
let rc = o.get(&"retweet_count").unwrap().as_i64().unwrap();
o.insert("retweet_count", rc + 1);
o.remove(&"entities");
o.insert("seen", true);
}
match name {
"twitter" => {
for s in r.get_mut("statuses").unwrap().as_array_mut().unwrap().iter_mut() {
status(s);
}
}
"status" => status(r),
"citm_catalog" => {
for p in r.get_mut("performances").unwrap().as_array_mut().unwrap().iter_mut() {
let o = p.as_object_mut().unwrap();
let start = o.get(&"start").unwrap().as_i64().unwrap();
o.insert("start", start + 1);
o.remove(&"seatMapImage");
o.insert("seen", true);
}
}
"canada" => {
let fs = r.get_mut("features").unwrap().as_array_mut().unwrap();
fs[0].get_mut("properties").unwrap().as_object_mut().unwrap().insert("name", "Kanada");
for f in fs.iter_mut() {
let ring = f.get_mut("geometry").unwrap().get_mut("coordinates").unwrap().get_mut(0).unwrap();
ring.as_array_mut().unwrap().push(sonic_rs::json!([0.5, 0.25]));
}
}
"jeopardy" => {
for q in r.as_array_mut().unwrap().iter_mut() {
let o = q.as_object_mut().unwrap();
o.insert("value", sonic_rs::Value::new_null());
o.remove(&"air_date");
o.insert("seen", true);
}
}
_ => {}
}
}
macro_rules! edit_simd {
($fname:ident, $t:ty) => {
fn $fname(name: &str, r: &mut $t) {
fn status(s: &mut $t) {
let o = s.as_object_mut().unwrap();
let rc = o.get("retweet_count").unwrap().as_i64().unwrap();
o.insert("retweet_count".into(), (rc + 1).into());
o.remove("entities");
o.insert("seen".into(), true.into());
}
match name {
"twitter" => {
for s in r.as_object_mut().unwrap().get_mut("statuses").unwrap().as_array_mut().unwrap().iter_mut() {
status(s);
}
}
"status" => status(r),
"citm_catalog" => {
for p in r.as_object_mut().unwrap().get_mut("performances").unwrap().as_array_mut().unwrap().iter_mut() {
let o = p.as_object_mut().unwrap();
let start = o.get("start").unwrap().as_i64().unwrap();
o.insert("start".into(), (start + 1).into());
o.remove("seatMapImage");
o.insert("seen".into(), true.into());
}
}
"canada" => {
let fs = r.as_object_mut().unwrap().get_mut("features").unwrap().as_array_mut().unwrap();
fs[0].as_object_mut().unwrap().get_mut("properties").unwrap().as_object_mut().unwrap().insert("name".into(), "Kanada".into());
for f in fs.iter_mut() {
let g = f.as_object_mut().unwrap().get_mut("geometry").unwrap().as_object_mut().unwrap();
let ring = &mut g.get_mut("coordinates").unwrap().as_array_mut().unwrap()[0];
ring.as_array_mut().unwrap().push(vec![<$t>::from(0.5), <$t>::from(0.25)].into());
}
}
"jeopardy" => {
for q in r.as_array_mut().unwrap().iter_mut() {
let o = q.as_object_mut().unwrap();
o.insert("value".into(), <$t>::Static(simd_json::StaticNode::Null));
o.remove("air_date");
o.insert("seen".into(), true.into());
}
}
_ => {}
}
}
};
}
edit_simd!(edit_simd_owned, simd_json::OwnedValue);
edit_simd!(edit_simd_borrowed, simd_json::BorrowedValue<'_>);
// ---- driver
struct Res {
st: Stats,
acc: f64,
want: bool,
out: Vec<u8>,
}
impl Res {
fn new(want: bool) -> Res {
Res { st: Stats::default(), acc: 0.0, want, out: Vec::new() }
}
}
fn emit(r: &mut Res, o: Vec<u8>) -> bool {
r.acc = o.len() as f64;
if r.want {
r.out = o;
}
true
}
struct Variant<'a> {
name: &'static str,
f: Box<dyn Fn(&[u8], &mut Res) -> bool + 'a>,
any_order: bool, // sum: visits objects in another order than the document's
}
fn v<'a>(name: &'static str, f: impl Fn(&[u8], &mut Res) -> bool + 'a) -> Variant<'a> {
Variant { name, f: Box::new(f), any_order: false }
}
fn v_any<'a>(name: &'static str, f: impl Fn(&[u8], &mut Res) -> bool + 'a) -> Variant<'a> {
Variant { name, f: Box::new(f), any_order: true }
}
fn run(var: &Variant, s: &[u8], r: &mut Res) -> bool {
catch_unwind(AssertUnwindSafe(|| (var.f)(s, r))).unwrap_or(false)
}
static mut SINK: f64 = 0.0;
fn sink(x: f64) {
unsafe { std::ptr::write_volatile(std::ptr::addr_of_mut!(SINK), x) }
}
// the input contract of bench_all.cpp: at least 64 bytes of spare capacity
fn padded(s: &[u8]) -> Vec<u8> {
let mut r = Vec::with_capacity(s.len() + 64);
r.extend_from_slice(s);
r
}
// the minified inputs (twitter_min, citm_catalog_min): the whitespace outside strings removed, the bytes inside strings
// untouched (the same function as strip_ws in bench_all.cpp)
fn strip_ws(s: &[u8]) -> Vec<u8> {
let mut r = Vec::with_capacity(s.len());
let mut in_string = false;
let mut i = 0;
while i < s.len() {
let ch = s[i];
if in_string {
r.push(ch);
if ch == b'\\' && i + 1 < s.len() {
i += 1;
r.push(s[i]);
} else if ch == b'"' {
in_string = false;
}
} else if ch != b' ' && ch != b'\n' && ch != b'\r' && ch != b'\t' {
r.push(ch);
in_string = ch == b'"';
}
i += 1;
}
r
}
// malformed inputs derived from a valid document, as in bench_all.cpp (0xFF in a string value, not a key)
// (what, text, is the 0xFF probe)
fn malformed(s: &[u8]) -> Vec<(&'static str, Vec<u8>, bool)> {
let (mut first_close, mut mid_quote, mut last_quote) = (None, None, None);
let mut in_string = false;
let mut stack: Vec<u8> = Vec::new();
let mut prev = 0u8;
let mut i = 0;
while i < s.len() {
let ch = s[i];
if in_string {
if ch == b'\\' {
i += 1;
} else if ch == b'"' {
in_string = false;
}
} else if ch == b'"' {
in_string = true;
let value = prev == b':' || (stack.last() == Some(&b'[') && (prev == b'[' || prev == b','));
if value {
last_quote = Some(i);
if mid_quote.is_none() && i >= s.len() / 2 {
mid_quote = Some(i);
}
}
prev = b'"';
} else if ch == b'{' || ch == b'[' {
stack.push(ch);
prev = ch;
} else if ch == b'}' || ch == b']' {
if ch == b'}' && first_close.is_none() {
first_close = Some(i);
}
stack.pop();
prev = ch;
} else if ch == b':' || ch == b',' {
prev = ch;
}
i += 1;
}
let mut t = s.to_vec();
while matches!(t.last(), Some(b' ' | b'\n' | b'\r' | b'\t')) {
t.pop();
}
let mut trailing = t.clone();
trailing.extend_from_slice(b" ]x"); // after the value
t.pop();
let mut out = vec![("truncated input", padded(&t), false), ("trailing garbage", padded(&trailing), false)];
let mut t = s.to_vec();
if let Some(c) = first_close {
t.insert(c, b',');
out.push(("a stray comma before '}'", padded(&t), false));
} else {
// a file without objects (--files): the comma goes before the last ']'
let c = (0..t.len()).rev().find(|&i| t[i] == b']').unwrap();
t.insert(c, b',');
out.push(("a stray comma before ']'", padded(&t), false));
}
// a file without string values (--files) gets no UTF-8 probe
if let Some(q) = mid_quote.or(last_quote) {
let mut t = s.to_vec();
t.insert(q + 1, 0xFF);
out.push(("invalid UTF-8 (0xFF in a string)", padded(&t), true));
}
out
}
fn same_text(out: &[u8], expected: &serde_json::Value) -> bool {
serde_json::from_slice::<serde_json::Value>(out).map(|v| v == *expected).unwrap_or(false)
}
fn main() {
let args: Vec<String> = std::env::args().collect();
// --files <dir>: every *.json file of the directory instead of the files below, without select and edit;
// --conformance <dir>: every validate variant on every *.json file of the directory (JSONTestSuite), no timing
let files = args.len() > 1 && args[1] == "--files";
let conformance = args.len() > 1 && args[1] == "--conformance";
let a = if files || conformance { 2 } else { 1 };
if args.len() <= a {
eprintln!("usage: {0} <json_test_data dir> [rounds] [document] [use case]\n {0} --files <dir> [rounds] [document] [use case]\n {0} --conformance <dir>", args[0]);
std::process::exit(1);
}
let t = format!("{}/", args[a]);
let rounds: usize = args.get(a + 1).map(|r| r.parse().unwrap()).unwrap_or(30).max(1);
let only: String = args.get(a + 2).cloned().unwrap_or_default();
let only_uc: String = if conformance { "validate".to_string() } else { args.get(a + 3).cloned().unwrap_or_default() };
// (file, text, batch, the document whose select and edit apply)
let mut docs: Vec<(String, Vec<u8>, usize, String)> = Vec::new();
if files || conformance {
let mut paths: Vec<std::path::PathBuf> = std::fs::read_dir(&t).unwrap().map(|e| e.unwrap().path()).filter(|p| p.extension().map_or(false, |x| x == "json")).collect();
paths.sort();
for p in paths {
// small files are timed in batches of about 500 KB of input, as status is
let text = padded(&std::fs::read(&p).unwrap());
let name = if conformance { p.file_name() } else { p.file_stem() }.unwrap().to_string_lossy().to_string();
let batch = (500000 / text.len().max(1)).max(1);
docs.push((name.clone(), text, batch, name));
}
} else {
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.clone(), padded(&std::fs::read(format!("{}{}", t, f)).unwrap()), 1, name));
}
let tw: serde_json::Value = serde_json::from_slice(&docs[0].1).unwrap();
docs.push(("status".to_string(), padded(serde_json::to_string(&tw["statuses"][0]).unwrap().as_bytes()), 200, "status".to_string()));
let (tw_min, citm_min) = (padded(&strip_ws(&docs[0].1)), padded(&strip_ws(&docs[1].1)));
docs.push(("twitter_min".to_string(), tw_min, 1, "twitter".to_string()));
docs.push(("citm_catalog_min".to_string(), citm_min, 1, "citm_catalog".to_string()));
}
std::panic::set_hook(Box::new(|_| {})); // a panic is a failed call, reported by the checks
let simd_reused = RefCell::new((simd_json::Buffers::default(), Vec::<u8>::new()));
let mut excluded_total = 0;
for (file, s, batch, base) in &docs {
if !only.is_empty() && *file != only {
continue;
}
let (file, name) = (file.as_str(), base.as_str());
let s = s.as_slice();
let bad = if conformance { Vec::new() } else { malformed(s) };
for uc in ["validate", "sum", "select", "edit", "minify", "write"] {
if (!only_uc.is_empty() && uc != only_uc) || (files && (uc == "select" || uc == "edit")) {
continue;
}
// write: each library's document, parsed once outside the timer
let mut wbuf: Vec<u8>;
let (w_serde, w_sonic, w_simd) = if uc == "write" {
wbuf = s.to_vec();
(
Some(serde_json::from_slice::<serde_json::Value>(s).unwrap()),
Some(sonic_rs::from_slice::<sonic_rs::Value>(s).unwrap()),
Some(simd_json::to_borrowed_value(&mut wbuf).unwrap()),
)
} else {
(None, None, None)
};
let reuse = &simd_reused;
let all: Vec<Variant> = match uc {
"validate" => vec![
v("serde_json: IgnoredAny", |s, _| serde_json::from_slice::<IgnoredAny>(s).is_ok()),
v("serde_json: from_utf8 + IgnoredAny", |s, _| std::str::from_utf8(s).ok().map_or(false, |t| serde_json::from_str::<IgnoredAny>(t).is_ok())),
v("serde_json: Value", |s, _| serde_json::from_slice::<serde_json::Value>(s).is_ok()),
v("sonic-rs: IgnoredAny", |s, _| sonic_rs::from_slice::<IgnoredAny>(s).is_ok()),
v("sonic-rs: Value", |s, _| sonic_rs::from_slice::<sonic_rs::Value>(s).is_ok()),
v("simd-json: tape (copy)", |s, _| { let mut b = s.to_vec(); simd_json::to_tape(&mut b).is_ok() }),
v("simd-json: borrowed (copy)", |s, _| { let mut b = s.to_vec(); let ok = simd_json::to_borrowed_value(&mut b).is_ok(); ok }),
v("simd-json: tape (reused)", move |s, _| {
let mut g = reuse.borrow_mut();
let (ref mut bufs, ref mut b) = *g;
b.clear();
b.extend_from_slice(s);
simd_json::to_tape_with_buffers(b, bufs).is_ok()
}),
],
"sum" => vec![
v("serde_json: visitor", |s, r| sum_serde(s, &mut r.st)),
v_any("serde_json: Value", |s, r| serde_json::from_slice::<serde_json::Value>(s).map(|x| walk_serde(&x, &mut r.st)).is_ok()),
v("sonic-rs: visitor", |s, r| sum_sonic(s, &mut r.st)),
v("sonic-rs: Value", |s, r| sonic_rs::from_slice::<sonic_rs::Value>(s).map(|x| walk_sonic(&x, &mut r.st)).is_ok()),
v("simd-json: visitor (copy)", |s, r| sum_simd(s, &mut r.st)),
v_any("simd-json: borrowed (copy)", |s, r| { let mut b = s.to_vec(); simd_json::to_borrowed_value(&mut b).map(|x| walk_simd(&x, &mut r.st)).is_ok() }),
v_any("simd-json: borrowed (reused)", move |s, r| {
let mut g = reuse.borrow_mut();
let (ref mut bufs, ref mut b) = *g;
b.clear();
b.extend_from_slice(s);
simd_json::to_borrowed_value_with_buffers(b, bufs).map(|x| walk_simd(&x, &mut r.st)).is_ok()
}),
],
"select" => vec![
v("serde_json: structs", move |s, r| pick_structs::<SerdeJson>(name, s).map(|a| r.acc = a).is_some()),
v("serde_json: from_utf8 + structs", move |s, r| pick_structs::<SerdeJsonStr>(name, s).map(|a| r.acc = a).is_some()),
v("serde_json: Value", move |s, r| serde_json::from_slice::<serde_json::Value>(s).map(|x| r.acc = pick_serde(name, &x)).is_ok()),
v("sonic-rs: structs", move |s, r| pick_structs::<SonicRs>(name, s).map(|a| r.acc = a).is_some()),
v("sonic-rs: Value", move |s, r| sonic_rs::from_slice::<sonic_rs::Value>(s).map(|x| r.acc = pick_sonic(name, &x)).is_ok()),
v("simd-json: structs (copy)", move |s, r| pick_structs::<SimdJson>(name, s).map(|a| r.acc = a).is_some()),
v("simd-json: borrowed (copy)", move |s, r| { let mut b = s.to_vec(); let ok = simd_json::to_borrowed_value(&mut b).map(|x| r.acc = pick_simd(name, &x)).is_ok(); ok }),
],
"edit" => vec![
v("serde_json: Value", move |s, r| match serde_json::from_slice::<serde_json::Value>(s) {
Ok(mut x) => { edit_serde(name, &mut x); emit(r, serde_json::to_vec(&x).unwrap()) }
Err(_) => false,
}),
v("sonic-rs: Value", move |s, r| match sonic_rs::from_slice::<sonic_rs::Value>(s) {
Ok(mut x) => { edit_sonic(name, &mut x); emit(r, sonic_rs::to_vec(&x).unwrap()) }
Err(_) => false,
}),
v("simd-json: owned (copy)", move |s, r| { let mut b = s.to_vec(); match simd_json::to_owned_value(&mut b) {
Ok(mut x) => { edit_simd_owned(name, &mut x); emit(r, simd_json::to_vec(&x).unwrap()) }
Err(_) => false,
}}),
v("simd-json: borrowed (copy)", move |s, r| { let mut b = s.to_vec(); let ok = match simd_json::to_borrowed_value(&mut b) {
Ok(mut x) => { edit_simd_borrowed(name, &mut x); emit(r, simd_json::to_vec(&x).unwrap()) }
Err(_) => false,
}; ok }),
],
"minify" => vec![
v("serde_json: Value", |s, r| serde_json::from_slice::<serde_json::Value>(s).map(|x| emit(r, serde_json::to_vec(&x).unwrap())).unwrap_or(false)),
v("sonic-rs: Value", |s, r| sonic_rs::from_slice::<sonic_rs::Value>(s).map(|x| emit(r, sonic_rs::to_vec(&x).unwrap())).unwrap_or(false)),
v("simd-json: borrowed (copy)", |s, r| { let mut b = s.to_vec(); simd_json::to_borrowed_value(&mut b).map(|x| emit(r, simd_json::to_vec(&x).unwrap())).unwrap_or(false) }),
],
_ => {
let (a, b, c) = (w_serde.as_ref().unwrap(), w_sonic.as_ref().unwrap(), w_simd.as_ref().unwrap());
vec![
v("serde_json: Value", move |_, r| emit(r, serde_json::to_vec(a).unwrap())),
v("sonic-rs: Value", move |_, r| emit(r, sonic_rs::to_vec(b).unwrap())),
v("simd-json: borrowed", move |_, r| emit(r, simd_json::to_vec(c).unwrap())),
]
}
};
// the reference result: serde_json's visitor (sum, document order), Value (select, edit, minify, write)
let mut ref_st = Stats::default();
let mut ref_acc = 0.0;
let mut ref_json = serde_json::Value::Null;
match uc {
"sum" => {
sum_serde(s, &mut ref_st);
println!("check {:<13} {:<9} reference: nodes {} str {} num {:.16e}", file, uc, ref_st.nodes, ref_st.str_, ref_st.num);
}
"select" => {
ref_acc = pick_serde(name, &serde_json::from_slice(s).unwrap());
println!("check {:<13} {:<9} reference: {:.16e}", file, uc, ref_acc);
}
"validate" => {}
_ => {
ref_json = serde_json::from_slice(s).unwrap();
if uc == "edit" {
edit_serde(name, &mut ref_json);
}
}
}
if conformance {
// CONF_VARIANT=<name>: only this variant (a stack overflow aborts the process, see conformance.sh)
let only_var = std::env::var("CONF_VARIANT").unwrap_or_default();
for var in all.iter().filter(|v| only_var.is_empty() || v.name == only_var) {
let mut r = Res::new(true);
let verdict = match catch_unwind(AssertUnwindSafe(|| (var.f)(s, &mut r))) {
Ok(true) => "accepted",
Ok(false) => "rejected",
Err(_) => "crashed",
};
println!("conformance: {} | {} | {}", file, var.name, verdict);
}
continue;
}
let n_all = all.len();
let mut vs: Vec<Variant> = Vec::new();
let mut levels: Vec<&str> = Vec::new();
for var in all {
let mut r = Res::new(true);
let mut why = String::new();
let mut panicked = false;
if !catch_unwind(AssertUnwindSafe(|| (var.f)(s, &mut r))).unwrap_or_else(|_| {
panicked = true;
false
}) {
why = if panicked { "crashes on the valid input" } else { "rejects the valid input" }.to_string();
} else if uc == "sum"
&& !(r.st.nodes == ref_st.nodes
&& r.st.str_ == ref_st.str_
&& if var.any_order { (r.st.num - ref_st.num).abs() <= 1e-9 * ref_st.num.abs() } else { r.st.num == ref_st.num })
{
why = format!("wrong result (nodes {} str {} num {:.16e})", r.st.nodes, r.st.str_, r.st.num);
} else if uc == "select" && r.acc != ref_acc {
why = format!("wrong result ({:.16e})", r.acc);
} else if (uc == "edit" || uc == "minify" || uc == "write") && !same_text(&r.out, &ref_json) {
why = "the output does not read back as the expected document".to_string();
}
if why.is_empty() {
// the validation level from the malformed-input probes, as in bench_all.cpp
let mut level = "n/a";
if uc != "write" {
let (mut structural, mut utf8) = (false, false);
let mut notes: Vec<String> = Vec::new();
for (what, b, is_utf8) in &bad {
let mut rb = Res::new(true);
match catch_unwind(AssertUnwindSafe(|| (var.f)(b, &mut rb))) {
Ok(false) => continue,
Ok(true) => {
notes.push(format!("accepts {}", what));
if *is_utf8 { utf8 = true } else { structural = true }
}
Err(_) => {
notes.push(format!("crashes on {}", what));
structural = true;
}
}
}
level = if structural { "partial" } else if utf8 { "no UTF-8" } else { "full" };
if !notes.is_empty() {
println!("level: {} {} | {} | {} | {}", file, uc, var.name, level, notes.join("; "));
}
}
vs.push(var);
levels.push(level);
} else {
excluded_total += 1;
println!("excluded: {} {} | {} | {}", file, uc, var.name, why);
}
}
println!("check {:<13} {:<9} {} of {} variants give the right result", file, uc, vs.len(), n_all);
if vs.is_empty() {
continue;
}
let r = if s.len() > 10_000_000 { rounds.min((rounds / 5).max(3)) } else { rounds };
let mut best = vec![f64::MAX; vs.len()];
// a new order of the variants in every round (as in bench_all.cpp): a Fisher-Yates shuffle with xorshift64
let mut order: Vec<usize> = (0..vs.len()).collect();
let mut rng: u64 = 12345;
for i in 0..r {
if i > 0 {
for j in (1..order.len()).rev() {
rng ^= rng << 13;
rng ^= rng >> 7;
rng ^= rng << 17;
order.swap(j, (rng % (j as u64 + 1)) as usize);
}
}
for &k in &order {
let var = &vs[k];
let mut res = Res::new(false);
run(var, s, &mut res); // untimed: the previous variant's leftovers are cleaned up here
sink(res.st.num + res.acc);
let t0 = Instant::now();
for _ in 0..*batch {
let mut rt = Res::new(false);
let ok = run(var, s, &mut rt);
sink(if ok { rt.st.num + rt.acc } else { -1.0 });
}
let ns = t0.elapsed().as_nanos() as f64 / *batch as f64;
best[k] = best[k].min(ns);
}
}
print!("{:<13} {:<9}", file, uc);
for (k, var) in vs.iter().enumerate() {
print!(" | {} [{}] {:.1}us ({:.2}x)", var.name, levels[k], best[k] / 1e3, best[k] / best[0]);
}
println!();
}
}
println!("done: {} variant(s) excluded", excluded_total);
}
+229
View File
@@ -0,0 +1,229 @@
#!/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 by use case.
Usage: summarize.py LOG... [--rust LOG...] [--json OUT.json]
Every variant carries a validation level from the malformed-input probes: "full" (rejects truncated input, a stray
comma and invalid UTF-8), "no UTF-8" (accepts only the invalid UTF-8), "partial" (accepts or crashes on a structural
error, or crashes on any probe), or "n/a" (write: no input to validate; counts as full). A variant's level is the
worst it got for any file. 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 (variants named "(reused)" or "(reused allocator)" keep a parser,
document or allocator across calls and are never chosen as a library's best; neither are "(copy untimed)"
variants, which copy their input before the timer starts), and in a "reused:" line the library's best reused
variant in the same way,
(b) all variants with their levels and times,
(c) the variants below "full" with what the probes found, and the variants excluded for wrong results.
--json writes {use case: {file: {library: {"best": {"variant", "us", "level"} or null, "best_any": {...},
"best_reused": {...} or null, "best_reused_any": {...} or null, "variants": {variant: {"us", "level"}}, "excluded":
{variant: reason}}}}} for the chart scripts ("best_reused" is the best full reused variant, "best_reused_any" the best
reused variant at any level); variant names there omit the "<library>: " prefix.
Several logs are combined by taking each cell's best time. A variant excluded for one file (or in one log) is excluded
for every file of that use case. The reference values that bench_all and bench_rs print for sum and select are
compared, so both programs are known to have checked against the same numbers.
"""
import json
import re
import sys
USE_CASES = ["validate", "sum", "select", "edit", "minify", "write"]
DOCS = ["twitter", "citm_catalog", "canada", "jeopardy", "status", "twitter_min", "citm_catalog_min"]
OURS = "nlohmann json_document"
RANK = {"full": 0, "n/a": 0, "no UTF-8": 1, "partial": 2}
CELL = re.compile(r"^(.+) \[(full|no UTF-8|partial|n/a)\] ([\d.]+)us(?: \(([\d.]+)x\))?$")
REF = re.compile(r"^check (\S+)\s+(\S+)\s+reference: (.*)$")
def library(name):
return name.split(": ", 1)[0]
def variant(name):
return name.split(": ", 1)[1] if ": " in name else name
def is_reused(name):
return re.search(r"\(reused[ )]", name) is not None
def is_untimed_copy(name):
return "(copy untimed)" in name
def worse(a, b):
return a if a is not None and (b is None or RANK[a] >= RANK[b]) else b
def parse(path, cells, levels, notes, excluded, refs):
for line in open(path, encoding="utf-8"):
line = line.rstrip("\n")
if line.startswith(("excluded: ", "level: ")):
kind, rest = line.split(": ", 1)
fields = rest.split(" | ")
doc, uc = fields[0].split()
name = fields[1]
target = excluded if kind == "excluded" else notes
for why in fields[-1].split("; "):
target.setdefault((uc, name), {}).setdefault(why, set()).add(doc)
if kind == "level":
levels[(uc, name)] = worse(levels.get((uc, name)), fields[2])
continue
m = REF.match(line)
if m:
refs.setdefault((m[1], m[2]), {})[path] = m[3]
continue
if line.startswith(("check", "done", "WARNING")) or " | " not in line:
continue
parts = line.split(" | ")
head = parts[0].split()
if len(head) != 2 or head[1] not in USE_CASES:
continue
for c in parts[1:]:
m = CELL.match(c.strip())
if m:
key = (head[0], head[1], m[1])
if head[0] not in DOCS: # bench_all --files: the documents are the files of a directory
DOCS.append(head[0])
cells[key] = min(cells.get(key, float("inf")), float(m[3]))
levels[(head[1], m[1])] = worse(levels.get((head[1], m[1])), m[2])
def check_refs(refs):
"""The sum and select references of all logs must agree (bench_rs prints Rust's float formatting)."""
def numbers(text):
return [float(x) for x in re.findall(r"[-+]?\d[\d.]*(?:e[-+]?\d+)?", text)]
ok = True
for (doc, uc), by_log in sorted(refs.items()):
values = {tuple(numbers(t)) for t in by_log.values()}
if len(values) > 1:
ok = False
print("WARNING: the %s %s references differ: %s" % (doc, uc, "; ".join("%s: %s" % kv for kv in by_log.items())))
return ok
def is_full(level):
return RANK[level] == 0
def main():
args = sys.argv[1:]
out_json = None
if "--json" in args:
i = args.index("--json")
out_json = args[i + 1]
del args[i:i + 2]
cpp = args[:args.index("--rust")] if "--rust" in args else args
rust = args[args.index("--rust") + 1:] if "--rust" in args else []
if not cpp and not rust:
sys.exit(__doc__)
cells, levels, notes, excluded, refs = {}, {}, {}, {}, {}
for p in cpp + rust:
parse(p, cells, levels, notes, excluded, refs)
check_refs(refs)
result = {}
def docs_of(uc):
return [d for d in DOCS if any(k[0] == d and k[1] == uc for k in cells)]
def cell_of(doc, uc):
return {n: t for (d, u, n), t in cells.items() if d == doc and u == uc and (uc, n) not in excluded}
def show(n, t, ours, uc):
ratio = "%5.2f" % (t / ours) if ours else " -"
return "%-40s %-10s %11.1f us %s" % (variant(n), "[%s]" % levels[(uc, n)], t, ratio)
print("# (a) Per library: the best full variant, and the best at any level if faster (ratio to the best full %s"
" variant; below 1: faster)" % OURS)
for uc in USE_CASES:
if not docs_of(uc):
continue
print("\n== " + uc)
for doc in docs_of(uc):
cell = cell_of(doc, uc)
libs = {}
for n, t in cell.items():
libs.setdefault(library(n), {})[n] = t
entry = result.setdefault(uc, {}).setdefault(doc, {})
rows = []
def pick(vs):
"""the best full variant and the best variant at any level"""
full = {n: t for n, t in vs.items() if is_full(levels[(uc, n)])}
return (min(full.items(), key=lambda x: x[1]) if full else None,
min(vs.items(), key=lambda x: x[1]) if vs else None)
def described(x):
return {"variant": variant(x[0]), "us": x[1], "level": levels[(uc, x[0])]} if x else None
for lib, vs in libs.items():
b, a = pick({n: t for n, t in vs.items() if not is_reused(n) and not is_untimed_copy(n)})
rb, ra = pick({n: t for n, t in vs.items() if is_reused(n)})
entry[lib] = {
"best": described(b),
"best_any": described(a),
"best_reused": described(rb),
"best_reused_any": described(ra),
"variants": {variant(n): {"us": t, "level": levels[(uc, n)]} for n, t in sorted(vs.items(), key=lambda x: x[1])},
}
rows.append((lib, b, a, rb, ra))
for (u, n), whys in excluded.items():
if u == uc:
lib = entry.setdefault(library(n), {"best": None, "best_any": None, "best_reused": None, "best_reused_any": None, "variants": {}})
lib.setdefault("excluded", {})[variant(n)] = "; ".join(sorted(whys))
ours = next((b[1] for lib, b, a, rb, ra in rows if lib == OURS and b), None)
print("%s%s" % (doc, "" if ours else " (no full %s result)" % OURS))
rows.sort(key=lambda r: (r[1] is None, r[1][1] if r[1] else r[2][1] if r[2] else 0))
for lib, b, a, rb, ra in rows:
line = " %-24s %s" % (lib, show(*b, ours, uc) if b else "%-40s %-10s %14s %5s" % ("(none)", "", "", ""))
if a and (not b or a[0] != b[0]):
line += " | faster: %s" % show(*a, ours, uc)
print(line)
if ra:
line = " %-22s %s" % ("reused:", show(*rb, ours, uc) if rb else "%-40s %-10s %14s %5s" % ("(none)", "", "", ""))
if not rb or ra[0] != rb[0]:
line += " | faster: %s" % show(*ra, ours, uc)
print(line)
print("\n# (b) All variants")
for uc in USE_CASES:
if not docs_of(uc):
continue
print("\n== " + uc)
for doc in docs_of(uc):
cell = cell_of(doc, uc)
ours = min((t for n, t in cell.items() if library(n) == OURS and not is_reused(n) and is_full(levels[(uc, n)])), default=None)
print(doc)
for n, t in sorted(cell.items(), key=lambda x: x[1]):
ratio = "%5.2f" % (t / ours) if ours else " -"
print(" %-66s %-10s %11.1f us %s" % (n, "[%s]" % levels[(uc, n)], t, ratio))
print("\n# (c) Variants below full, and excluded variants")
for uc in USE_CASES:
rows = sorted((n, whys) for (u, n), whys in notes.items() if u == uc)
ex = sorted((n, whys) for (u, n), whys in excluded.items() if u == uc)
if not rows and not ex:
continue
print("\n== " + uc)
for n, whys in rows:
for why, docs in sorted(whys.items()):
print(" %-58s %-10s %s (%s)" % (n, "[%s]" % levels.get((uc, n), "?"), why, ", ".join(d for d in DOCS if d in docs)))
for n, whys in ex:
for why, docs in sorted(whys.items()):
print(" %-58s %-10s %s (%s)" % (n, "excluded", why, ", ".join(d for d in DOCS if d in docs)))
if out_json:
with open(out_json, "w", encoding="utf-8") as f:
json.dump(result, f, indent=2)
f.write("\n")
if __name__ == "__main__":
main()