mirror of
https://github.com/nlohmann/json.git
synced 2026-11-09 14:01:47 +00:00
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:
126 files changed
+74739
-763
No files matched your search
@@ -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
|
||||
|
||||
@@ -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.
|
||||
@@ -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); });
|
||||
}
|
||||
Executable
+151
@@ -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()
|
||||
@@ -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
@@ -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
@@ -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
@@ -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()
|
||||
Reference in new issue
Block a user