diff --git a/docs/mkdocs/docs/home/blog/how_3_13_0_got_faster.md b/docs/mkdocs/docs/home/blog/how_3_13_0_got_faster.md
index a87280bdf..ed3dc4842 100644
--- a/docs/mkdocs/docs/home/blog/how_3_13_0_got_faster.md
+++ b/docs/mkdocs/docs/home/blog/how_3_13_0_got_faster.md
@@ -18,9 +18,9 @@ Since 2016, the [README](https://github.com/nlohmann/json#readme) of this librar
> There are certainly faster JSON libraries out there. However, if your goal is to speed up your development by adding
> JSON support with a single header, then this library is the way to go.
-That sentence is still true, but the gap is a lot smaller now. Swap the 3.12.0 header for the 3.13.0 one and
-recompile, and the 182 benchmarks of the library's own suite run 3.0× as fast on an Apple M1 Max (geometric mean; 3.1× on an Intel MacBook Pro from 2016),
-without a line of your code changing. Parsing typical documents got 3.1–4.3× as fast, and writing them
+That sentence is still true, but the gap is a lot smaller now. Swap the 3.12.0 header for the 3.13.0 one,
+recompile, and the 182 benchmarks of the library's own suite run 3.0× as fast on an Apple M1 Max (geometric mean; 3.1× on an Intel MacBook Pro from 2016).
+Not a line of your code changes. Parsing typical documents got 3.1–4.3× as fast, and writing them
3.0–6.7×. The new version also adds [`json_document`](../../features/json_view.md), a second way to
hold a parsed document, which competes with the fastest C++ and Rust JSON libraries; [How it
compares](#how-it-compares) has the numbers, including where it loses. The price is compile time: two small
@@ -31,12 +31,12 @@ This post is about *how*. Hardly any of the techniques is new: we borrowed them
[simdjson](https://github.com/simdjson/simdjson), [yyjson](https://github.com/ibireme/yyjson),
[fast_float](https://github.com/fastfloat/fast_float), [Żmij](https://github.com/vitaut/zmij),
[serde_json](https://github.com/serde-rs/json), [Glaze](https://github.com/stephenberry/glaze),
-[SQLite](https://sqlite.org), [Boost.JSON](https://github.com/boostorg/json), or a textbook, and we name the source at each step. What we think is
-worth writing about is the *combination*, and the conditions it had to work under: a C++11 header-only library whose
-3.x API has been stable for almost nine years, with a value type that is a template over its string, number, object,
-array, and allocator types; input from strings, streams,
-`FILE*`, iterator pairs, wide strings, and user-defined adapters; output that had to stay the same byte for byte; and
-error messages that had to keep pointing at the same byte. The few deliberate changes of output are listed in
+[SQLite](https://sqlite.org), [Boost.JSON](https://github.com/boostorg/json), or a textbook, and we name the source at each step. What is
+worth writing about is the *combination*, and the conditions it had to work under. It is a header-only C++11 library
+whose 3.x API has been stable for almost nine years, and whose value type is a template over its string, number,
+object, array, and allocator types. Input comes from strings, streams, `FILE*`, iterator pairs, wide strings, and
+user-defined adapters. Output had to stay the same byte for byte, and error messages had to keep pointing at the same
+byte. The few deliberate changes of output are listed in
[Appendix F](#f-behavior-changes).
Every number comes from a benchmark we ran for this post or from the pull request that made the change. The
@@ -135,7 +135,7 @@ A few things stand out:
- **Regressions happen in between.** A UTF-8 check of every text string in the binary readers, byte by byte, made
`from_cbor()` of `twitter.json` 37 % slower for 66 commits, until the readers were made lenient again by default, as
in 3.12.0, with the check as an option. That decision was about the format specifications, which don't require a
- decoder to check, not about speed; the cost went with it. It never shipped in a release.
+ decoder to check, not about speed. The cost went with it. It never shipped in a release.
- **The input decides how big a step is.** Removing the error-message buffer measured 8 % on the benchmark of its pull
request; on the indented `citm_catalog.json` it saved 35 %, because every space of the indentation went through
that buffer.
@@ -226,8 +226,8 @@ the branch on `next_unget` and the adapter's bounds check and pointer increment.
Containers with `data()` and `size()` over single-byte elements now become a pointer
range in every C++ standard, not only with C++20's contiguous iterators. `data()` and `size()` alone would be duck
-typing, so the trait also requires the container's `value_type` to be the element type behind `data()`; a type whose
-`size()` counts something else keeps the iterator path, which is slower but correct. On a pointer range, strings,
+typing, so the trait also requires the container's `value_type` to be the element type behind `data()`. A type whose
+`size()` counts something else keeps the iterator path: slower, but correct. On a pointer range, strings,
numbers and the indentation after a line break are scanned in bulk (themes 2 and 3). Every fast path defers to the byte path on
anything malformed, so error messages and byte positions stay the same, with one deliberate correction (the column
after a number at the end of a line). [Appendix J](#j-validation) describes how that was checked.
@@ -264,19 +264,22 @@ request's measurement before the change; now it takes just 9–16 % longer.
`std::fgetc` locks the `FILE` on every call. 3.13.0 locks it once per parse (`flockfile`, then
`getc_unlocked`; `_lock_file` and `_fgetc_nolock` with the Microsoft runtime). `flockfile` is recursive, so the binary
-readers' `fread()` and a SAX handler's own `ftell()` keep working. On macOS, parsing `citm_catalog.json` from a
-`FILE*` got 4.4× as fast in the pull request's measurement; with glibc only 1.1–1.3×,
-because glibc's per-call locking is already cheap. Two details: MemorySanitizer builds keep `fgetc`, because glibc's inline `getc_unlocked`
-reads a buffer that uninstrumented glibc filled; and a SAX handler that waits for another thread using the same
-`FILE` now deadlocks, which the documentation says. Both paths can be switched off, with
+readers' `fread()` and a SAX handler's own `ftell()` keep working. Whereas parsing
+`citm_catalog.json` from a `FILE*` got 4.4× as fast on macOS in the pull request's measurement, with glibc it got only
+1.1–1.3× as fast, because glibc's per-call locking is already cheap. Two details: MemorySanitizer builds keep `fgetc`,
+because glibc's inline `getc_unlocked` reads a buffer that uninstrumented glibc filled. And a SAX handler that waits
+for another thread using the same `FILE` now deadlocks, as the documentation says. Both paths can be switched off, with
[`JSON_DISABLE_STREAM_BULK_SCAN`](../../api/macros/json_disable_stream_bulk_scan.md) and
[`JSON_DISABLE_UNLOCKED_FILE_IO`](../../api/macros/json_disable_unlocked_file_io.md).
## 2 Work on words, not bytes { #theme-2 }
+The core of a JSON lexer is "copy bytes until something interesting happens". Theme 1 gave it ranges of bytes. This
+theme reads them a word at a time.
+
### Eight bytes in a word
-The core of a JSON lexer is "copy bytes until something interesting happens". In a string, the interesting bytes are
+In a string, the interesting bytes are
the closing quote, a backslash, a control character, and anything above 0x7F (UTF-8). This test checks eight bytes at
once with ordinary 64-bit arithmetic:
@@ -369,8 +372,8 @@ while (e - p >= 16)
On `twitter.json`, the vector scan made `json::parse()` 5 % faster with Clang and 9 % with GCC on an M1 Max; on long
strings, 19–24 %. It did not win from the first byte. On the M1, starting with NEON made short strings 3–4 %
-slower and the rest of the lexer bigger, so ARM runs four 8-byte words first and keeps the vector loop out of line;
-x86-64 runs one word first. Inlining the vector code into the lexer cost 5 % of validating `jeopardy.json` on the M1,
+slower and the rest of the lexer bigger. So ARM runs four 8-byte words first and keeps the vector loop out of line,
+while x86-64 runs one word first. Inlining the vector code into the lexer cost 5 % of validating `jeopardy.json` on the M1,
although no string of that file is long enough to reach it: the scan function, which runs once per token, saved more
registers and needed a stack slot.
@@ -444,9 +447,9 @@ write_buffer_pos += escape[7]; // keep o
One 8-byte store and one add, with no branch on the length; the padding lies behind the write position and is
overwritten by the next write. The writer makes sure that 8 bytes are free before it copies. The idea is from [orjson](https://github.com/ijl/orjson)'s
-`QUOTE_TAB`, which orjson took from [sonic-rs](https://github.com/cloudwego/sonic-rs). Dumping 100,000 log lines full of `\n`, `\t`, and quotes got 23 % faster
-on an M1 Max, but most of that came from writing straight into the write buffer instead of an intermediate one; the
-table alone gained 2–7 %.
+`QUOTE_TAB`, which orjson took from [sonic-rs](https://github.com/cloudwego/sonic-rs). Although dumping 100,000 log lines full of `\n`, `\t`, and quotes got 23 %
+faster on an M1 Max, most of that came from writing straight into the write buffer instead of an intermediate one.
+The table alone gained 2–7 %.
## 3 Own the number conversions { #theme-3 }
@@ -540,7 +543,7 @@ up to 19 digits, that always suffices ([Mushtak and Lemire, 2023](https://arxiv.
their first 19 digits and that plus one; only if the two round differently are all digits compared with the exact
midpoint. [Appendix I](#i-details-of-the-themes) has the details. Because it is integer arithmetic, the result no
longer depends on the C locale. (JSON always uses `.`, but `strtod` expects the locale's decimal separator, so 3.12.0
-replaced the `.` before calling it, which failed for separators longer than one byte, [#5660][5660].)
+replaced the `.` before calling it, which [failed for separators longer than one byte][5660].)
[Appendix J](#j-validation) describes how the conversion was checked against `strtod`.
### Writing: Żmij
@@ -573,9 +576,8 @@ A lot of what the 3.12.0 parser did was correct, cheap per call, and read by nob
The old `get()` of theme 1 pushed every character into `token_string`, a buffer that only the "last
read" part of an error message uses. For seekable input, the lexer now remembers the offset where the token started
-and copies the text from the input if an error happens. The pull request measured 8 % on its own benchmark; in the
-timeline, parsing the indented `citm_catalog.json` got 35 % faster at that commit, because every space of the
-indentation went through the buffer. For streams, the carry buffer of theme 1 does the same job; the per-character
+and copies the text from the input if an error happens. This is the 35 % step of the timeline: in the indented
+`citm_catalog.json`, every space of the indentation went through the buffer. For streams, the carry buffer of theme 1 does the same job; the per-character
copy had been about 20 % of what was left of a stream parse.
[`accept()`](../../api/basic_json/accept.md) went one step further: it doesn't copy strings and keys at all. The
@@ -633,7 +635,7 @@ A class derived from `json_sax_dom_parser` does not match the specialization, so
every other handler, the lexer only records a pointer to the token and its length, and copies on the first
`get_string()`. That is safe for contiguous input, which has to outlive the parse anyway; in a stream's window, the
next refill could overwrite the text, so there the copy is made at once. The first version put the "is there pending
-text?" check into the `get_string()` that keys and strings use too, and lost 2–3 % on string-heavy input; they now
+text?" check into the `get_string()` that keys and strings use too, and lost 2–3 % on string-heavy input. Keys and strings now
call a separate function without the check. Parsing `canada.json` got 9–11 % faster.
### A copy that should have been a move
@@ -679,10 +681,9 @@ Container::swap(tmp);
The new element is constructed before any value is moved, so `j.emplace("new", std::move(j["old"]))` still works, and
the strong guarantee holds without a `try`, which matters because the library also builds with `-fno-exceptions`.
Parsing `twitter.json` into `ordered_json` needs 33 % of the allocations and, in the pull request's
-measurement, about 50 % of the time. A trap on the way: compilers with the EDG front end (Intel's icpc, NVIDIA's nvc++)
-consider that pair's move constructor
-`noexcept`, so there the vector already "moves", the new path is not taken, and a key copy that throws during growth
-calls `std::terminate`.
+measurement, about 50 % of the time. Unfortunately, compilers with the EDG front end (Intel's icpc, NVIDIA's
+nvc++) consider that pair's move constructor `noexcept`. There, the vector already "moves", the new path is not
+taken, and a key copy that throws during growth calls `std::terminate`.
### Containers built one element at a time
@@ -715,9 +716,9 @@ creates a temporary `basic_json` for every object key it writes (−55 % on `twi
The SAX interface hands the lexer's buffer to the handler as `string_t&` and allows it to be moved from; the lexer
starts each token with an empty buffer anyway. In September, moving string values instead of copying them saved
- 2–3 %. Moving keys too was measured and dropped: the buffer is sized for the longest token so far, keys are
- short and values long, so moving a key hands a large buffer to a short string and the next value has to grow a
- new one, 11.9 % slower on 40,000 short strings. In October, with much of the lexer around it rewritten, keys were
+ 2–3 %. We measured moving keys too, and dropped it. The buffer is sized for the longest token so far, and
+ keys are short while values are long. So moving a key hands a large buffer to a short string, and the next value
+ has to grow a new one: 11.9 % slower on 40,000 short strings. In October, with much of the lexer around it rewritten, keys were
measured again: `parse()` of `twitter.json` got 3.3 % faster with Clang and 5.0 % with GCC, so keys are moved
now too. An idea that lost once is worth measuring again when the code around it changes.
@@ -743,8 +744,8 @@ case 't': // 3.12.0
With `-fstack-protector-strong`, which Ubuntu's GCC enables by default and many distributions use for their packages,
*any* local array gives a function a stack canary: the prologue loads a secret (on x86-64 Linux from thread-local
-storage) and stores it next to the locals, and every return compares it. That happens on every call, also for the `[`, `,` and `"` tokens that never touch the array; the
-canary's load alone was 11 % of the function's samples. (Plain `-fstack-protector` only protects character arrays of
+storage) and stores it next to the locals, and every return compares it. Even worse, that happens on every call, also for the `[`, `,` and `"` tokens that never touch the
+array. The canary's load alone was 11 % of the function's samples. (Plain `-fstack-protector` only protects character arrays of
at least 8 bytes, so it didn't trigger here.) Now the literal is a string literal whose length is a template
parameter, compared on contiguous input with one `memcmp` of constant size, and the dispatch has no canary and no array on the
stack:
@@ -788,7 +789,7 @@ throw_incompatible_reference_type(obj); // JSON_HEDLEY_NO_RETURN JSON_HEDLEY_N
The same function in 3.13.0: a test and a call of a helper that is never inlined.
-That fixed 27 throw sites, and the ones that build a message got helpers that are never inlined; iterator arithmetic got 6× as fast with Clang, and `std::sort` over a `json` array of
+That fixed 27 throw sites. Iterator arithmetic got 6× as fast with Clang, and `std::sort` over a `json` array of
10,000 integers in user code 6–9 % faster. The value's own lifecycle had the same problem: the destructor, the copy
constructor, and [`operator==`](../../api/basic_json/operator_eq.md) contained the code for every type, so they were too big to inline even for a number. Clang
reported about 380 call sites where they were not inlined. Now the destructor and the copy constructor start with an
@@ -999,7 +1000,7 @@ std::size_t encode_first_byte_and_tail(CharType* out, std::uint8_t first_byte,
The table is a table because Clang vectorized the equivalent comparisons into slower code, and the unconditional
8-byte store is why `reserve()` asks for 9 bytes per integer. Writing an array of small signed integers as CBOR,
-which had not got faster before, is now 4.2× as fast as with 3.12.0 with Clang on an M1 Max, and was 2.8× as fast
+which had not gotten faster before, is now 4.2× as fast as with 3.12.0 with Clang on an M1 Max, and was 2.8× as fast
with GCC 13 on x86-64 when the change was made. One more
lesson from these changes: the loop over an array is a separate function that is never inlined. Inlined into the
function that writes any value, its register needs made every call of that function save more registers, once per
@@ -1029,7 +1030,7 @@ matters to anyone who keeps `json` values in a `std::set`, uses them as `std::ma
`operator<` is derived from [`operator<=>`](../../api/basic_json/operator_spaceship.md), which asks once and was never affected.
3.13.0 compares in one pass over an explicit stack of iterator pairs and gets "less", "equal", "greater" or
-"unordered" for each pair at once, so the ordering question is asked once per element. Two subtleties were found by
+"unordered" for each pair at once, so the ordering question is asked once per element. We found two subtleties by
comparing 68,121 results with the old code, not by reading it. `std::lexicographical_compare` steps over a pair it
cannot order (a NaN) and goes on, while the three-way version stops there, so the pass has a flag for which behavior
the caller needs. And object keys must be compared with `operator==` when testing equality, not with the object's comparator:
@@ -1043,10 +1044,10 @@ constructor, `std::hash`, [`flatten()`](../../api/basic_json/flatten.md) and the
optimized builds `operator==`, [`diff()`](../../api/basic_json/diff.md) and the other binary writers at 300,000 levels;
servers that copy JSON from clients hit that. Other libraries limit the depth of what they parse ([Boost.JSON](https://github.com/boostorg/json) at 32,
[serde_json](https://github.com/serde-rs/json) at 128, [simdjson](https://github.com/simdjson/simdjson) at 1024) or
-parse and write iteratively ([yyjson](https://github.com/ibireme/yyjson)). Making every walk iterative made `dump()`,
-copying and merging up to 20 % slower on ordinary documents; [RapidJSON](https://github.com/Tencent/rapidjson)
-also parses recursively unless asked
-otherwise. So 3.13.0 bounds the recursion instead:
+parse and write iteratively ([yyjson](https://github.com/ibireme/yyjson)), and
+[RapidJSON](https://github.com/Tencent/rapidjson) parses recursively unless asked otherwise. We tried making every walk
+iterative, but that made `dump()`, copying and merging up to 20 % slower on ordinary documents. So 3.13.0 bounds the
+recursion instead:
```cpp
constexpr std::size_t recursion_depth_limit() noexcept { return 128; }
@@ -1138,10 +1139,9 @@ static_assert(sizeof(node) == 16, "node must stay 16 bytes");
Offsets are 32 bits, which limits a document to 4 GiB.
- **Integers are converted while their digits are in the cache,** into the last 8 bytes of their node.
- **Floats stay text until you read them.** The node records the number of digits before and after the point, and
- reading converts with the same Clinger/Eisel-Lemire code as `json::parse`. Converting them during the parse was
- tried and dropped: parsing `canada.json` and reading every number got 4–7 % faster, but parsing alone 36–90 %
- slower. A
- default `dump()` writes the shortest form of a float, as `json` does; [`number_format::source`](../../api/basic_json_view/number_format.md) copies the source
+ reading converts with the same Clinger/Eisel-Lemire code as `json::parse`. We tried converting them during the
+ parse. Although parsing `canada.json` and reading every number got 4–7 % faster, parsing alone got 36–90 % slower,
+ so we dropped it. A default `dump()` writes the shortest form of a float, as `json` does; [`number_format::source`](../../api/basic_json_view/number_format.md) copies the source
text of every number instead.
- **SIMD and tables** as in theme 2, and `\u` escapes decoded with a table, as yyjson does.
- **A hash index for big objects,** an idea from [Boost.JSON](https://github.com/boostorg/json). An object with 128 or more members gets an open-addressing
@@ -1170,9 +1170,9 @@ fast because it can't change. [`json_editable_document`](../../features/json_vie
which stay where they are. The list doubles when it grows, so `push_back()` is amortized constant.
Views stay valid across edits, because values never move. Reading an edited container costs one flag test per
-container and one test per element for a link; the link case is out of line, so that it compiles to a predicted
-branch rather than a conditional select that would delay the following loads. A read-only document doesn't carry any
-of this; an editable one that hasn't been edited reads within 2 % of it on most rows and within 8 % on the worst. And because untouched values keep
+container and one test per element for a link. The link case is out of line, so that it compiles to a predicted
+branch rather than a conditional select that would delay the following loads. Whereas a read-only document carries
+none of this, an editable one that hasn't been edited reads within 2 % of it on most rows and within 8 % on the worst. And because untouched values keep
their source text, [`dump()`](../../api/basic_json_view/dump.md) with
`number_format::source` writes back a patched configuration file with its member order
and the spelling of every number (`19.90`) intact, which `json` (sorted keys) and `ordered_json` (`19.9`) can't.
@@ -1231,11 +1231,16 @@ or the operating system. It is not neutral in every detail, though:
- The string has 64 bytes of spare capacity, so simdjson, which reads up to 64 bytes past the end, can use it without
a copy. A program would load the file into such a buffer in the first place.
-- yyjson, RapidJSON, and simd-json can parse in place, which needs a buffer they may change. So they first copy the
- string, inside the timed call, and the copy counts towards their time: about 30 µs for `twitter.json` on the Mac. Even so, yyjson's in-place parse is about
- as fast as its parse without a copy, which is measured too.
+- yyjson, RapidJSON, and simd-json can parse in place, which changes the buffer. So every call gets a fresh copy of the
+ string, and the copy counts toward their time: 11–14 µs for `twitter.json` on the Mac, and 30–46 µs on x86-64,
+ where fresh memory costs page faults. yyjson's normal read makes the same copy internally, which is why both ways
+ take about as long. A server that validates a request and then passes it on needs the copy. A program that reads a
+ file into a buffer it owns can skip it, so we also timed the in-place ways with the copy made before the timer
+ starts. On the Mac, yyjson then comes 2–4 % closer and stays second, and on `twitter.json` it sums and selects 2–3 %
+ faster than `json_document`. On x86-64, it ties `json_document` in *sum* with Clang and takes 1.06× and 1.19× its
+ time with GCC 16 and GCC 13.
- `json_document` reads the string in place and keeps pointing into it. A document that owns its text
- ([`parse_copy()`](../../api/basic_json_document/parse_copy.md)) costs one copy more.
+ ([`parse_copy()`](../../api/basic_json_document/parse_copy.md)) costs one copy more: 3–5 % on the Mac.
- Reading from files and streams is not part of the comparison.
```mermaid
@@ -1259,46 +1264,45 @@ finding them. A minifier only needs to find whitespace, not understand the value
RapidJSON's default and sonic-cpp's too. Each is a good choice for input that is known to be valid, for example text
your own program wrote a moment ago. To tell these ways apart, every way is also fed four broken inputs: the file cut
short by one byte, a stray comma, an invalid UTF-8 byte in a string, and garbage after the value. A way that rejects all four validates fully,
-and each cell shows the fastest such way. If a way that checks or does less is faster, it is shown in the right half
-of the cell: "U" trusts UTF-8, "P" reads lazily, only removes whitespace or, in one Glaze cell on x86-64, doesn't check
-what follows the value, and "N" writes each number by copying its text instead of formatting it, which `json_document`,
-yyjson, and RapidJSON can do.
+and each cell shows the fastest such way. A way that checks or does less appears in the right half
+of the cell, with a letter that the caption explains.
A word on who set the rules: we wrote every library's benchmark code, chose the files and the rules, and tuned
-3.13.0 on the same machines and files. No maintainer of the other libraries has reviewed their code. To check
-ourselves, we also ran files and a conformance suite we never tuned on ([below](#files-and-checks-we-didnt-choose)).
+3.13.0 on the same machines and files. No maintainer of the other libraries has reviewed their code. If you know a
+faster way for any of them, please open an issue, and we will rerun the comparison. To check ourselves, we also ran files and a conformance suite we never tuned on ([below](#files-and-checks-we-didnt-choose)).
--8<-- "images/perf_blog_usecases_mac.svg"
-
Time per library and use case on an Apple M1 Max: each library's fastest fully validating way, as the geometric mean over `twitter.json`, `citm_catalog.json`, `canada.json` and `jeopardy.json`, relative to the fastest library, so that every file counts equally. Less is better. Where a way that checks or does less is at least 3 % faster, it is shown in the right half of the cell: "U" trusts UTF-8, "P" reads lazily, only removes whitespace or, in one Glaze cell on x86-64, doesn't check what follows the value, and "N" copies number text instead of formatting it. "n/a" means the library has no API for the use case. Hover over or tap a cell for the times per file and the way; the tables are in [Appendix E](#e-comparison-with-other-libraries). sonic-cpp's README lists only x86-64 Linux; we show it on the Mac anyway.
+
Time per library and use case on an Apple M1 Max: each library's fastest fully validating way, as the geometric mean over `twitter.json`, `citm_catalog.json`, `canada.json` and `jeopardy.json`, so that every file counts equally, divided by the fastest library's mean, which therefore shows 1.00. Less is better. Where a way that checks or does less is at least 3 % faster, it is shown in the right half of the cell: "U" trusts UTF-8, "P" reads lazily, only removes whitespace or, in one Glaze cell on x86-64, doesn't check what follows the value, and "N" copies number text instead of formatting it. "n/a" means the library has no API for the use case. Hover over or tap a cell for the times per file and the way; the tables are in [Appendix E](#e-comparison-with-other-libraries). sonic-cpp's README lists only x86-64 Linux; we show it on the Mac anyway.
### Results on the Mac
-On the Apple M1 Max, `json_document` is the fastest library that validates fully in all six use cases, and yyjson is
-second in every column. That includes editing, where the editable document is faster than yyjson's mutable document:
+On the Apple M1 Max, among the ways that reject all four broken inputs, `json_document` has the lowest mean on the four
+files in all six use cases, and yyjson is second in every column. That includes editing, where the editable document is faster than yyjson's mutable document:
the speed doesn't depend on being read-only.
Some caveats belong next to that:
-- **The lead is not the same on every file.** On `twitter.json`, yyjson is within 1–5 % in *validate*, *sum*,
- and *select*. Most of the lead comes from the indentation of `citm_catalog.json`, the floats of `canada.json` (next
- point) and the size of `jeopardy.json`.
+- **The lead is not the same on every file.** On `twitter.json`, yyjson is within 3–7 % in *validate*, *sum*,
+ and *select*. Most of the lead comes from `citm_catalog.json`, the floats of `canada.json` (next point) and the
+ size of `jeopardy.json`.
- **Floats are converted only when read.** That helps `json_document` in *validate* and *select* on `canada.json`,
where most others convert 111,080 floats nobody looks at. Libraries that can skip the conversion use that mode
- there, but only Boost.JSON gains much: it validates `canada.json` in 48 % of the time. *sum* reads every number, so it
+ there, but only Boost.JSON gains much: it validates `canada.json` in 47 % of the time. *sum* reads every number, so it
is the fair comparison for floats; there, `json_document` is still slightly ahead of yyjson.
- **Checking less pays in *select*.** If the input needn't be checked fully, simdjson's On-Demand API and Glaze
- reading into structs select in 0.75× and 0.82× `json_document`'s time, and in 0.56–0.61× on x86-64: the right
+ reading into structs select in 0.75× and 0.82× `json_document`'s time (Glaze is faster only on `citm_catalog.json` and
+ `canada.json`), and in 0.54–0.62× on x86-64: the right
halves of the *select* column.
- **Indentation.** Two of the four files are indented, which theme 2 helps. On minified copies of
- `twitter.json` and `citm_catalog.json`, yyjson comes closer (*validate* 1.26 → 1.25, *sum* 1.10 → 1.03), but
- `json_document` stays first.
+ `twitter.json` and `citm_catalog.json`, yyjson comes closer in *sum* (1.11 → 1.04) but not in
+ *validate* (1.28 → 1.27), and `json_document` stays first.
- **Number text can be copied.** With [`number_format::source`](../../api/basic_json_view/number_format.md), `json_document` writes each number by copying its text
instead of formatting it, and yyjson and RapidJSON can read numbers as text and write them back unchanged. The
output keeps the source's spelling of each number, which is all a minifier needs. It makes writing `canada.json`
- with `json_document` 4.1× as fast and minifying it 1.9× as fast; the "N" halves of *edit*, *minify*, and
+ with `json_document` 4.0× as fast and minifying it 1.9× as fast; the "N" halves of *edit*, *minify*, and
*write* show it.
- **Small documents.** On a 2.5 KB message, the fastest libraries take about a microsecond on the Mac and 1.2–2
on x86-64. yyjson validates and sums it as fast as `json_document` on the Mac, and with GCC 16 on x86-64,
@@ -1319,14 +1323,14 @@ And nlohmann/json without `json_document`? `json` builds a tree of separate allo
*select*, *edit*, and *minify* it takes several times as long as `json_document`, like Boost.JSON and RapidJSON. Where
no tree is needed, it is closer: on the Mac, a SAX handler is on par with Boost.JSON's, and
[`json::accept()`](../../api/basic_json/accept.md) takes 6 % longer than Boost.JSON without number conversion (on
-x86-64, Boost.JSON validates in 0.67–0.70× and sums in 0.76–0.81× the time). `dump()` writes faster than Boost.JSON,
+x86-64, Boost.JSON validates in 0.65–0.69× and sums in 0.75–0.79× the time). `dump()` writes faster than Boost.JSON,
RapidJSON and serde_json, and about as fast as Glaze.
### Results on x86-64
-On x86-64, the same programs ran on a virtual server whose CPU presents itself as Haswell, pinned to one core and built with `-march=haswell`: with the
-newest GCC, 16.2 (the figure below), and with GCC 13.4 and Clang 23 (both in
-[Appendix E](#e-comparison-with-other-libraries)), on Ubuntu 26.04. The compiler matters less than one might think. On the Mac, GCC 16
+On x86-64, the same programs ran on a virtual server with Ubuntu 26.04 whose CPU presents itself as Haswell,
+pinned to one core. We built them with `-march=haswell` and three compilers: the newest GCC, 16.2 (the figure below),
+GCC 13.4, and Clang 23 (both in [Appendix E](#e-comparison-with-other-libraries)). The compiler matters less than one might think. On the Mac, GCC 16
and Clang 23 put the same library first in every column as Apple clang, and on x86-64, the three compilers disagree
only about *sum*.
@@ -1336,25 +1340,29 @@ only about *sum*.
The same comparison on the x86-64 server with GCC 16.2, with the same cells and halves as the figure above. Less is better; hover over or tap a cell for the times and the way. The figures for GCC 13 and Clang 23 are in [Appendix E](#e-comparison-with-other-libraries).
-`json_document` is the fastest in five of the six columns with GCC and in all six with Clang, where its lead in
-*sum* (7 % over sonic-rs) is within the noise of this machine. In *validate*, the next library takes 1.8–2× as long, because validating no longer builds an index (theme 8). Behind those means, it loses on single files:
+`json_document` has the lowest mean in five of the six columns with GCC, where simdjson is ahead in *sum*, and in all
+six with Clang. This machine is noisy, though: between two runs, a library's time relative to `json_document`'s moved
+by up to 9–10 % (95th percentile; 2.5 % on the Mac). So Clang's 7 % lead in *sum* over sonic-rs is no result, and
+without `twitter.json`, sonic-rs is first. In *validate*, the next library takes 1.8–1.9× as long, or 1.6–1.7× if every
+library may reuse its parser, because validating no longer builds an index (theme 8). Behind those means, it loses on single files:
- **One pass over every value, with GCC.** simdjson's On-Demand API sums all numbers of `twitter.json`,
- `citm_catalog.json`, and `canada.json` in 0.83–0.95× `json_document`'s time with GCC 16 (0.88–0.91× with
+ `citm_catalog.json`, and `canada.json` in 0.83–0.99× `json_document`'s time with GCC 16 (0.86–0.91× with
GCC 13). With Clang, `json_document` is ahead there, except on `canada.json`, where yyjson, which converts the
floats during the parse, is faster than converting them on access.
- **Fresh large documents.** For the 56 MB `jeopardy.json`, a fresh document needs a fresh 52 MB index, and on Linux
the page faults of that much new memory are a large part of the time: copying the file into fresh memory alone
takes about 50 % as long as `json_document`'s parse. Boost.JSON's SAX parser and sonic-rs, which build nothing,
- sum it in 0.65–0.74× the time. Reusing the document cuts `json_document`'s time by 37–43 %, to 52–54 ms,
- a little less than sonic-rs takes. Validating needs no index: `json_document::accept()` takes 34–35 ms, and no
- library validates `jeopardy.json` faster, even reused (the next is simdjson's reused parser at 47–54 ms). On the Mac, a
+ sum it in 0.66–0.72× the time. Reusing the document cuts `json_document`'s time by 40–43 %, to 51–53 ms,
+ less than sonic-rs's 60 ms. Validating needs no index: `json_document::accept()` takes 35 ms, and no
+ library validates `jeopardy.json` faster, even reused (the next is simdjson's reused parser at 47–49 ms). On the Mac, a
fresh document costs hardly more than a reused one.
-- **Writing floats.** yyjson writes `canada.json` in 0.87× (GCC 16) to 0.93× (Clang 23) the time, because
+- **Writing floats.** yyjson writes `canada.json` in 0.87–0.97× the time (and Glaze, with GCC 13, in 0.96×), because
`json_document` first converts most floats from their text. Writing the source text instead
(`number_format::source`, the "N" half) avoids that.
-- **simdjson's DOM.** It selects from `citm_catalog.json` 10 % faster with GCC 16 and 5 % faster with Clang 23;
- with GCC 13, `json_document` is 2 % ahead.
+- **simdjson's DOM.** It selects from `citm_catalog.json` 16 % faster with GCC 16 and 1–4 % faster with GCC 13 and
+ Clang 23, and from `twitter.json` 1–6 % faster. Boost.JSON, Glaze, sonic-cpp, and yyjson are 1–3 % ahead in a few
+ more cells, which is within the noise.
The full tables, with each library's variants, are in [Appendix E](#e-comparison-with-other-libraries).
@@ -1363,7 +1371,7 @@ The full tables, with each library's variants, are in [Appendix E](#e-comparison
The four files are the ones 3.13.0 was tuned on, so we also ran *validate*, *sum*, *minify*, and *write* on the 19
other files of simdjson's [`jsonexamples`](https://github.com/simdjson/simdjson-data/tree/master/jsonexamples). By
geometric mean, `json_document` stays the fastest in *validate*, *minify*, and *write* on every compiler, and in *sum*
-with Clang and on the Mac; with GCC, simdjson sums in 0.87–0.88× its time. File by file, for *validate*:
+with Clang and on the Mac; with GCC, simdjson sums in 0.86–0.87× its time. File by file, for *validate*:
--8<-- "images/perf_blog_heldout.svg"
@@ -1371,19 +1379,17 @@ with Clang and on the Mac; with GCC, simdjson sums in 0.87–0.88× its time. Fi
*validate* on the 19 files: `json_document`'s fastest fully validating way against the fastest other library's, per file; below 1.0× it is the fastest. Less is better. The right column names the next library on x86-64 (GCC 16.2).