Skip to content

Compare json_view with yyjson, simdjson, and Boost.JSON - #5627

Merged
nlohmann merged 4 commits into
developfrom
json-view/15-view-bench
Oct 22, 2026
Merged

nlohmann merged 4 commits into
developfrom
json-view/15-view-bench

Conversation

@nlohmann

@nlohmann nlohmann commented Sep 29, 2026 •

Copy link
Copy Markdown
Owner

Part of the stack for the zero-copy view (#5295). This PR adds a benchmark comparison of json_view with other libraries, and a workflow to run it on GitHub-hosted runners. It is the last PR of the stack: the PRs below it are merged into develop, and this one measures their parser and serializer work.

Summary

tests/benchmarks/json_view/ holds a comparison with other libraries. It answers the question users ask when they pick a library. It is not built by CMake and not run by CI.

  • bench_view.cpp: parse, traverse, select, and dump of twitter, citm_catalog, canada, jeopardy, a single tweet, and a JSON-RPC request.
    • Engines: json_view, yyjson, simdjson (DOM and On-Demand), Boost.JSON, and json::parse.
    • All engines must agree on every document (value count, bytes of all strings and keys, sum of numbers) before anything is timed, and they run interleaved in every round. The best round is reported.
  • bench_corpus.cpp: parse, traverse, and dump of any list of files. Files that cannot be opened are reported instead of aborting the run.
  • bench_edit.cpp: parses, applies the same logical edits with each library's own API (a handful at fixed places, or one in every record), and serializes; all outputs must describe the same value.
  • compare.py: reproduces the numbers with one command.
    • It builds the three programs against include/, using the libraries of the system (pkg-config or Homebrew) or pinned downloads (--download): yyjson 0.13.0, simdjson 4.6.11, and Boost 1.92.0.
    • Downloads have a timeout and go to a .part file. Each archive is hashed in chunks and checked against its SHA-256; an archive that does not match is removed. It is extracted into a temporary directory that is renamed when complete, so an interrupted run is not mistaken for a finished one.
    • --data, --corpus and --build-dir are made absolute, since the benchmarks run in the build directory. CXX and CC may contain arguments (they are split with shlex).
    • It writes the results to results/<date>-<host>.md and .csv, together with the date, commit, CPU, OS, compiler, flags, and library versions.
    • Only the Python 3 standard library is used.
  • README.md: how to run the comparison, what is measured, and which features each engine has, so that the numbers can be read correctly.
  • .github/workflows/json_view_benchmarks.yml: runs compare.py --download on GitHub-hosted Ubuntu runners and shows the results as the job summary and as an artifact.
    • It never runs on its own: it is started by hand (workflow_dispatch: x86-64 or AArch64, GCC or Clang), or by adding the label benchmark to a pull request (both architectures, GCC).
    • It has timeout-minutes: 60 and a concurrency group keyed by the label, so adding an unrelated label does not cancel a running benchmark.
    • Shared runners are noisy, so the numbers show where json_view stands on another architecture; they are not for publication.
  • .github/labeler.yml labels changes under tests/benchmarks/json_view/.

Fair comparison

  • Each engine runs once untimed before each timed call. Otherwise the engine after json::parse pays for the allocator cleaning up the tens of thousands of nodes json::parse just freed (with glibc, this made json_view look 1.7 times slower than yyjson on citm_catalog traverse).
  • bench_corpus compares the dump with source numbers (number_format::source) against yyjson writing numbers read as raw text (YYJSON_READ_NUMBER_AS_RAW); both write the same bytes.
  • Columns "json_view" (fresh document) and "json_view (reused)" (parsed into the same document, timed for traverse and select too) are compared with "simdjson DOM" (parser reused) and "simdjson DOM (fresh)".
  • Boost.JSON is optional (JSON_VIEW_BENCH_BOOST; --no-boost).

No results are committed. They should come from a quiet machine and include x86-64; the benchmark workflow gives a first x86-64 impression (label benchmark).

Performance

No library code changes in this PR. The numbers below were measured on 2026-10-21 at this PR's head 774ea21 (develop bd5227c with the whole json_view stack merged, plus this PR), with compare.py --download --rounds 30 (-O3), fresh documents, µs, best round. Libraries: yyjson 0.13.0, simdjson 4.6.11, Boost 1.92.0.

  • x86-64: KVM VPS (Haswell with AVX2, 8 vCPUs, Ubuntu 24.04), GCC 13.3 and Clang 18.1, pinned with taskset -c 5. The machine has a load of about 3 from other services, so identical runs differ by up to ±10%.
  • Apple M1 Max: Darwin 27.0, Apple clang 21.0.
json_view GCC / Clang / M1 yyjson GCC / Clang / M1 simdjson DOM GCC / Clang / M1
twitter parse 375 / 308 / 186 426 / 386 / 193 316 / 309 / 217
citm_catalog parse 910 / 760 / 368 1141 / 927 / 480 847 / 727 / 489
citm_catalog traverse 1070 / 945 / 489 1273 / 1105 / 578 1068 / 945 / 611
canada parse 1667 / 1824 / 944 3732 / 3531 / 1489 3392 / 3279 / 2102
jeopardy parse (ms) 86.3 / 80.2 / 24.1 143.9 / 143.2 / 30.8 52.6 / 53.1 / 27.4
  • json_view is ahead of yyjson in every row, with both compilers and on both architectures.
  • On x86-64 with Clang it ties simdjson's AVX2 DOM on twitter and citm_catalog traverse and is 5% behind on citm_catalog parse. With GCC it is 19% behind on twitter parse, 7% on citm_catalog parse, and even on traverse.
  • On the M1 it is ahead of simdjson (NEON) in every row.
  • jeopardy on x86-64: simdjson's parser is reused across runs. A fresh json_view document pays for the page faults of 4 KiB pages; with a reused document (json_view (reused)) it takes 47 ms with GCC and 43 ms with Clang, ahead of simdjson. A fresh simdjson DOM takes 131–132 ms.

Dump with the numbers of the input (µs; same output bytes), GCC / Clang / M1:

json_view (source numbers) yyjson (raw numbers)
canada 843 / 983 / 399 743 / 822 / 790
citm_catalog 260 / 270 / 134 281 / 306 / 181
twitter 159 / 159 / 72 212 / 259 / 108
jeopardy (ms) 74.1 / 76.1 / 14.6 79.4 / 78.2 / 20.3

On canada on x86-64, yyjson's lead is mostly the zero-fill of std::string::resize(), which yyjson's malloced output does not have.

bench_edit (parse, edit, serialize; patch):

json_view GCC / Clang / M1 yyjson takes ... as long
twitter 532 / 492 / 279 1.84× / 1.89× / 1.69×
citm_catalog 1126 / 1031 / 536 1.62× / 1.69× / 1.70×
canada 8576 / 9075 / 4654 1.00× / 0.99× / 0.80×

Comparison with the previous numbers

Each row of bench_view, bench_corpus, and bench_edit was compared with the json_view stack tip of 2026-10-16 (4933b60), with the same harness. On the M1 that is one old run and two new ones; on x86-64 (Clang) two old runs and three new runs, interleaved; best of each:

  • M1: no change. All json_view rows are within ±3.6% of the old ones; on rpc, a reused document is 4–6% faster.
  • x86-64: no change beyond the noise. The rows that came out more than 3% slower are citm_catalog traverse with a fresh document (+4.3%, but −1.2% with a reused document), canada parse with a reused document (+3.4%, +0.8% fresh), bench_edit citm_catalog update (+3.3%), canada patch (+3.4%; yyjson also +3.7%), and the 55 MB jeopardy in bench_corpus (parse with a reused document +4.9%, traverse +3.3%; the same parse in bench_view is 3.9% faster). Single old runs differ from each other by up to 13% in the same rows, so none of them is a measured regression.

Some figures in the previous version of this description were worse than these numbers or were too optimistic. In detail:

previous description now reason
canada traverse behind yyjson "about 10%" M1 1.23×, GCC 1.18×, Clang 1.06× as long The 10% figure only holds for x86-64 with Clang. The 10-16 stack tip measures the same 1.23× on the M1, so the code did not get slower.
canada dump (with conversion) behind yyjson "about 1.9x" M1 2.12×, GCC 1.91×, Clang 1.91× Same: 1.9× is the x86-64 figure, and the M1 ratio was already 2.1× on 10-16.
canada dump, source numbers, x86-64 903 µs (yyjson 793) GCC 843 (743), Clang 983 (822) Now per compiler. With Clang, json_view takes 1.20× as long as yyjson, compared with 1.14× before. The interleaved Clang runs show the merged code 5.7% faster than the 10-16 tip (best 2.29 vs 2.16 GB/s), so the 903 µs came from a quieter moment of the VPS, not from different code.
jeopardy dump, x86-64 72.2 ms (yyjson 73.6) GCC 74.1 (79.4), Clang 76.1 (78.2) Slower on both libraries (machine load); json_view's lead over yyjson grew.
twitter parse, GCC, behind simdjson part of "6–23%" 19% Unchanged in substance: GCC's code for the string scan is slower than Clang's; with Clang, json_view now ties simdjson.

What tuning does not close: canada traverse (the lazy float conversion runs at fast_float's speed; yyjson converts during the parse) and canada dump with number conversion (converting each 17-digit token to a double during the dump). Both are larger on the M1 than on x86-64.

Tests

None: this PR adds benchmarks, not unit tests. The benchmarks require all engines to agree on every document's content before any timing starts. compare.py is not run by CI.

Public API

No breaking changes, and no change to the API: this PR only adds files under tests/benchmarks/json_view/, a GitHub Actions workflow, and a labeler rule. Nothing in include/ changes.


Written by Claude Code.

🤖 Generated with Claude Code

@nlohmann
nlohmann added this pull request to stack #5636 September 29, 2026 14:20
@nlohmann nlohmann changed the title json view/15 view bench Compare json_view with yyjson, simdjson, and Boost.JSON Sep 29, 2026
@nlohmann
nlohmann removed this pull request from stack #5636 September 30, 2026 13:19
@nlohmann
nlohmann force-pushed the json-view/15-view-bench branch from f72abf8 to acf0cc4 Compare September 30, 2026 13:20
@nlohmann
nlohmann added this pull request to stack #5739 September 30, 2026 13:21
@nlohmann
nlohmann force-pushed the json-view/15-view-bench branch 3 times, most recently from c7a8571 to 13dbe93 Compare September 30, 2026 18:06
@nlohmann nlohmann added the review needed It would be great if someone could review the proposed changes. label Sep 30, 2026
@nlohmann
nlohmann marked this pull request as ready for review September 30, 2026 18:17
@nlohmann
nlohmann force-pushed the json-view/15-view-bench branch from 13dbe93 to a91f6f4 Compare September 30, 2026 19:15
@github-actions github-actions Bot added documentation L CI tests python Pull requests that update Python code labels Oct 1, 2026
Comment thread tests/benchmarks/json_view/bench_corpus.cpp Fixed
Comment thread tests/benchmarks/json_view/bench_corpus.cpp Fixed
Comment thread tests/benchmarks/json_view/bench_corpus.cpp Fixed
Comment thread tests/benchmarks/json_view/bench_corpus.cpp Dismissed
Comment thread tests/benchmarks/json_view/bench_corpus.cpp Dismissed
Comment thread tests/benchmarks/json_view/bench_view.cpp Dismissed
Comment thread tests/benchmarks/json_view/bench_view.cpp Dismissed
Comment thread tests/benchmarks/json_view/bench_view.cpp Dismissed
Comment thread tests/benchmarks/json_view/bench_view.cpp Dismissed
Comment thread tests/benchmarks/json_view/bench_view.cpp Dismissed
@nlohmann
nlohmann removed this pull request from stack #5739 October 6, 2026 09:26
@nlohmann
nlohmann force-pushed the json-view/15-view-bench branch from 1d49222 to f39b5b1 Compare October 6, 2026 09:28
@nlohmann
nlohmann changed the base branch from json-view/14b-view-float-layout to json-view/22-view-dump-fast October 6, 2026 09:29
@nlohmann
nlohmann added this pull request to stack #5768 October 6, 2026 09:29
Comment thread tests/benchmarks/json_view/bench_corpus.cpp Dismissed
Comment thread tests/benchmarks/json_view/bench_corpus.cpp Dismissed
Comment thread tests/benchmarks/json_view/bench_corpus.cpp Dismissed
Comment thread tests/benchmarks/json_view/bench_edit.cpp Dismissed
Comment thread tests/benchmarks/json_view/bench_view.cpp Dismissed
@nlohmann
nlohmann force-pushed the json-view/15-view-bench branch from f39b5b1 to eb34240 Compare October 7, 2026 14:44
@nlohmann nlohmann closed this Oct 10, 2026
@nlohmann
nlohmann force-pushed the json-view/15-view-bench branch from 677089d to 7ee0248 Compare October 17, 2026 06:02
@nlohmann
nlohmann force-pushed the json-view/15-view-bench branch from 7ee0248 to c590911 Compare October 17, 2026 06:33
@nlohmann
nlohmann force-pushed the json-view/15-view-bench branch from c590911 to 2b98fb7 Compare October 17, 2026 12:28
@nlohmann
nlohmann force-pushed the json-view/15-view-bench branch from 2b98fb7 to bc532e9 Compare October 17, 2026 21:25
@nlohmann
nlohmann force-pushed the json-view/15-view-bench branch from bc532e9 to c2fd058 Compare October 18, 2026 02:27
@nlohmann
nlohmann force-pushed the json-view/15-view-bench branch from c2fd058 to 3298199 Compare October 18, 2026 04:25
@nlohmann
nlohmann force-pushed the json-view/15-view-bench branch 2 times, most recently from 0d324d3 to cbdc9ff Compare October 18, 2026 08:38
@nlohmann
nlohmann force-pushed the json-view/15-view-bench branch from cbdc9ff to ba41476 Compare October 18, 2026 08:54
@nlohmann
nlohmann force-pushed the json-view/15-view-bench branch from ba41476 to 4747b35 Compare October 18, 2026 11:41
@nlohmann
nlohmann force-pushed the json-view/15-view-bench branch from 4747b35 to 00b7d24 Compare October 18, 2026 12:52
@nlohmann nlohmann mentioned this pull request Oct 18, 2026
1 of 4 tasks
@nlohmann
nlohmann force-pushed the json-view/15-view-bench branch from 00b7d24 to 7ac4e04 Compare October 18, 2026 20:30
@nlohmann
nlohmann force-pushed the json-view/15-view-bench branch from 7ac4e04 to 83e0829 Compare October 19, 2026 15:59
tests/benchmarks/json_view/ holds a comparison with other
libraries, answering the question users ask when they pick one.
It is not built by CMake and not run by CI.

- bench_view.cpp: parse, traverse, select and dump of twitter,
  citm_catalog, canada, jeopardy, a tweet and an RPC request,
  against json_view, yyjson, simdjson, Boost.JSON and json::parse,
  all agreeing and run interleaved.
- bench_corpus.cpp: parse, traverse and dump of any file list.
- bench_edit.cpp: parses, edits through each library's own API,
  and serializes; all outputs must describe the same value.
- compare.py: builds both programs with system or pinned,
  SHA-256-checked downloads, and writes results with the commit,
  CPU, OS, compiler and library versions.
- README.md and a workflow that runs on demand or via a PR label.

Fairness fixes folded in: each engine runs once untimed before
each timed round, so the next one no longer pays for the
previous one's cleanup; dump is also compared with source numbers
against yyjson's raw-number mode; a "simdjson DOM (fresh)" column
and a reused-document column for json_view were added;
compare.py's paths are absolute; downloads use a .part file and a
failed checksum removes the archive.

Signed-off-by: Niels Lohmann <mail@nlohmann.me>
Hash the archives in chunks instead of reading them into memory, download
with a timeout, extract into a temporary directory that is renamed into
place only after success (a half-extracted directory was trusted forever),
and split CXX and CC into arguments so that values like 'ccache g++' work.
Note at the pins that a SHA-256 mismatch of a GitHub tag archive means
that GitHub regenerated it.

Signed-off-by: Niels Lohmann <mail@nlohmann.me>
Signed-off-by: Niels Lohmann <mail@nlohmann.me>
fopen() of the CSV output was not checked, so an unwritable directory made
fprintf() write to a null FILE*; the round count is parsed with strtol and
clamped instead of atoi.

Signed-off-by: Niels Lohmann <mail@nlohmann.me>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

aspect: json_view json_document, json_view (zero-copy view) CI documentation L python Pull requests that update Python code tests

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Idea: borrowed / zero-copy read-only view over a source buffer

2 participants