diff --git a/.github/workflows/codeql-analysis.yml b/.github/workflows/codeql-analysis.yml
index 99541a96f..672c17c8b 100644
--- a/.github/workflows/codeql-analysis.yml
+++ b/.github/workflows/codeql-analysis.yml
@@ -41,11 +41,10 @@ jobs:
uses: github/codeql-action/init@9b3254085494b55a0f7c4c2b3d2d52cc712ce370 # v4.39.1
with:
languages: c-cpp
-
- # Autobuild attempts to build any compiled languages (C/C++, C#, or Java).
- # If this step fails, then you should remove it and run the build manually (see below)
- - name: Autobuild
- uses: github/codeql-action/autobuild@9b3254085494b55a0f7c4c2b3d2d52cc712ce370 # v4.39.1
+ # Analyze the sources without compiling them. Autobuild compiled the
+ # whole test suite and the resulting database was so large that the
+ # runner often lost communication with the server while importing it.
+ build-mode: none
- name: Perform CodeQL Analysis
uses: github/codeql-action/analyze@9b3254085494b55a0f7c4c2b3d2d52cc712ce370 # v4.39.1
diff --git a/.github/workflows/ubuntu.yml b/.github/workflows/ubuntu.yml
index 581fb6081..e0cd83af9 100644
--- a/.github/workflows/ubuntu.yml
+++ b/.github/workflows/ubuntu.yml
@@ -42,7 +42,7 @@ jobs:
- name: Check that Meson and pkg-config offer the CMake options
run: make check_build_options
- name: Install Meson
- run: pip install meson
+ run: pip install --require-hashes -r cmake/requirements/requirements-meson.txt
- name: Install with Meson
run: |
meson setup build-meson --prefix=${{ github.workspace }}/install
diff --git a/BUILD.bazel b/BUILD.bazel
index 2b1c9f146..b7908bcf7 100644
--- a/BUILD.bazel
+++ b/BUILD.bazel
@@ -30,6 +30,7 @@ cc_library(
"include/nlohmann/detail/exceptions.hpp",
"include/nlohmann/detail/hash.hpp",
"include/nlohmann/detail/input/binary_reader.hpp",
+ "include/nlohmann/detail/input/contiguous_parser.hpp",
"include/nlohmann/detail/input/input_adapters.hpp",
"include/nlohmann/detail/input/json_sax.hpp",
"include/nlohmann/detail/input/lexer.hpp",
diff --git a/cmake/requirements/requirements-meson.txt b/cmake/requirements/requirements-meson.txt
new file mode 100644
index 000000000..b8810a0f6
--- /dev/null
+++ b/cmake/requirements/requirements-meson.txt
@@ -0,0 +1,3 @@
+meson==1.12.1 \
+ --hash=sha256:930bc7542cbd9f57009e182fd014eba48cf1a0180a7b9006d2d32d8f168d8b02 \
+ --hash=sha256:ab0a6ca09f8ef70c564c8241fb5a23957886a0b53fb58412b5e07eaf07dba743
diff --git a/docs/mkdocs/docs/api/macros/json_diagnostic_positions.md b/docs/mkdocs/docs/api/macros/json_diagnostic_positions.md
index 4dac9f9ff..340498acb 100644
--- a/docs/mkdocs/docs/api/macros/json_diagnostic_positions.md
+++ b/docs/mkdocs/docs/api/macros/json_diagnostic_positions.md
@@ -34,6 +34,9 @@ or quotes.
Note that enabling this macro increases the size of every JSON value by two `std::size_t` fields and adds slight runtime
overhead to parsing, copying JSON value objects, and the generation of error messages for exceptions. It also causes
these values to be reported in those error messages.
+In addition, `parse()`, `accept()`, and `sax_parse()` no longer use the
+[faster path for input in one block of memory](../../features/parsing/index.md#input-sources-and-performance), so
+parsing a string or a buffer is slower than without the macro.
If [`JSON_DIAGNOSTICS`](json_diagnostics.md) is also set, the message contains the path and the byte range, such as
`(/1) (bytes 4-8)`. An exception about the root value has no path and only the range, such as `(bytes 0-4)`.
diff --git a/docs/mkdocs/docs/api/macros/json_disable_stream_bulk_scan.md b/docs/mkdocs/docs/api/macros/json_disable_stream_bulk_scan.md
index 7befb6cde..9af9c3aea 100644
--- a/docs/mkdocs/docs/api/macros/json_disable_stream_bulk_scan.md
+++ b/docs/mkdocs/docs/api/macros/json_disable_stream_bulk_scan.md
@@ -48,8 +48,8 @@ The default value is `0`: streams are scanned in bulk.
!!! note "Performance"
Parsing from a `std::ifstream` or `std::istringstream` is typically 1.5 to 2 times as fast as before, and reading
- many small values from one stream with `operator>>` 1.4 to 4 times as fast. Parsing from a stream is then within
- about 10 to 20 % of first reading the whole file into a `std::string` and parsing that.
+ many small values from one stream with `operator>>` 1.4 to 4 times as fast. Parsing from a stream then takes about
+ 1.15 to 1.45 times as long as first reading the whole file into a `std::string` and parsing that.
A stream buffer without a get area (one that only overrides `underflow()` and `uflow()` and hands out every
character from there) offers nothing to scan in bulk, so it is read one character at a time as before.
diff --git a/docs/mkdocs/docs/features/files.md b/docs/mkdocs/docs/features/files.md
index bf3632e89..c69879a45 100644
--- a/docs/mkdocs/docs/features/files.md
+++ b/docs/mkdocs/docs/features/files.md
@@ -32,11 +32,11 @@ json read_json_file(const std::string& path)
column 1 instead of saying that the file is missing. A relative path is resolved against the working directory of the
running program, which is often not the directory of the source file (see the
[FAQ](../home/faq.md#parsing-a-file-fails-with-unexpected-end-of-input)).
-- **Don't read the file into a string first.** Parsing from a `#!cpp std::ifstream` is only about 10 to 20 % slower
- than reading the whole file into a string with one `read()` and parsing that, and it needs no memory for the copy (see
- [input sources and performance](parsing/index.md#input-sources-and-performance)). The common way to read a file with
- `#!cpp std::istreambuf_iterator` is so slow that it takes about twice as long as parsing from the stream. Read
- the file into a string only if you need the text itself, for example for an error message. This advice is for
+- **Read the file into a string only if the memory is no concern.** Parsing from a `#!cpp std::ifstream` takes about
+ 1.15 to 1.45 times as long as reading the whole file into a string with one `read()` and parsing that, but it needs no
+ memory for the copy (see [input sources and performance](parsing/index.md#input-sources-and-performance)). The common
+ way to read a file with `#!cpp std::istreambuf_iterator` is so slow that it takes about twice as long as parsing
+ from the stream. This advice is for
`#!cpp json::parse`. [`json_document`](json_view/index.md) reads a stream into a buffer that it owns anyway, so there is no
copy to save: reading the file into a `#!cpp std::string` yourself and passing that as an lvalue, which the document
borrows, was about 6 to 12 % faster than parsing from the stream, with the same single buffer.
diff --git a/docs/mkdocs/docs/features/parsing/index.md b/docs/mkdocs/docs/features/parsing/index.md
index 984de9f7e..38e523f88 100644
--- a/docs/mkdocs/docs/features/parsing/index.md
+++ b/docs/mkdocs/docs/features/parsing/index.md
@@ -80,8 +80,16 @@ them faster than others:
| `#!cpp FILE*` | with glibc and the Apple C library, the characters the `FILE` holds in its buffer are scanned in bulk like a stream's; elsewhere one character at a time; the file is locked once for the whole parse |
| other iterator ranges (e.g., `#!cpp std::list`, `#!cpp std::istreambuf_iterator`), wide strings | one character at a time |
-Parsing a file from a `#!cpp std::ifstream` is therefore only about 10 to 20 % slower than reading the whole file into a
-`#!cpp std::string` first and parsing that, and it needs no memory for a copy of the file. The stream is never read
+Input in one block of memory is read even faster with the default options (no
+[parser callback](../../api/basic_json/parser_callback_t.md), no comments, no trailing commas, and for `sax_parse()`
+`strict = true`): `parse()`, `accept()`, and `sax_parse()` then read the grammar and the tokens in one loop over the
+memory. Only after an error do `parse()` and `sax_parse()` read the input again, to report the error exactly as before.
+This faster path is not used when [`JSON_DIAGNOSTIC_POSITIONS`](../../api/macros/json_diagnostic_positions.md) is
+enabled, because the positions are recorded by the default parser: the input is then parsed as before, at the same speed
+as before.
+
+Parsing a file from a `#!cpp std::ifstream` therefore takes about 1.15 to 1.45 times as long as reading the whole file
+into a `#!cpp std::string` first and parsing that, but it needs no memory for a copy of the file. The stream is never read
ahead: after parsing, it is positioned exactly as if every character had been read one at a time, so
[`operator>>`](../../api/operator_gtgt.md) can still read several values from one stream (see also
[JSON Lines](json_lines.md)).
diff --git a/docs/mkdocs/docs/features/performance.md b/docs/mkdocs/docs/features/performance.md
index 570130055..b901e7ff3 100644
--- a/docs/mkdocs/docs/features/performance.md
+++ b/docs/mkdocs/docs/features/performance.md
@@ -29,9 +29,9 @@ up. Nothing is read ahead, so the stream is left exactly where it would be after
[`operator>>`](../api/operator_gtgt.md) can still read several values from one stream. A stream buffer without a get
area is read one character at a time. A `#!cpp FILE*` is locked once per parse instead of once per character, and with
glibc and the Apple C library, the characters in its buffer are scanned in bulk as a stream's get area is. See
-[input sources and performance](parsing/index.md#input-sources-and-performance) for the full table. According to the
-description of [#5828](https://github.com/nlohmann/json/pull/5828), parsing from a stream is within about 10 to 20 % of
-parsing an equivalent `#!cpp std::string`.
+[input sources and performance](parsing/index.md#input-sources-and-performance) for the full table. Parsing from a
+stream takes about 1.15 to 1.45 times as long as parsing an equivalent `#!cpp std::string`, which `parse()` reads with
+a parser of its own (see there).
Inputs that are still read one character at a time: iterator ranges that are not contiguous (e.g.
`#!cpp std::list` or `#!cpp std::istreambuf_iterator`), wide strings, and user-defined adapters. An
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 7f6417e38..e13fd94a8 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
@@ -5,14 +5,14 @@
+ comparison), including the line counts in "The price". The numbers below are from develop 7fc8312c64 with the
+ open pull requests #6029, #6031, #6033 and #6039 merged (local branch local/all-prs-1107 at fcae242ac8, plus
+ #6039 uncommitted), measured 11-07: suite, timeline (the commits after 11-03 and 3.13.0; the older points are from
+ 10-27 and 11-03), compile times (5 rounds), the stream gap, and the comparison on the Mac and on x86-64.
+ Appendix D and the Intel MacBook Pro column of Appendix B are from 11-03 (the MacBook was not reachable), and so
+ is the check with GCC 16 and Clang 23 on the Mac in "Results on x86-64". The Rust times are from 10-29 (Mac) and
+ 11-03 (x86-64), the x86-64 conformance run from 10-30. Once #6031 is merged, remove it from PENDING in
+ scripts/figures/perf_blog.py. Merge tests/benchmarks/json_view/others before publishing (the links 404). -->
Since 2016, the [README](https://github.com/nlohmann/json#readme) of this library has said this about speed:
@@ -20,19 +20,20 @@ Since 2016, the [README](https://github.com/nlohmann/json#readme) of this librar
> 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, 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
+the 182 benchmarks of the library's own suite run 3.3× 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.5–5.1× as fast, and writing
+them 2.9–6.6×. The new version also adds [`json_document`](../../features/json_view/index.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 example programs take 34–62% longer to compile.
-A precompiled header or a C++20 module wins back most or all of that in debug builds, and 41–91% of it in optimized
-builds ([The price](#the-price)).
+numbers, including where it loses. The price is compile time: two small example programs take 36–68% longer to compile.
+A precompiled header or a C++20 module wins back most or all of that in debug builds, and 41% to all of it in
+optimized builds ([The price](#the-price)).
This post is about *how*. Hardly any of the techniques are new: we borrowed them from
[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
+[SQLite](https://sqlite.org), [Boost.JSON](https://github.com/boostorg/json), [gnulib](https://www.gnu.org/software/gnulib/),
+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
@@ -60,8 +61,8 @@ A](#a-test-files) says what makes each of them hard.
Speedup of 3.13.0 over 3.12.0 for a selection of workloads: the old time divided by the
-new one, Google Benchmark suite on an Apple M1 Max, median of 8 runs. More is better; the vertical line at 1× is 3.12.0.
-The times are in [Appendix B](#b-3120-against-3130).
+new one, Google Benchmark suite on an Apple M1 Max, median of 12 runs. More is better; the vertical line at 1× is
+3.12.0. The times are in [Appendix B](#b-3120-against-3130).
The rows above are chosen to show the range. All 182 benchmarks of the suite:
@@ -69,12 +70,13 @@ The rows above are chosen to show the range. All 182 benchmarks of the suite:
--8<-- "images/perf_blog_distribution.svg"
-
How many of the suite's 182 benchmarks got how much faster. The one below 1× is copying
-`citm_catalog.json` without destroying the copy. Every row is in [Appendix B](#b-3120-against-3130).
+
How many of the suite's 182 benchmarks got how much faster. The two below 1× copy
+`citm_catalog.json` and `twitter.json` without destroying the copy (0.95× and 0.99×). Every row is in [Appendix
+B](#b-3120-against-3130).
The median benchmark got 2.9× as fast. On an Intel MacBook Pro (Skylake), the median is 2.9× as well, but parsing
-floating-point numbers gained more (`canada.json` 6.1× instead of 3.4×) and writing integers less (`dump()` 1.4–2.3×
-instead of 2.2–3.8×); [Appendix B](#b-3120-against-3130) has both columns. Copying and comparing values gained the
+floating-point numbers gained more (`canada.json` 6.1× instead of 4.1×) and writing integers less (`dump()` 1.4–2.3×
+instead of 2.2–3.7×); [Appendix B](#b-3120-against-3130) has both columns. Copying and comparing values gained the
least, and the largest gains come from fixing algorithms rather than tuning them (theme
7).
@@ -104,7 +106,7 @@ Eight themes did most of the work:
## Where the time went: a timeline
The suite compares two points. To see which change did what, we compiled one small benchmark against the header of each
-of the 351 commits on `develop` that changed `json.hpp` or `json_view.hpp` from 3.12.0 to November 3, and of 3.13.0, and
+of the 358 commits on `develop` that changed `json.hpp` or `json_view.hpp` from 3.12.0 to November 7, and of 3.13.0, and
ran all of them interleaved. Four of the eight workloads we followed:
- `parse()` of `citm_catalog.json`: the lexer's structure and whitespace handling, and building many small objects;
@@ -131,8 +133,8 @@ larger steps mean something.
-
Speedup over 3.12.0 after each of the 351 commits that changed the headers up to November
-3, from 3.12.0 (left) to 3.13.0 (right). More is better; each point is the median of 4 runs of one build, taken in
+
Speedup over 3.12.0 after each of the 358 commits that changed the headers up to November
+7, from 3.12.0 (left) to 3.13.0 (right). More is better; each point is the median of 4 runs of one build, taken in
random order (with 4 runs, the mean of the two middle ones). The badges are theme numbers; "U" is a UTF-8 check that
came and went. The other four workloads, and every labeled step with its pull request and size, are in [Appendix
C](#c-the-timeline).
@@ -202,14 +204,16 @@ iterators over a `std::list`, or a user's own adapter. But it makes every
```mermaid
flowchart LR
src[/"input"/] --> q{{"what is it?"}}
- q -->|"std::string, std::vector, pointers, contiguous ranges"| c["contiguous: scan the whole buffer"]
+ q -->|"std::string, std::vector, their iterators, pointers, contiguous ranges"| c["contiguous: scan the whole buffer"]
q -->|"std::istream"| w["window: scan the streambuf's get area"]
- q -->|"FILE*"| f["one lock per parse, unlocked reads"]
+ q -->|"FILE*"| f["window: scan the FILE's buffer"]
q -->|"other iterators, user adapters"| b["byte at a time"]
```
The four kinds of input the lexer distinguishes in 3.13.0. Everything on the fast paths
-falls back to the byte path on anything unusual, so the result and the error messages stay the same.
+falls back to the byte path on anything unusual, so the result and the error messages stay the same. The `FILE*`
+window needs glibc or Apple's C library; elsewhere, a `FILE*` is read one character at a time with one lock per
+parse.
### Detecting the input in C++11
@@ -236,7 +240,8 @@ which also keeps MSVC from warning about constant conditions. `get()` still look
token bookkeeping are now calls. For an adapter that can seek, their `std::true_type` overloads are empty and cost
nothing, because the position and the text of a token are derived only when an error message needs them (theme
4). For a `std::string`, what remains is the branch on
-`next_unget` and the adapter's bounds check and pointer increment.
+`next_unget` and the adapter's bounds check and pointer increment. For the most common calls, not even that remains:
+they don't use the lexer at all ([One loop instead of two](#one-loop-instead-of-two)).
### Contiguous input
@@ -249,6 +254,43 @@ are scanned in bulk (themes
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.
+A pair of iterators is harder. Before C++20, `s.begin()` and `s.end()` of a `std::string` can't be told from the
+iterators of any other random-access container, so `json::parse(s.begin(), s.end())` was read one character at a time,
+while `json::parse(s)` got all the bulk scans. The library now matches the iterator types of `std::string` and of a
+`std::vector` of one-byte integers exactly, and turns the pair into the pointer range it covers. That made
+`parse(s.begin(), s.end())` 1.7–2.0× as fast on the M1, and `accept()` 2.7–3.1×.
+
+### One loop instead of two
+
+Even on a pointer range, the parser and the lexer still talked through an interface: the parser asks for a token, the
+lexer finds it and hands it over, and the parser dispatches on its type. The lexer's state, the current character, a
+pending `unget()`, and where the token starts, lives in members, so it goes through memory on every token. After all of
+the above, `json::accept()` of `twitter.json` still took 2.8× as long as `json_document::accept()` (theme
+8), which reads the same bytes.
+
+[yyjson](https://github.com/ibireme/yyjson)'s reader shows the alternative: one function that holds the input in a
+local pointer and in which the grammar, the whitespace and the tokens are one loop. After a `:`, it tests for the
+characters that can start a value right there, instead of returning a token type and dispatching on it. 3.13.0 adds
+such a parser for input in one block of memory, `contiguous_parser`. Its stack of open arrays and objects is a 64-bit
+word with one bit per level, so the kind of the enclosing container is a shift away and stays in a register; every 64
+levels, the word is pushed to a vector. It doesn't build values itself: it drives the library's own SAX handlers, so
+`parse()` builds the same `json` with the same code as before. It is used by `parse()` without a callback, by
+`accept()`, and by `sax_parse()` with the user's own handler, each with the default options; everything else keeps the
+old parser.
+
+The hard part was the errors. A second parser would have to produce the same exception, message and position for
+every malformed input, a large surface for small differences. So it doesn't report errors at all. It stops at the
+token the old parser would reject, before it reports that token to the handler, and the old parser runs again on the
+same input to report the error. `parse()` gives it a handler that only reports, and `sax_parse()` one that forwards
+only `parse_error()` to the user's handler, so that no event arrives twice. An error at the end of a large input costs
+`parse()` about 1.2× as much as before; valid input costs nothing extra.
+
+On the M1, `accept()` got 1.6–2.0× as fast, `sax_parse()` 1.4–1.5×, and `parse()` 10–16% faster, about the same with
+GCC and Clang on x86-64. In `accept()`, which needs no values, the parser also skips the digits of a number without
+computing its value, except for the rare float whose exponent may reach the largest `double`: another 16% on
+`canada.json`. The cost is about 1,000 lines of header, and a translation unit that parses a string and dumps it
+compiles 3% slower at `-O0` and 9% at `-O2`.
+
### Streams
An `std::istream` already has a buffer: the get area of its `streambuf`, `gptr()` to `egptr()`. The lexer now scans that
@@ -276,7 +318,10 @@ Before the stream refills its get area, the adapter appends the part of the curr
buffer; only an error message ever reads the token text back. Nothing is read ahead: the adapter advances `gptr()` by
exactly what the lexer consumes, so the stream is left where it was before. On an M1 Max, parsing from an
`std::ifstream` took 1.8–2.5× as long as reading the file into a string and parsing that, in the pull request's
-measurement before the change; now it takes just 9–16% longer.
+measurement before the change, and 9–16% longer after it. Since then, parsing a string got faster still, because the
+fused parser of [One loop instead of two](#one-loop-instead-of-two) needs input in one block of memory, so a stream now
+takes 25–38% longer than the string again (the file read with one `read()`, parsing `twitter.json`,
+`citm_catalog.json`, and `canada.json`).
### `FILE*`
@@ -284,11 +329,26 @@ measurement before the change; now it takes just 9–16% longer.
`_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. 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
+already cheap.
+
+That still left a call per character, while a stream had its get area scanned in bulk. But a `FILE` has a buffer too,
+and `getc_unlocked()` is a macro that reads it: glibc's compares `_IO_read_ptr` with `_IO_read_end`, Apple's counts
+down `_r` and advances `_p`. The adapter now reads those same fields and offers the lexer the rest of the buffer as a
+window, as for a stream. It consumes exactly the characters `getc_unlocked()` would have returned, so the file is
+left where it was before, and when the C library is about to refill the buffer, the part of the current token in it is
+kept for error messages. The idea is gnulib's
+[`freadptr()`](https://github.com/coreutils/gnulib/blob/master/lib/freadptr.c), which knows these fields for many C
+libraries. Parsing from a `FILE*` got another 2.0–2.6× faster on the M1 and 1.7–2.2× with glibc on x86-64, and
+`accept()` 2.9–4.4×; it is now about as fast as parsing from an `std::ifstream`. uClibc
+defines `__GLIBC__` but has other fields, so it is excluded, and other C libraries keep reading one character at a
+time.
+
+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).
+[`JSON_DISABLE_UNLOCKED_FILE_IO`](../../api/macros/json_disable_unlocked_file_io.md); the second turns the `FILE`
+window off as well, as it needs the lock for the whole parse.
## 2 Work on words, not bytes { #theme-2 }
@@ -325,6 +385,20 @@ Hacks](https://graphics.stanford.edu/~seander/bithacks.html#ZeroInWord). The sub
it can flag a byte *after* a real hit, but never one before it: the lowest flag is always right, and the number of
trailing zero bits divided by eight is the index of the first special byte.
+The code above is the readable form, and it does more work than it needs to. The `& ~x` of the zero-byte test only
+keeps bytes from 0x80 out of the result, and `has_non_ascii` flags exactly those bytes anyway. So all three `& ~x`
+terms can go, and the whole test becomes one expression:
+
+```cpp
+return ((q - ones) | (b - ones) | (v - 0x2020202020202020ull) | v) & high;
+```
+
+
The same test without the redundant `& ~x` terms.
+
+Clang finds this reduction on its own; GCC doesn't, and emitted 24 instructions per word instead of 16. With GCC on
+x86-64, writing it out made validating `twitter.json` and `citm_catalog.json` 5–7% faster and `dump()` 2–3%. The
+masks of both forms are the same for every pair of adjacent byte values and for 1.3 billion random words.
+
--8<-- "images/perf_blog_swar.svg"
@@ -359,10 +433,16 @@ The same kind of word test now runs in several places:
### 16 bytes with SSE2 and NEON
Where a vector unit is part of the baseline instruction set (SSE2 on x86-64, NEON on 64-bit Arm), the string paths take
-16 bytes per step. Two well-known details are worth repeating. A signed comparison `byte < 0x20` catches control
-characters *and* every byte from 0x80, because those are negative as `int8_t`, so four classes of bytes take three
-comparisons. And NEON has no equivalent of x86's `pmovmskb`, which turns a vector of comparison results into a bit mask.
-The usual replacement, by [Danila
+16 bytes per step. Three details are worth repeating. A signed comparison `byte < 0x20` catches control characters
+*and* every byte from 0x80, because those are negative as `int8_t`. Flipping bit 1 first folds the quote into that
+comparison too: `c ^ 2` maps 0x00–0x1F onto themselves and the quote (0x22) onto 0x20, keeps the bytes from 0x80
+negative, and maps every other ASCII byte to 33 or more. So `(int8_t)(c ^ 2) < 33` finds three of the four classes of
+bytes, and only the backslash needs a comparison of its own (Daniel Lemire, ["Detect control characters, quotes and
+backslashes efficiently using
+SWAR"](https://lemire.me/blog/2025/04/13/detect-control-characters-quotes-and-backslashes-efficiently-using-swar/),
+2025; ruby/json uses the same trick in its SIMD scan). Neither GCC nor Clang finds this one on its own. And NEON has no
+equivalent of x86's `pmovmskb`, which turns a vector of comparison results into a bit mask. The usual replacement, by
+[Danila
Kutenin](https://developer.arm.com/community/arm-community-blogs/b/servers-and-cloud-computing-blog/posts/porting-x86-vector-bitmask-optimizations-to-arm-neon),
shifts each 16-bit lane right by four and narrows it to eight bits, which leaves four bits per input byte in a 64-bit
integer:
@@ -371,10 +451,11 @@ integer:
while (e - p >= 16)
{
const uint8x16_t in = vld1q_u8(p);
- const uint8x16_t quote_or_backslash = vorrq_u8(vceqq_u8(in, vdupq_n_u8('"')),
- vceqq_u8(in, vdupq_n_u8('\\')));
- const uint8x16_t special = vorrq_u8(quote_or_backslash,
- vcltq_s8(vreinterpretq_s8_u8(in), vdupq_n_s8(0x20)));
+ // the backslash, and (c ^ 2) < 33 as int8_t for the quote, the control
+ // characters and the bytes from 0x80
+ const uint8x16_t special = vorrq_u8(vceqq_u8(in, vdupq_n_u8('\\')),
+ vcltq_s8(vreinterpretq_s8_u8(veorq_u8(in, vdupq_n_u8(2))),
+ vdupq_n_s8(33)));
// each byte of special is 0x00 or 0xFF; shifting each 16-bit lane right by 4 and
// keeping its low byte leaves 4 bits per input byte
const uint8x8_t nibbles = vshrn_n_u16(vreinterpretq_u16_u8(special), 4);
@@ -387,7 +468,7 @@ while (e - p >= 16)
}
```
-
The NEON string scan: three comparisons for four classes of bytes, and a narrowing
+
The NEON string scan: two comparisons for four classes of bytes, and a narrowing
shift in place of `pmovmskb`.
On `twitter.json`, the vector scan made `json::parse()` 5% faster with Clang and 9% with GCC on an M1 Max; on long
@@ -397,7 +478,7 @@ runs one word first. Inlining the vector code into the lexer cost 5% of validati
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.
-[`json_document`](../../features/json_view.md), the new document type of `json_view.hpp`, also skips the digits after a
+[`json_document`](../../features/json_view/index.md), the new document type of `json_view.hpp`, also skips the digits after a
decimal point 16 bytes at a time with SSE2: a compare and a bit mask instead of a branch per digit, about 33% of the
instructions for a 12-digit fraction. Parsing `canada.json` got 22% faster on x86-64, a file of 10,000 floats 28–32%.
@@ -738,7 +819,12 @@ arrays `[1,2]` at most 1.5%.
The parser checks for the expected `:` or `,` before it falls into the lexer's general token switch (−7% on
`citm_catalog.json`, a contribution by [@mvanslobbe](https://github.com/mvanslobbe)), and
[`to_cbor()`](../../api/basic_json/to_cbor.md) no longer creates a temporary `basic_json` for every object key it writes
-(−55% on `twitter.json`, by [@alexprabhat99](https://github.com/alexprabhat99)).
+(−55% on `twitter.json`, by [@alexprabhat99](https://github.com/alexprabhat99)). And the binary readers wrote every
+byte of a string twice: they grew the string with `resize()`, which fills it with zeros, and then copied the bytes over
+the zeros. The zero fill alone was 5% of the instructions of `from_cbor()`. A string that the input holds in one block
+is now appended from it directly, after its claimed length has been checked against the input, so a huge claimed
+length still allocates nothing. Reading `twitter.json` and `citm_catalog.json` from CBOR and MessagePack got 5–9%
+faster on the M1, 3–9% on x86-64.
!!! note "The same idea, measured twice"
@@ -1006,6 +1092,15 @@ oa.commit(out - start);
Encoding a run of integers through a cursor, with one capacity check for the whole
run.
+The cursor needs the vector to be as long as the space it writes into, so the sink grew it with `resize()` to its whole
+new capacity. That zeroes the new bytes, so every doubling zeroed megabytes of fresh memory at once: it faulted in all
+the pages and pushed them out of the cache before the writer got there. The writers had gained much less on x86-64 than
+on the M1 (writing an array of numbers as CBOR got 2.4× as fast on a Skylake laptop and 4.8× on the M1 Max), and this
+was part of the reason: page faults, zeroing and copies on reallocation of a 9–80 MB output were 30–76% of the 3.13.0
+time on x86-64. The sink still reserves the same capacity, but now resizes the vector only to 64 KiB ahead of the
+cursor, a window that is still in the cache when the writer's stores arrive. On x86-64, writing large arrays of numbers
+got 9–26% faster; on the M1, 1–2.5%.
+
### Integers without branches
The last step also took the branches out of encoding an integer. After the bounds checks, mispredicted branches
@@ -1132,7 +1227,7 @@ million small objects 1.39×.
After all of the above, [`json::parse()`](../../api/basic_json/parse.md) is still bound by what it builds: one heap
allocation per string, array and object, and a `std::map` node per member, all of which have to be freed again. Many
programs only parse a document, read some fields and throw it away, or change a few values and write it back. For them,
-3.13.0 adds [`json_document`](../../features/json_view.md) and `json_view` in a new header, ``.
+3.13.0 adds [`json_document`](../../features/json_view/index.md) and `json_view` in a new header, ``.
The two figures below show what each parser makes of this 54-byte text:
```json
@@ -1215,7 +1310,7 @@ static_assert(sizeof(node) == 16, "node must stay 16 bytes");
### It can be edited, too
A read-only index is the easy case, and a fair objection is that `json_document` is only fast because it can't change.
-[`json_editable_document`](../../features/json_view.md#editing-a-document) has `set()`, `insert()`, `erase()` and
+[`json_editable_document`](../../features/json_view/editing.md) has `set()`, `insert()`, `erase()` and
`push_back()`. An edit never writes into the source text and never moves the parsed index:
- a new scalar is written over its own node, with new string and number text in an append-only arena;
@@ -1291,9 +1386,8 @@ operating system. It is not neutral in every detail, though:
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.
+ yyjson then comes 3–5% closer, and on `twitter.json` it validates and selects 1–2% faster than `json_document`. On
+ x86-64, it ties `json_document` in *sum* with Clang and takes 1.04× and 1.15× 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: 3–5% on the Mac.
- Reading from files and streams is not part of the comparison.
@@ -1344,12 +1438,13 @@ lists only x86-64 Linux; we show it on the Mac anyway.
### Results on the Mac
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
+files in all six use cases, and yyjson is the next library in every column; in *validate*, `json::accept()` comes in
+between (1.38× against yyjson's 1.41×). 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 3–7% in *validate*, *sum*, and
+- **The lead is not the same on every file.** On `twitter.json`, yyjson is within 4–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
@@ -1357,44 +1452,45 @@ Some caveats belong next to that:
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 (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.
+ into structs select in 0.76× and 0.81× `json_document`'s time (Glaze is faster only on `citm_catalog.json` and
+ `canada.json`), and in 0.55–0.61× 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 in *sum* (1.11 → 1.04) but not in *validate* (1.28 → 1.27), and
+ `citm_catalog.json`, yyjson comes closer in *sum* (1.11 → 1.01) and a little in *validate* (1.27 → 1.24), 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.0× as fast and minifying it 1.9× as fast; the
+ 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 *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, simdjson and
- yyjson sum it slightly faster.
+- **Small documents.** On a 2.5 KB message, the fastest libraries take about a microsecond on the Mac and 1.1–2 on
+ x86-64. yyjson validates and sums it as fast as `json_document` on the Mac, and with GCC on x86-64, simdjson and
+ yyjson sum it as fast.
### Reading into structs
A view can be converted into your own types with the same [`from_json()`](../../features/arbitrary_types.md) as `json`,
including the ones the [`NLOHMANN_DEFINE_TYPE_*`](../../api/macros/nlohmann_define_type_non_intrusive.md) macros
generate. That is as fast as picking the fields by hand from the view; only for the many small records of
-`jeopardy.json` does it take 23% longer. On the Mac, Glaze, serde_json, and Boost.JSON's `parse_into`, reading into the
-same structs and checking everything, take 1.7×, 2.2×, and 3.1× as long (geometric mean over the four files). On a
+`jeopardy.json` does it take 22% longer. On the Mac, Glaze, serde_json, and Boost.JSON's `parse_into`, reading into the
+same structs and checking everything, take 1.7×, 2.3×, and 3.1× as long (geometric mean over the four files). On a
`json` value, reading into the structs takes about as long as `json::parse()` alone.
### nlohmann/json without `json_document`
And nlohmann/json without `json_document`? `json` builds a tree of separate allocations, and it shows: for *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.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.
+needed, the fused parser ([One loop instead of two](#one-loop-instead-of-two)) puts it among the fastest:
+[`json::accept()`](../../api/basic_json/accept.md) validates the four files in 1.38× `json_document`'s time on the Mac,
+ahead of every other library, and a SAX handler sums them in 1.34×, behind only yyjson and simdjson. Boost.JSON takes
+1.8× and 1.5× as long as `json` there, and 1.2–1.4× and 1.1–1.3× on x86-64. `dump()` writes faster than Boost.JSON,
+RapidJSON and serde_json, and about as fast as Glaze.
Much of Boost.JSON's lead in building a tree comes from its allocator: here it builds the tree in a
-`monotonic_resource`, an arena that is freed in one go, which saves it 28–41%. With its default allocator, which
+`monotonic_resource`, an arena that is freed in one go, which saves it 27–40%. With its default allocator, which
allocates every string, array, and object on its own as `json` does, `boost::json::parse()` with precise numbers takes
-as long as `json::parse()` on `twitter.json`, 5% longer on `citm_catalog.json`, and 17% less time on `canada.json` (on
-the Mac, freeing the tree included).
+13–17% longer than `json::parse()` on `twitter.json`, 19–20% longer on `citm_catalog.json`, and 2% less time on
+`canada.json` (on the Mac, freeing the tree included).
### Results on x86-64
@@ -1412,30 +1508,34 @@ compilers disagree only about *sum*.
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` 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:
+`json_document` has the lowest mean in five of the six columns with every compiler. The exception is *sum*, where
+simdjson is ahead with GCC, and `json` with a SAX handler with GCC 13 and Clang 23 (with Clang by 5%, all of it on
+`jeopardy.json`; below). This machine is noisy, though: between two runs, a library's time relative to
+`json_document`'s moved by up to 7–8% (95th percentile; 2.4% on the Mac), so a lead of 5% is no result. In *validate*,
+the next library takes 1.8–2.0× as long, or 1.6–1.7× if every library may reuse its parser, because validating no
+longer builds an index (theme 8). The only way in between is
+`json::accept()` at 1.3–1.7×, which builds nothing either ([One loop instead of two](#one-loop-instead-of-two)).
+Behind those means, `json_document` 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.99× `json_document`'s time with GCC 16 (0.86–0.91× with GCC 13). With
+ `citm_catalog.json`, and `canada.json` in 0.76–0.97× `json_document`'s time with GCC 16 (0.79–0.92× 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.
+ parse, is faster than converting them on access, and on `citm_catalog.json`, where `json`'s SAX handler and
+ Boost.JSON's are 1–3% faster.
- **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.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–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` 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.
+ 0.64–0.71× the time, and `json`'s SAX handler in 0.70–0.75×. Reusing the document cuts `json_document`'s time by
+ 41–43%, to 51 ms, less than sonic-rs's 60 ms. Validating needs no index: `json_document::accept()` takes 33–35 ms,
+ `json::accept()` 39–40 ms, and no other library validates `jeopardy.json` faster, even reused (the next is simdjson's
+ reused parser at 48–52 ms). On the Mac, a fresh document costs hardly more than a reused one.
+- **Writing floats.** yyjson writes `canada.json` in 0.84–0.93× the time (and Glaze with GCC 13 and `json`'s `dump()`
+ with GCC 16, which start from `double`s, in 0.98× and 0.99×), 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` 19% faster with GCC 16 and 7–13% faster with GCC 13 and Clang
+ 23, and from `twitter.json` 2–4% faster with GCC. Boost.JSON, Glaze, sonic-rs, sonic-cpp, and yyjson are ahead in a
+ few more cells, by 1–10%: summing `citm_catalog.json` (Boost.JSON with GCC), and selecting from or writing
+ `jeopardy.json`. Most of that is within the noise.
The full tables, with each library's variants, are in [Appendix E](#e-comparison-with-other-libraries).
@@ -1444,7 +1544,8 @@ 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.86–0.87× its time. File by file, for *validate*:
+and on the Mac; with GCC, simdjson sums in 0.87–0.88× its time. `json::accept()` validates them in 1.4–1.6× the time
+of `json_document` on x86-64, close to simdjson (1.44–1.46×), the fastest other library. File by file, for *validate*:
--8<-- "images/perf_blog_heldout.svg"
@@ -1454,8 +1555,10 @@ and on the Mac; with GCC, simdjson sums in 0.86–0.87× its time. File by file,
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).
-`json_document` validates all 19 files fastest with GCC 16 and Clang 23, and 18 with GCC 13 and on the Mac: simdjson
-validates `apache_builds` 8% faster on the Mac and `google_maps_api_response` 2% faster with GCC 13. Before validating
+`json_document` validates 18 of the 19 files fastest with GCC 16, Clang 23, and on the Mac: simdjson ties on
+`google_maps_api_response` with GCC 16, Boost.JSON on `mesh.pretty` with Clang 23, and simdjson validates
+`apache_builds` 7% faster on the Mac. With GCC 13 it is 17: simdjson validates `google_maps_api_response` 6% faster,
+and Boost.JSON `mesh.pretty` 10% faster (1% in the first of our two runs on November 7). Before validating
without an index (theme 8) and skipping fraction digits 16 bytes
at a time (theme 2), Boost.JSON validated the number-heavy files
up to 1.5× as fast on x86-64.
@@ -1490,8 +1593,8 @@ the pull requests that tried them.
## The price
-None of this is free at compile time. `json.hpp` grew from 25,526 to 46,630 lines (`json_view.hpp` is a separate header
-of 11,898 lines that you only pay for if you include it). To see what that means for a program, we compiled two small
+None of this is free at compile time. `json.hpp` grew from 25,526 to 48,025 lines (`json_view.hpp` is a separate header
+of 11,909 lines that you only pay for if you include it). To see what that means for a program, we compiled two small
ones with both versions:
- `count_numbers` reads `twitter.json` and adds up all of its numbers, like the *sum* use case in [How it
@@ -1511,16 +1614,16 @@ better: Apple clang 21 and GCC 16.2, CPU time of the compiler, median of 5 inter
one-off cost of building the precompiled header or the module is not included. Hover over a bar for the time in
milliseconds; [Appendix G](#g-how-we-measured) has all times, the programs, and the commands.
-3.13.0 takes 34–62% longer to compile these programs. A good part of that is reading the header, and a precompiled
+3.13.0 takes 36–68% longer to compile these programs. A good part of that is reading the header, and a precompiled
header or a module does that once instead of in every translation unit:
- **In debug builds,** which are what you wait for while you edit, a precompiled 3.13.0 compiles faster than 3.12.0 with
- Apple clang, and within 8–15% of it with GCC.
+ Apple clang, and within 12–15% of it with GCC.
- **In optimized builds,** less of the increase goes away, because the optimizer still works through all the code a
- program instantiates. A precompiled header or a module wins back 41–91% of it.
-- **The one-off cost** is 1.0 s (Apple clang) or 2.6 s (GCC) to build the precompiled header, and 1.6 s or 2.5 s to
+ program instantiates. A precompiled header or a module wins back 41% to all of it.
+- **The one-off cost** is 0.9 s (Apple clang) or 2.3 s (GCC) to build the precompiled header, and 1.4 s or 2.3 s to
build the module, once per set of compiler flags. A precompiled header pays for itself after two to four translation
- units, a module after three to five.
+ units, a module after three to six.
- **Modules have caveats.** Apple clang needs `-fcxx-modules`, [module support](../../features/modules.md) still depends
heavily on the compiler version, and macros don't cross a module boundary: with `import nlohmann.json;`, the
`NLOHMANN_DEFINE_TYPE_*` macros aren't available, which is why `server_config` writes `to_json()` and `from_json()` by
@@ -1615,27 +1718,27 @@ The rows of the first chart:
| workload | 3.12.0 | 3.13.0 | speedup | Intel |
| --- | --: | --: | --: | --: |
-| `json::parse()` of `twitter.json` | 3.30 ms | 1.05 ms | 3.1× | 2.6× |
-| `json::parse()` of `citm_catalog.json` | 7.56 ms | 1.78 ms | 4.3× | 3.5× |
-| `json::parse()` of `canada.json` | 15.5 ms | 4.63 ms | 3.4× | 6.1× |
-| `json::parse()` of `canada.json`, indented by 4 spaces | 35.0 ms | 5.37 ms | 6.5× | 8.0× |
-| `ordered_json::parse()` of `citm_catalog.json` | 11.5 ms | 1.68 ms | 6.9× | 6.6× |
-| `json::parse()` of `twitter.json` from an `std::ifstream` | 3.43 ms | 1.37 ms | 2.5× | 2.3× |
-| `json::parse()` of `citm_catalog.json` from a `FILE*` | 36.0 ms | 7.58 ms | 4.7× | 5.7× |
-| `json::accept()` of `citm_catalog.json` | 6.53 ms | 0.923 ms | 7.1× | 8.4× |
-| `dump()` of `twitter.json` | 1.44 ms | 0.214 ms | 6.7× | 5.7× |
-| `dump()` of `canada.json` | 6.95 ms | 2.34 ms | 3.0× | 3.3× |
-| `dump(4)` of `citm_catalog.json` | 1.99 ms | 0.423 ms | 4.7× | 4.1× |
-| `to_cbor()` of `twitter.json` | 0.659 ms | 0.143 ms | 4.6× | 6.7× |
-| `from_cbor()` of `twitter.json` from memory | 3.23 ms | 0.953 ms | 3.4× | 2.8× |
-| `from_cbor()` of `twitter.json` from a `FILE*` | 10.4 ms | 1.57 ms | 6.6× | 6.5× |
-| `diff()` of `canada.json` with every eighth scalar changed | 39.8 ms | 3.80 ms | 10.5× | 11.2× |
-| iterating over every value of `canada.json` | 0.897 ms | 0.328 ms | 2.7× | 2.0× |
-| copying `citm_catalog.json` and destroying the copy | 1.70 ms | 1.51 ms | 1.13× | 1.12× |
-| `==` of `citm_catalog.json` and a copy | 0.280 ms | 0.263 ms | 1.06× | 0.95× |
+| `json::parse()` of `twitter.json` | 3.21 ms | 0.921 ms | 3.5× | 2.6× |
+| `json::parse()` of `citm_catalog.json` | 7.40 ms | 1.45 ms | 5.1× | 3.5× |
+| `json::parse()` of `canada.json` | 15.2 ms | 3.73 ms | 4.1× | 6.1× |
+| `json::parse()` of `canada.json`, indented by 4 spaces | 34.2 ms | 4.61 ms | 7.4× | 8.0× |
+| `ordered_json::parse()` of `citm_catalog.json` | 11.0 ms | 1.34 ms | 8.2× | 6.6× |
+| `json::parse()` of `twitter.json` from an `std::ifstream` | 3.40 ms | 1.38 ms | 2.5× | 2.3× |
+| `json::parse()` of `citm_catalog.json` from a `FILE*` | 35.0 ms | 2.34 ms | 14.9× | 5.7× |
+| `json::accept()` of `citm_catalog.json` | 6.48 ms | 0.474 ms | 13.7× | 8.4× |
+| `dump()` of `twitter.json` | 1.41 ms | 0.214 ms | 6.6× | 5.7× |
+| `dump()` of `canada.json` | 6.71 ms | 2.31 ms | 2.9× | 3.3× |
+| `dump(4)` of `citm_catalog.json` | 1.95 ms | 0.422 ms | 4.6× | 4.1× |
+| `to_cbor()` of `twitter.json` | 0.637 ms | 0.144 ms | 4.4× | 6.7× |
+| `from_cbor()` of `twitter.json` from memory | 3.09 ms | 0.879 ms | 3.5× | 2.8× |
+| `from_cbor()` of `twitter.json` from a `FILE*` | 9.92 ms | 1.07 ms | 9.2× | 6.5× |
+| `diff()` of `canada.json` with every eighth scalar changed | 39.2 ms | 3.75 ms | 10.4× | 11.2× |
+| iterating over every value of `canada.json` | 0.874 ms | 0.445 ms | 2.0× | 2.0× |
+| copying `citm_catalog.json` and destroying the copy | 1.65 ms | 1.49 ms | 1.11× | 1.12× |
+| `==` of `citm_catalog.json` and a copy | 0.264 ms | 0.261 ms | 1.01× | 0.95× |
Time per call with 3.12.0 and 3.13.0, Google Benchmark suite on an Apple M1 Max,
-median of 8 runs; the speedup is the old time divided by the new one. The Intel column is the speedup on the Intel
+median of 12 runs; the speedup is the old time divided by the new one. The Intel column is the speedup on the Intel
MacBook Pro of [Appendix G](#g-how-we-measured). The parse rows do not include destroying the result (the suite pauses
the timer for that).
@@ -1643,188 +1746,188 @@ the timer for that).
| benchmark | 3.12.0 | 3.13.0 | speedup | Intel |
| --- | --: | --: | --: | --: |
- | `Accept/canada` | 12.9 ms | 2.35 ms | 5.51× | 13× |
- | `Accept/citm_catalog` | 6.53 ms | 923 µs | 7.07× | 8.45× |
- | `Accept/floats` | 116 ms | 22.7 ms | 5.10× | 12.2× |
- | `Accept/jeopardy` | 221 ms | 59.3 ms | 3.72× | 6.64× |
- | `Accept/signed_ints` | 117 ms | 28.3 ms | 4.15× | 4.99× |
- | `Accept/small_signed_ints` | 57.5 ms | 18 ms | 3.19× | 3.90× |
- | `Accept/twitter` | 2.51 ms | 484 µs | 5.20× | 6.89× |
- | `Accept/unsigned_ints` | 116 ms | 29.7 ms | 3.91× | 4.95× |
- | `BinaryToCbor/1024` | 298 ns | 107 ns | 2.79× | 3.67× |
- | `BinaryToCbor/128` | 249 ns | 117 ns | 2.13× | 2.27× |
- | `BinaryToCbor/16384` | 952 ns | 414 ns | 2.30× | 3.46× |
- | `BinaryToCbor/16` | 245 ns | 49.6 ns | 4.93× | 6.33× |
- | `BinaryToCbor/2048` | 328 ns | 125 ns | 2.62× | 3.74× |
- | `BinaryToCbor/256` | 265 ns | 90.9 ns | 2.91× | 3.33× |
- | `BinaryToCbor/32768` | 1.54 µs | 803 ns | 1.92× | 2.11× |
- | `BinaryToCbor/32` | 252 ns | 50.4 ns | 4.99× | 6.24× |
- | `BinaryToCbor/4096` | 407 ns | 177 ns | 2.29× | 3.48× |
- | `BinaryToCbor/512` | 290 ns | 102 ns | 2.84× | 3.02× |
- | `BinaryToCbor/64` | 255 ns | 84.6 ns | 3.01× | 3.78× |
- | `BinaryToCbor/8192` | 514 ns | 256 ns | 2.01× | 3.30× |
- | `BinaryToCbor/8` | 248 ns | 49.4 ns | 5.01× | 6.18× |
- | `Copy/canada` | 3.78 ms | 2.93 ms | 1.29× | 1.17× |
- | `Copy/citm_catalog` | 1.7 ms | 1.51 ms | 1.13× | 1.12× |
- | `Copy/twitter` | 840 µs | 767 µs | 1.09× | 1.14× |
- | `CopyAssign/canada` | 3.86 ms | 2.94 ms | 1.31× | 1.19× |
- | `CopyAssign/citm_catalog` | 1.77 ms | 1.59 ms | 1.11× | 1.12× |
- | `CopyAssign/twitter` | 885 µs | 791 µs | 1.12× | 1.07× |
- | `CopyOnly/canada` | 1.6 ms | 1.58 ms | 1.02× | 1.00× |
- | `CopyOnly/citm_catalog` | 786 µs | 817 µs | 0.96× | 0.93× |
- | `CopyOnly/twitter` | 424 µs | 421 µs | 1.01× | 1.02× |
- | `CopyRing/canada` | 3.86 ms | 3 ms | 1.29× | 1.20× |
- | `CopyRing/citm_catalog` | 1.84 ms | 1.61 ms | 1.15× | 1.20× |
- | `CopyRing/twitter` | 952 µs | 818 µs | 1.16× | 1.15× |
- | `DestroyOnly/canada` | 2.22 ms | 1.4 ms | 1.58× | 1.32× |
- | `DestroyOnly/citm_catalog` | 926 µs | 711 µs | 1.30× | 1.20× |
- | `DestroyOnly/twitter` | 416 µs | 336 µs | 1.24× | 1.15× |
- | `Diff/canada` | 39.8 ms | 3.8 ms | 10.5× | 11.2× |
- | `Diff/citm_catalog` | 7.19 ms | 1.12 ms | 6.40× | 7.78× |
- | `Diff/twitter` | 5.11 ms | 661 µs | 7.73× | 8.79× |
- | `Dump/canada / -` | 6.95 ms | 2.34 ms | 2.98× | 3.34× |
- | `Dump/canada / 4` | 9.1 ms | 3.1 ms | 2.93× | 2.82× |
- | `Dump/citm_catalog / -` | 1.42 ms | 296 µs | 4.81× | 3.93× |
- | `Dump/citm_catalog / 4` | 1.99 ms | 423 µs | 4.70× | 4.09× |
- | `Dump/floats / -` | 64.3 ms | 23.8 ms | 2.71× | 2.50× |
- | `Dump/floats / 4` | 72.6 ms | 24.6 ms | 2.95× | 2.71× |
- | `Dump/jeopardy / -` | 207 ms | 31.7 ms | 6.51× | 3.75× |
- | `Dump/jeopardy / 4` | 228 ms | 35.4 ms | 6.45× | 2.61× |
- | `Dump/signed_ints / -` | 24.1 ms | 10.7 ms | 2.24× | 1.44× |
- | `Dump/signed_ints / 4` | 32.5 ms | 11.3 ms | 2.87× | 2.01× |
- | `Dump/small_signed_ints / -` | 18.1 ms | 8.12 ms | 2.23× | 1.81× |
- | `Dump/small_signed_ints / 4` | 25.5 ms | 8.81 ms | 2.90× | 2.21× |
- | `Dump/twitter / -` | 1.44 ms | 214 µs | 6.70× | 5.75× |
- | `Dump/twitter / 4` | 1.63 ms | 253 µs | 6.45× | 5.18× |
- | `Dump/unsigned_ints / -` | 24 ms | 8.12 ms | 2.95× | 1.58× |
- | `Dump/unsigned_ints / 4` | 32.3 ms | 8.59 ms | 3.76× | 2.30× |
- | `Equal/canada` | 474 µs | 349 µs | 1.36× | 1.38× |
- | `Equal/citm_catalog` | 280 µs | 263 µs | 1.06× | 0.95× |
- | `Equal/twitter` | 146 µs | 131 µs | 1.11× | 1.02× |
- | `Flatten/canada` | 29.4 ms | 11.1 ms | 2.65× | 2.68× |
- | `Flatten/citm_catalog` | 6.72 ms | 3.07 ms | 2.19× | 2.45× |
- | `Flatten/twitter` | 3.66 ms | 2.11 ms | 1.74× | 2.19× |
- | `FlattenOrdered/citm_catalog` | 262 ms | 1.43 ms | 184× | 185× |
- | `FlattenOrdered/twitter` | 51.3 ms | 890 µs | 57.6× | 46.1× |
- | `FromBinaryBuffer/bjdata / canada` | 8.82 ms | 4.17 ms | 2.12× | 2.02× |
- | `FromBinaryBuffer/bjdata / twitter` | 3.32 ms | 976 µs | 3.40× | 2.87× |
- | `FromBinaryBuffer/bjdata_optimized / canada` | 9.24 ms | 2.48 ms | 3.73× | 3.51× |
- | `FromBinaryBuffer/bjdata_optimized / twitter` | 3.31 ms | 986 µs | 3.35× | 2.86× |
- | `FromBinaryBuffer/bson / canada` | 10 ms | 4.32 ms | 2.33× | 2.20× |
- | `FromBinaryBuffer/bson / citm_catalog` | 4.08 ms | 1.61 ms | 2.54× | 2.59× |
- | `FromBinaryBuffer/bson / twitter` | 3.26 ms | 937 µs | 3.48× | 3.08× |
- | `FromBinaryBuffer/cbor / canada` | 8.9 ms | 2.12 ms | 4.21× | 3.86× |
- | `FromBinaryBuffer/cbor / citm_catalog` | 3.94 ms | 1.45 ms | 2.71× | 2.81× |
- | `FromBinaryBuffer/cbor / floats` | 30.5 ms | 5.48 ms | 5.56× | 3.64× |
- | `FromBinaryBuffer/cbor / jeopardy` | 373 ms | 115 ms | 3.25× | 3.43× |
- | `FromBinaryBuffer/cbor / signed_ints` | 36.7 ms | 9.89 ms | 3.71× | 2.37× |
- | `FromBinaryBuffer/cbor / twitter` | 3.23 ms | 953 µs | 3.39× | 2.83× |
- | `FromBinaryBuffer/msgpack / canada` | 8.86 ms | 2.12 ms | 4.17× | 3.92× |
- | `FromBinaryBuffer/msgpack / citm_catalog` | 3.85 ms | 1.45 ms | 2.65× | 2.94× |
- | `FromBinaryBuffer/msgpack / jeopardy` | 373 ms | 111 ms | 3.36× | 3.51× |
- | `FromBinaryBuffer/msgpack / twitter` | 3.24 ms | 955 µs | 3.39× | 3.02× |
- | `FromBinaryBuffer/ubjson / canada` | 8.84 ms | 4.2 ms | 2.10× | 2.13× |
- | `FromBinaryBuffer/ubjson / citm_catalog` | 4.07 ms | 1.79 ms | 2.28× | 2.44× |
- | `FromBinaryBuffer/ubjson / jeopardy` | 379 ms | 117 ms | 3.24× | 3.40× |
- | `FromBinaryBuffer/ubjson / twitter` | 3.3 ms | 981 µs | 3.36× | 2.90× |
- | `FromBinaryBuffer/ubjson_optimized / canada` | 8.89 ms | 2.43 ms | 3.66× | 3.67× |
- | `FromBinaryBuffer/ubjson_optimized / twitter` | 3.3 ms | 975 µs | 3.38× | 2.88× |
- | `FromBinaryFile/bjdata / twitter` | 10.9 ms | 1.95 ms | 5.61× | 5.89× |
- | `FromBinaryFile/bson / twitter` | 10.7 ms | 1.85 ms | 5.80× | 6.21× |
- | `FromBinaryFile/cbor / canada` | 14.9 ms | 4.49 ms | 3.32× | 3.68× |
- | `FromBinaryFile/cbor / twitter` | 10.4 ms | 1.57 ms | 6.59× | 6.46× |
- | `FromBinaryFile/ubjson / canada` | 14.3 ms | 6.43 ms | 2.23× | 2.50× |
- | `FromBinaryFile/ubjson / twitter` | 10.9 ms | 1.97 ms | 5.54× | 5.90× |
- | `FromBinaryShape/containers / bjdata` | 14.7 ms | 7.23 ms | 2.03× | 2.05× |
- | `FromBinaryShape/containers / bson` | 17.6 ms | 7.89 ms | 2.23× | 2.20× |
- | `FromBinaryShape/containers / cbor` | 14.6 ms | 4.16 ms | 3.52× | 3.49× |
- | `FromBinaryShape/containers / msgpack` | 14.6 ms | 3.89 ms | 3.75× | 3.52× |
- | `FromBinaryShape/containers / ubjson_optimized` | 15.5 ms | 4.56 ms | 3.40× | 3.34× |
- | `FromBinaryShape/containers / ubjson` | 15 ms | 7.24 ms | 2.07× | 2.03× |
- | `FromBinaryShape/nested / bjdata` | 96.8 µs | 38.3 µs | 2.53× | 2.50× |
- | `FromBinaryShape/nested / bson` | 101 µs | 42.6 µs | 2.38× | 2.42× |
- | `FromBinaryShape/nested / cbor` | 94.9 µs | 35.8 µs | 2.65× | 2.57× |
- | `FromBinaryShape/nested / msgpack` | 92.6 µs | 35.1 µs | 2.64× | 2.60× |
- | `FromBinaryShape/nested / ubjson` | 96.1 µs | 38.3 µs | 2.51× | 2.50× |
- | `FromBinaryShape/scalars / bjdata` | 25.3 ms | 11.6 ms | 2.18× | 2.22× |
- | `FromBinaryShape/scalars / cbor` | 26.2 ms | 5.89 ms | 4.45× | 3.56× |
- | `FromBinaryShape/scalars / msgpack` | 25.1 ms | 5.65 ms | 4.44× | 3.69× |
- | `FromBinaryShape/scalars / ubjson` | 25.3 ms | 11.6 ms | 2.18× | 2.03× |
- | `FromCborChunkedString/10000 chunks` | 91.8 µs | 82.9 µs | 1.11× | 1.09× |
- | `FromMsgpack/canada` | 14.9 ms | 4.56 ms | 3.26× | 3.63× |
- | `FromMsgpack/citm_catalog` | 9.36 ms | 2.26 ms | 4.14× | 4.60× |
- | `FromMsgpack/floats` | 61.3 ms | 28.7 ms | 2.14× | 2.89× |
- | `FromMsgpack/jeopardy` | 1,170 ms | 190 ms | 6.14× | 6.80× |
- | `FromMsgpack/signed_ints` | 63.9 ms | 32.9 ms | 1.94× | 2.44× |
- | `FromMsgpack/small_signed_ints` | 64.5 ms | 34.4 ms | 1.87× | 2.33× |
- | `FromMsgpack/twitter` | 10.3 ms | 1.49 ms | 6.92× | 6.76× |
- | `FromMsgpack/unsigned_ints` | 57.8 ms | 28.3 ms | 2.04× | 2.77× |
- | `GetArray>` | 2.27 µs | 654 ns | 3.48× | 5.38× |
- | `GetArray>` | 8.29 µs | 1.18 µs | 7.05× | 4.75× |
- | `GetArray>` | 60 µs | 42.5 µs | 1.41× | 1.30× |
- | `GetArray>` | 6.79 µs | 682 ns | 9.95× | 4.10× |
- | `GetMap/citm_catalog` | 3.6 ms | 2.49 ms | 1.45× | 1.48× |
- | `GetVectorDouble/floats` | 6.84 ms | 656 µs | 10.4× | 2.98× |
- | `GetVectorInt/signed_ints` | 8.22 ms | 3.46 ms | 2.37× | 2.02× |
- | `Iterate/canada` | 897 µs | 328 µs | 2.73× | 2.02× |
- | `Iterate/citm_catalog` | 305 µs | 171 µs | 1.78× | 1.53× |
- | `Iterate/twitter` | 90 µs | 62 µs | 1.45× | 1.20× |
- | `JsonPointerAt/citm_catalog` | 1.9 ms | 1.18 ms | 1.61× | 1.47× |
- | `JsonPointerAt/twitter` | 818 µs | 612 µs | 1.34× | 1.14× |
- | `JsonPointerContains/citm_catalog` | 2.82 ms | 1.27 ms | 2.22× | 2.51× |
- | `JsonPointerContains/twitter` | 1.34 ms | 629 µs | 2.12× | 2.16× |
- | `JsonPointerParse/citm_catalog` | 5.81 ms | 2.25 ms | 2.59× | 3.74× |
- | `JsonPointerParse/twitter` | 2.48 ms | 1.02 ms | 2.43× | 3.18× |
- | `JsonPointerToString/citm_catalog` | 6.79 ms | 1.94 ms | 3.50× | 4.61× |
- | `JsonPointerToString/twitter` | 2.23 ms | 834 µs | 2.67× | 2.66× |
- | `ParseFILE/canada` | 54.2 ms | 14.7 ms | 3.69× | 5.46× |
- | `ParseFILE/citm_catalog` | 36 ms | 7.58 ms | 4.74× | 5.73× |
- | `ParseFILE/twitter` | 14.1 ms | 3.45 ms | 4.08× | 4.24× |
- | `ParseFile/canada` | 18.3 ms | 6.47 ms | 2.83× | 4.67× |
- | `ParseFile/citm_catalog` | 8.13 ms | 2.52 ms | 3.22× | 2.98× |
- | `ParseFile/floats` | 131 ms | 48.1 ms | 2.73× | 5.68× |
- | `ParseFile/jeopardy` | 338 ms | 162 ms | 2.09× | 2.23× |
- | `ParseFile/signed_ints` | 129 ms | 46.2 ms | 2.78× | 3.41× |
- | `ParseFile/small_signed_ints` | 64.7 ms | 33.4 ms | 1.94× | 2.57× |
- | `ParseFile/twitter` | 3.43 ms | 1.37 ms | 2.49× | 2.33× |
- | `ParseFile/unsigned_ints` | 124 ms | 47.4 ms | 2.61× | 3.52× |
- | `ParseIndented/canada / 4` | 35 ms | 5.37 ms | 6.51× | 8.02× |
- | `ParseIndented/citm_catalog / 4` | 7.52 ms | 1.81 ms | 4.15× | 3.49× |
- | `ParseIndented/jeopardy / 4` | 345 ms | 133 ms | 2.59× | 2.46× |
- | `ParseIndented/twitter / 4` | 3.72 ms | 1.13 ms | 3.30× | 2.60× |
- | `ParseStreamValues/ndjson` | 7.16 ms | 3.77 ms | 1.90× | 2.06× |
- | `ParseStreamValues/numbers` | 1.93 ms | 383 µs | 5.03× | 6.06× |
- | `ParseString/canada` | 15.5 ms | 4.63 ms | 3.35× | 6.13× |
- | `ParseString/citm_catalog` | 7.56 ms | 1.78 ms | 4.25× | 3.46× |
- | `ParseString/floats` | 122 ms | 28.3 ms | 4.30× | 8.68× |
- | `ParseString/jeopardy` | 317 ms | 131 ms | 2.42× | 2.32× |
- | `ParseString/signed_ints` | 123 ms | 32 ms | 3.84× | 4.29× |
- | `ParseString/small_signed_ints` | 62.7 ms | 25 ms | 2.51× | 3.38× |
- | `ParseString/twitter` | 3.3 ms | 1.05 ms | 3.14× | 2.60× |
- | `ParseString/unsigned_ints` | 121 ms | 32.9 ms | 3.68× | 4.23× |
- | `ParseStringOrdered/canada` | 15.4 ms | 4.53 ms | 3.40× | 6.21× |
- | `ParseStringOrdered/citm_catalog` | 11.5 ms | 1.68 ms | 6.85× | 6.59× |
- | `ParseStringOrdered/jeopardy` | 374 ms | 113 ms | 3.29× | 3.38× |
- | `ParseStringOrdered/twitter` | 4.6 ms | 949 µs | 4.85× | 4.93× |
- | `Patch/canada` | 10.4 ms | 3.48 ms | 2.98× | 2.46× |
- | `Patch/citm_catalog` | 2.75 ms | 1.74 ms | 1.58× | 1.38× |
- | `Patch/twitter` | 1.58 ms | 889 µs | 1.78× | 1.45× |
- | `SaxParse/canada` | 13.1 ms | 3.27 ms | 4.01× | 8.84× |
- | `SaxParse/citm_catalog` | 6.55 ms | 967 µs | 6.77× | 6.85× |
- | `SaxParse/twitter` | 2.53 ms | 483 µs | 5.24× | 5.91× |
- | `ToCbor/canada` | 1.41 ms | 557 µs | 2.54× | 2.27× |
- | `ToCbor/citm_catalog` | 1.05 ms | 259 µs | 4.04× | 5.60× |
- | `ToCbor/floats` | 9.05 ms | 1.9 ms | 4.76× | 1.94× |
- | `ToCbor/jeopardy` | 73.9 ms | 22.1 ms | 3.35× | 2.87× |
- | `ToCbor/signed_ints` | 11.5 ms | 2.83 ms | 4.08× | 1.83× |
- | `ToCbor/small_signed_ints` | 10.4 ms | 2.48 ms | 4.19× | 2.78× |
- | `ToCbor/twitter` | 659 µs | 143 µs | 4.59× | 6.67× |
- | `ToCbor/unsigned_ints` | 8.18 ms | 2.82 ms | 2.90× | 1.48× |
- | `ToJsonStrings/copy` | 6.12 ms | 5.65 ms | 1.08× | 1.05× |
- | `ToJsonStrings/move` | 6.13 ms | 3.13 ms | 1.96× | 1.78× |
- | `Unflatten/canada` | 42.5 ms | 11.1 ms | 3.83× | 4.32× |
- | `Unflatten/citm_catalog` | 10.2 ms | 3.62 ms | 2.81× | 3.32× |
- | `Unflatten/twitter` | 4.75 ms | 2.21 ms | 2.15× | 2.42× |
+ | `Accept/canada` | 12.8 ms | 1.34 ms | 9.51× | 13× |
+ | `Accept/citm_catalog` | 6.48 ms | 474 µs | 13.7× | 8.45× |
+ | `Accept/floats` | 114 ms | 17.2 ms | 6.63× | 12.2× |
+ | `Accept/jeopardy` | 218 ms | 27.9 ms | 7.82× | 6.64× |
+ | `Accept/signed_ints` | 116 ms | 23.4 ms | 4.96× | 4.99× |
+ | `Accept/small_signed_ints` | 56.5 ms | 13.2 ms | 4.29× | 3.90× |
+ | `Accept/twitter` | 2.49 ms | 234 µs | 10.6× | 6.89× |
+ | `Accept/unsigned_ints` | 115 ms | 22.8 ms | 5.03× | 4.95× |
+ | `BinaryToCbor/1024` | 315 ns | 108 ns | 2.92× | 3.67× |
+ | `BinaryToCbor/128` | 266 ns | 114 ns | 2.33× | 2.27× |
+ | `BinaryToCbor/16384` | 950 ns | 418 ns | 2.27× | 3.46× |
+ | `BinaryToCbor/16` | 246 ns | 49.9 ns | 4.93× | 6.33× |
+ | `BinaryToCbor/2048` | 340 ns | 127 ns | 2.68× | 3.74× |
+ | `BinaryToCbor/256` | 280 ns | 91.8 ns | 3.05× | 3.33× |
+ | `BinaryToCbor/32768` | 1.58 µs | 838 ns | 1.88× | 2.11× |
+ | `BinaryToCbor/32` | 262 ns | 50.3 ns | 5.21× | 6.24× |
+ | `BinaryToCbor/4096` | 432 ns | 174 ns | 2.48× | 3.48× |
+ | `BinaryToCbor/512` | 297 ns | 102 ns | 2.91× | 3.02× |
+ | `BinaryToCbor/64` | 259 ns | 84.3 ns | 3.08× | 3.78× |
+ | `BinaryToCbor/8192` | 527 ns | 254 ns | 2.08× | 3.30× |
+ | `BinaryToCbor/8` | 251 ns | 49.9 ns | 5.02× | 6.18× |
+ | `Copy/canada` | 3.69 ms | 2.9 ms | 1.27× | 1.17× |
+ | `Copy/citm_catalog` | 1.65 ms | 1.49 ms | 1.11× | 1.12× |
+ | `Copy/twitter` | 811 µs | 729 µs | 1.11× | 1.14× |
+ | `CopyAssign/canada` | 3.74 ms | 2.94 ms | 1.27× | 1.19× |
+ | `CopyAssign/citm_catalog` | 1.65 ms | 1.54 ms | 1.08× | 1.12× |
+ | `CopyAssign/twitter` | 818 µs | 752 µs | 1.09× | 1.07× |
+ | `CopyOnly/canada` | 1.54 ms | 1.52 ms | 1.01× | 1.00× |
+ | `CopyOnly/citm_catalog` | 755 µs | 795 µs | 0.95× | 0.93× |
+ | `CopyOnly/twitter` | 407 µs | 410 µs | 0.99× | 1.02× |
+ | `CopyRing/canada` | 3.75 ms | 2.93 ms | 1.28× | 1.20× |
+ | `CopyRing/citm_catalog` | 1.75 ms | 1.55 ms | 1.13× | 1.20× |
+ | `CopyRing/twitter` | 876 µs | 784 µs | 1.12× | 1.15× |
+ | `DestroyOnly/canada` | 2.16 ms | 1.38 ms | 1.56× | 1.32× |
+ | `DestroyOnly/citm_catalog` | 896 µs | 682 µs | 1.31× | 1.20× |
+ | `DestroyOnly/twitter` | 406 µs | 327 µs | 1.24× | 1.15× |
+ | `Diff/canada` | 39.2 ms | 3.75 ms | 10.4× | 11.2× |
+ | `Diff/citm_catalog` | 6.92 ms | 1.09 ms | 6.33× | 7.78× |
+ | `Diff/twitter` | 4.93 ms | 640 µs | 7.71× | 8.79× |
+ | `Dump/canada / -` | 6.71 ms | 2.31 ms | 2.90× | 3.34× |
+ | `Dump/canada / 4` | 8.82 ms | 3.03 ms | 2.91× | 2.82× |
+ | `Dump/citm_catalog / -` | 1.39 ms | 296 µs | 4.71× | 3.93× |
+ | `Dump/citm_catalog / 4` | 1.95 ms | 422 µs | 4.62× | 4.09× |
+ | `Dump/floats / -` | 63.5 ms | 23.8 ms | 2.67× | 2.50× |
+ | `Dump/floats / 4` | 72 ms | 24.7 ms | 2.92× | 2.71× |
+ | `Dump/jeopardy / -` | 202 ms | 30.6 ms | 6.59× | 3.75× |
+ | `Dump/jeopardy / 4` | 220 ms | 34.4 ms | 6.42× | 2.61× |
+ | `Dump/signed_ints / -` | 23.7 ms | 10.7 ms | 2.21× | 1.44× |
+ | `Dump/signed_ints / 4` | 32 ms | 11.4 ms | 2.82× | 2.01× |
+ | `Dump/small_signed_ints / -` | 17.9 ms | 7.93 ms | 2.25× | 1.81× |
+ | `Dump/small_signed_ints / 4` | 25.1 ms | 8.81 ms | 2.84× | 2.21× |
+ | `Dump/twitter / -` | 1.41 ms | 214 µs | 6.60× | 5.75× |
+ | `Dump/twitter / 4` | 1.59 ms | 249 µs | 6.38× | 5.18× |
+ | `Dump/unsigned_ints / -` | 23.6 ms | 8.07 ms | 2.92× | 1.58× |
+ | `Dump/unsigned_ints / 4` | 31.7 ms | 8.51 ms | 3.73× | 2.30× |
+ | `Equal/canada` | 463 µs | 336 µs | 1.38× | 1.38× |
+ | `Equal/citm_catalog` | 264 µs | 261 µs | 1.01× | 0.95× |
+ | `Equal/twitter` | 139 µs | 129 µs | 1.08× | 1.02× |
+ | `Flatten/canada` | 28.7 ms | 11.2 ms | 2.56× | 2.68× |
+ | `Flatten/citm_catalog` | 6.44 ms | 3.01 ms | 2.14× | 2.45× |
+ | `Flatten/twitter` | 3.53 ms | 2.05 ms | 1.72× | 2.19× |
+ | `FlattenOrdered/citm_catalog` | 256 ms | 1.41 ms | 182× | 185× |
+ | `FlattenOrdered/twitter` | 50 ms | 864 µs | 57.9× | 46.1× |
+ | `FromBinaryBuffer/bjdata / canada` | 8.15 ms | 4.17 ms | 1.95× | 2.02× |
+ | `FromBinaryBuffer/bjdata / twitter` | 3.18 ms | 893 µs | 3.56× | 2.87× |
+ | `FromBinaryBuffer/bjdata_optimized / canada` | 8.74 ms | 2.42 ms | 3.61× | 3.51× |
+ | `FromBinaryBuffer/bjdata_optimized / twitter` | 3.16 ms | 885 µs | 3.57× | 2.86× |
+ | `FromBinaryBuffer/bson / canada` | 9.33 ms | 4.27 ms | 2.18× | 2.20× |
+ | `FromBinaryBuffer/bson / citm_catalog` | 3.92 ms | 1.61 ms | 2.43× | 2.59× |
+ | `FromBinaryBuffer/bson / twitter` | 3.18 ms | 910 µs | 3.49× | 3.08× |
+ | `FromBinaryBuffer/cbor / canada` | 8.39 ms | 2.08 ms | 4.03× | 3.86× |
+ | `FromBinaryBuffer/cbor / citm_catalog` | 3.71 ms | 1.32 ms | 2.81× | 2.81× |
+ | `FromBinaryBuffer/cbor / floats` | 29.2 ms | 5.48 ms | 5.32× | 3.64× |
+ | `FromBinaryBuffer/cbor / jeopardy` | 353 ms | 101 ms | 3.50× | 3.43× |
+ | `FromBinaryBuffer/cbor / signed_ints` | 36 ms | 9.78 ms | 3.68× | 2.37× |
+ | `FromBinaryBuffer/cbor / twitter` | 3.09 ms | 879 µs | 3.52× | 2.83× |
+ | `FromBinaryBuffer/msgpack / canada` | 8.26 ms | 2.1 ms | 3.94× | 3.92× |
+ | `FromBinaryBuffer/msgpack / citm_catalog` | 3.72 ms | 1.29 ms | 2.89× | 2.94× |
+ | `FromBinaryBuffer/msgpack / jeopardy` | 357 ms | 96.9 ms | 3.68× | 3.51× |
+ | `FromBinaryBuffer/msgpack / twitter` | 3.06 ms | 850 µs | 3.60× | 3.02× |
+ | `FromBinaryBuffer/ubjson / canada` | 8.13 ms | 4.14 ms | 1.96× | 2.13× |
+ | `FromBinaryBuffer/ubjson / citm_catalog` | 3.82 ms | 1.66 ms | 2.31× | 2.44× |
+ | `FromBinaryBuffer/ubjson / jeopardy` | 366 ms | 101 ms | 3.61× | 3.40× |
+ | `FromBinaryBuffer/ubjson / twitter` | 3.15 ms | 896 µs | 3.52× | 2.90× |
+ | `FromBinaryBuffer/ubjson_optimized / canada` | 8.51 ms | 2.39 ms | 3.56× | 3.67× |
+ | `FromBinaryBuffer/ubjson_optimized / twitter` | 3.12 ms | 882 µs | 3.54× | 2.88× |
+ | `FromBinaryFile/bjdata / twitter` | 10.4 ms | 1.45 ms | 7.18× | 5.89× |
+ | `FromBinaryFile/bson / twitter` | 10.3 ms | 1.24 ms | 8.33× | 6.21× |
+ | `FromBinaryFile/cbor / canada` | 14.3 ms | 2.41 ms | 5.96× | 3.68× |
+ | `FromBinaryFile/cbor / twitter` | 9.92 ms | 1.07 ms | 9.23× | 6.46× |
+ | `FromBinaryFile/ubjson / canada` | 13.6 ms | 6.23 ms | 2.17× | 2.50× |
+ | `FromBinaryFile/ubjson / twitter` | 10.5 ms | 1.46 ms | 7.20× | 5.90× |
+ | `FromBinaryShape/containers / bjdata` | 14.3 ms | 7.24 ms | 1.98× | 2.05× |
+ | `FromBinaryShape/containers / bson` | 17.4 ms | 7.98 ms | 2.18× | 2.20× |
+ | `FromBinaryShape/containers / cbor` | 13.9 ms | 3.93 ms | 3.54× | 3.49× |
+ | `FromBinaryShape/containers / msgpack` | 13.8 ms | 3.88 ms | 3.55× | 3.52× |
+ | `FromBinaryShape/containers / ubjson_optimized` | 15 ms | 4.49 ms | 3.34× | 3.34× |
+ | `FromBinaryShape/containers / ubjson` | 14.4 ms | 7.27 ms | 1.98× | 2.03× |
+ | `FromBinaryShape/nested / bjdata` | 96.4 µs | 38.2 µs | 2.52× | 2.50× |
+ | `FromBinaryShape/nested / bson` | 103 µs | 42.5 µs | 2.41× | 2.42× |
+ | `FromBinaryShape/nested / cbor` | 93.8 µs | 35.4 µs | 2.65× | 2.57× |
+ | `FromBinaryShape/nested / msgpack` | 92 µs | 34.9 µs | 2.64× | 2.60× |
+ | `FromBinaryShape/nested / ubjson` | 96.5 µs | 37.9 µs | 2.54× | 2.50× |
+ | `FromBinaryShape/scalars / bjdata` | 24.8 ms | 11.5 ms | 2.16× | 2.22× |
+ | `FromBinaryShape/scalars / cbor` | 25.7 ms | 5.69 ms | 4.51× | 3.56× |
+ | `FromBinaryShape/scalars / msgpack` | 24.4 ms | 5.57 ms | 4.39× | 3.69× |
+ | `FromBinaryShape/scalars / ubjson` | 24.9 ms | 11.6 ms | 2.15× | 2.03× |
+ | `FromCborChunkedString/10000 chunks` | 90.9 µs | 61.5 µs | 1.48× | 1.09× |
+ | `FromMsgpack/canada` | 14.4 ms | 2.4 ms | 5.97× | 3.63× |
+ | `FromMsgpack/citm_catalog` | 9.04 ms | 1.67 ms | 5.42× | 4.60× |
+ | `FromMsgpack/floats` | 59.8 ms | 28.2 ms | 2.12× | 2.89× |
+ | `FromMsgpack/jeopardy` | 1,140 ms | 112 ms | 10.2× | 6.80× |
+ | `FromMsgpack/signed_ints` | 63.1 ms | 32.7 ms | 1.93× | 2.44× |
+ | `FromMsgpack/small_signed_ints` | 63.4 ms | 35.3 ms | 1.80× | 2.33× |
+ | `FromMsgpack/twitter` | 9.99 ms | 1.01 ms | 9.87× | 6.76× |
+ | `FromMsgpack/unsigned_ints` | 56.1 ms | 28.2 ms | 1.99× | 2.77× |
+ | `GetArray>` | 2.24 µs | 641 ns | 3.50× | 5.38× |
+ | `GetArray>` | 8.16 µs | 1.17 µs | 6.96× | 4.75× |
+ | `GetArray>` | 58.8 µs | 41.6 µs | 1.42× | 1.30× |
+ | `GetArray>` | 6.73 µs | 671 ns | 10.0× | 4.10× |
+ | `GetMap/citm_catalog` | 3.44 ms | 2.44 ms | 1.41× | 1.48× |
+ | `GetVectorDouble/floats` | 6.7 ms | 651 µs | 10.3× | 2.98× |
+ | `GetVectorInt/signed_ints` | 8.07 ms | 3.41 ms | 2.37× | 2.02× |
+ | `Iterate/canada` | 874 µs | 445 µs | 1.96× | 2.02× |
+ | `Iterate/citm_catalog` | 302 µs | 158 µs | 1.91× | 1.53× |
+ | `Iterate/twitter` | 87.3 µs | 58.9 µs | 1.48× | 1.20× |
+ | `JsonPointerAt/citm_catalog` | 1.85 ms | 1.16 ms | 1.60× | 1.47× |
+ | `JsonPointerAt/twitter` | 791 µs | 600 µs | 1.32× | 1.14× |
+ | `JsonPointerContains/citm_catalog` | 2.74 ms | 1.25 ms | 2.18× | 2.51× |
+ | `JsonPointerContains/twitter` | 1.32 ms | 620 µs | 2.13× | 2.16× |
+ | `JsonPointerParse/citm_catalog` | 5.73 ms | 2.21 ms | 2.59× | 3.74× |
+ | `JsonPointerParse/twitter` | 2.47 ms | 1.01 ms | 2.45× | 3.18× |
+ | `JsonPointerToString/citm_catalog` | 6.65 ms | 1.9 ms | 3.51× | 4.61× |
+ | `JsonPointerToString/twitter` | 2.14 ms | 812 µs | 2.63× | 2.66× |
+ | `ParseFILE/canada` | 52.9 ms | 6.02 ms | 8.79× | 5.46× |
+ | `ParseFILE/citm_catalog` | 35 ms | 2.34 ms | 14.9× | 5.73× |
+ | `ParseFILE/twitter` | 13.8 ms | 1.35 ms | 10.2× | 4.24× |
+ | `ParseFile/canada` | 17.7 ms | 6.23 ms | 2.84× | 4.67× |
+ | `ParseFile/citm_catalog` | 7.86 ms | 2.47 ms | 3.18× | 2.98× |
+ | `ParseFile/floats` | 129 ms | 48.4 ms | 2.67× | 5.68× |
+ | `ParseFile/jeopardy` | 330 ms | 156 ms | 2.12× | 2.23× |
+ | `ParseFile/signed_ints` | 128 ms | 46.6 ms | 2.74× | 3.41× |
+ | `ParseFile/small_signed_ints` | 64.5 ms | 33.4 ms | 1.93× | 2.57× |
+ | `ParseFile/twitter` | 3.4 ms | 1.38 ms | 2.46× | 2.33× |
+ | `ParseFile/unsigned_ints` | 123 ms | 47.3 ms | 2.61× | 3.52× |
+ | `ParseIndented/canada / 4` | 34.2 ms | 4.61 ms | 7.41× | 8.02× |
+ | `ParseIndented/citm_catalog / 4` | 7.45 ms | 1.46 ms | 5.11× | 3.49× |
+ | `ParseIndented/jeopardy / 4` | 339 ms | 114 ms | 2.96× | 2.46× |
+ | `ParseIndented/twitter / 4` | 3.61 ms | 975 µs | 3.70× | 2.60× |
+ | `ParseStreamValues/ndjson` | 7.08 ms | 3.72 ms | 1.91× | 2.06× |
+ | `ParseStreamValues/numbers` | 1.93 ms | 388 µs | 4.97× | 6.06× |
+ | `ParseString/canada` | 15.2 ms | 3.73 ms | 4.08× | 6.13× |
+ | `ParseString/citm_catalog` | 7.4 ms | 1.45 ms | 5.11× | 3.46× |
+ | `ParseString/floats` | 120 ms | 25 ms | 4.80× | 8.68× |
+ | `ParseString/jeopardy` | 303 ms | 113 ms | 2.68× | 2.32× |
+ | `ParseString/signed_ints` | 121 ms | 28.7 ms | 4.22× | 4.29× |
+ | `ParseString/small_signed_ints` | 60.8 ms | 17.7 ms | 3.43× | 3.38× |
+ | `ParseString/twitter` | 3.21 ms | 921 µs | 3.49× | 2.60× |
+ | `ParseString/unsigned_ints` | 120 ms | 28.5 ms | 4.21× | 4.23× |
+ | `ParseStringOrdered/canada` | 15.3 ms | 3.78 ms | 4.04× | 6.21× |
+ | `ParseStringOrdered/citm_catalog` | 11 ms | 1.34 ms | 8.18× | 6.59× |
+ | `ParseStringOrdered/jeopardy` | 360 ms | 98.1 ms | 3.67× | 3.38× |
+ | `ParseStringOrdered/twitter` | 4.41 ms | 804 µs | 5.49× | 4.93× |
+ | `Patch/canada` | 10.1 ms | 3.41 ms | 2.97× | 2.46× |
+ | `Patch/citm_catalog` | 2.54 ms | 1.65 ms | 1.54× | 1.38× |
+ | `Patch/twitter` | 1.49 ms | 870 µs | 1.72× | 1.45× |
+ | `SaxParse/canada` | 12.9 ms | 2.5 ms | 5.15× | 8.84× |
+ | `SaxParse/citm_catalog` | 6.44 ms | 655 µs | 9.82× | 6.85× |
+ | `SaxParse/twitter` | 2.49 ms | 334 µs | 7.45× | 5.91× |
+ | `ToCbor/canada` | 1.39 ms | 555 µs | 2.51× | 2.27× |
+ | `ToCbor/citm_catalog` | 1.03 ms | 260 µs | 3.97× | 5.60× |
+ | `ToCbor/floats` | 8.94 ms | 1.85 ms | 4.84× | 1.94× |
+ | `ToCbor/jeopardy` | 71.5 ms | 21.1 ms | 3.39× | 2.87× |
+ | `ToCbor/signed_ints` | 11.4 ms | 2.74 ms | 4.15× | 1.83× |
+ | `ToCbor/small_signed_ints` | 10.3 ms | 2.38 ms | 4.33× | 2.78× |
+ | `ToCbor/twitter` | 637 µs | 144 µs | 4.41× | 6.67× |
+ | `ToCbor/unsigned_ints` | 8.05 ms | 2.74 ms | 2.94× | 1.48× |
+ | `ToJsonStrings/copy` | 5.86 ms | 5.6 ms | 1.05× | 1.05× |
+ | `ToJsonStrings/move` | 5.92 ms | 3.05 ms | 1.94× | 1.78× |
+ | `Unflatten/canada` | 41.8 ms | 10.8 ms | 3.86× | 4.32× |
+ | `Unflatten/citm_catalog` | 9.8 ms | 3.54 ms | 2.77× | 3.32× |
+ | `Unflatten/twitter` | 4.57 ms | 2.17 ms | 2.11× | 2.42× |
### C. The timeline
@@ -1866,18 +1969,21 @@ six code layouts: they were equal within 0.2%.
| | 4 | [#5809][5809] | positions derived only on error | −12% |
| | 2 | [#5810][5810] | indentation skipped eight bytes at a time | −12% |
| | 4 | [#5882][5882] | arrays allocated once, at their final size | −6% |
+| | 1 | [#6031][6031] | one loop for parser and lexer on contiguous input | −12% |
| `parse()` `canada.json` | 4 | [#5234][5234] | the token buffer is filled only for error messages | −10% |
| | 1 | [#5283][5283] | fast paths for contiguous input | −24% |
| | 3 | [#5617][5617] | Eisel-Lemire instead of `strtod` | −9% |
| | 3 | [#5738][5738] | own float parser, string scan, `\u` table | −14% |
| | 3 | [#5851][5851] | digits accumulated while validating, eight at a time | −12% |
| | 4 | [#5882][5882] | arrays allocated once, at their final size | −15% |
+| | 1 | [#6031][6031] | one loop for parser and lexer on contiguous input | −16% |
| `accept()` `twitter.json` | 4 | [#5234][5234] | the token buffer is filled only for error messages | −18% |
| | 1 | [#5283][5283] | fast paths for contiguous input | −53% |
| | 4 | [#5592][5592] | check for the expected `:` or `,` before the lexer's token switch | −11% |
| | 4 | [#5809][5809] | positions derived only on error | −23% |
| | 2 | [#5810][5810] | indentation skipped eight bytes at a time | −9% |
| | 2 | [#5853][5853] | strings scanned 16 bytes at a time with SSE2 or NEON | −11% |
+| | 1 | [#6031][6031] | one loop for parser and lexer on contiguous input | −51% |
| `parse()` `twitter.json`, `std::ifstream` | 1 | [#5828][5828] | scanning the stream's get area in bulk | −48% |
| | 1 | [#5884][5884] | inline get-area path, binary reads | −5% |
| `dump()` `twitter.json` | 6 | [#5285][5285] | a write buffer and bulk copies of strings | −70% |
@@ -1890,12 +1996,14 @@ six code layouts: they were equal within 0.2%.
| | 4 | [#5501][5501] | the result moved instead of copied | −33% |
| | U | [#5185][5185], [#5531][5531] | UTF-8 validation of every text string | +37% |
| | U | [#5746][5746] | the validation made an option ([#5741][5741]) | −27% |
+| | 4 | [#6028][6028] | binary strings appended without filling them with zeros first | −6% |
| `to_cbor()` `twitter.json` | 4 | [#5328][5328] | no temporary `basic_json` per object key (by [@alexprabhat99](https://github.com/alexprabhat99)) | −55% |
| | 6 | [#5824][5824] | a cursor instead of `vector::insert()` | −38% |
| | 6 | [#5885][5885] | runs of numbers encoded in a loop | −32% |
The labeled steps of the timeline: the change in time at the first commit of each
-pull request, against the commit before it. [#5592][5592] is by [@mvanslobbe](https://github.com/mvanslobbe).
Compile time in milliseconds of the two programs in [The price](#the-price) (CPU
-time of the compiler, median of 5 runs). Building the precompiled header took 973 and 987 ms with Apple clang (`-O0`,
-`-O2`) and 2,565 and 2,589 ms with GCC; building the module took 1,617 and 1,602 ms or 2,499 and 2,465 ms.
+time of the compiler, median of 5 runs). Building the precompiled header took 917 and 879 ms with Apple clang (`-O0`,
+`-O2`) and 2,306 and 2,264 ms with GCC; building the module took 1,399 and 1,433 ms or 2,339 and 2,325 ms.
??? example "The programs and the build commands"
@@ -2566,19 +2675,21 @@ suite also on an Intel laptop with Clang. The comparison with other libraries al
which Haswell doesn't have, so the real CPU is probably a later one. It shares its cores with other work, so its
differences of a few percent are noise. The theme sections quote x86-64 numbers where a pull request measured them,
mostly with GCC 13 and Clang 18. Expect smaller gains for some workloads on x86-64 with glibc: its `FILE*` locking is
-cheaper to begin with, and its allocator behaves differently. We did not measure on Windows, and we did not measure
-binary size or memory use.
+cheaper to begin with, so taking it once per parse gains less, its allocator behaves differently, and fresh output
+memory costs more page faults. We did not measure on Windows, and we did not measure binary size or memory use.
### H. Pull requests by theme
- 1 **Specialize for each input source:** contiguous input
- [#5283][5283]; streams and `FILE*` [#5828][5828], [#5884][5884]; binary strings in bulk [#5233][5233]; sized sentinels
- [#5268][5268].
+ [#5283][5283]; streams and `FILE*` [#5828][5828], [#5884][5884]; the buffer of a `FILE` [#6030][6030]; iterator
+ pairs [#6029][6029]; one loop for parser and lexer [#6031][6031]; binary strings in bulk [#5233][5233]; sized
+ sentinels [#5268][5268].
- 2 **Work on words, not bytes:** word-at-a-time strings, UTF-8
runs, and digits [#5283][5283], [#5738][5738], [#5851][5851]; indentation [#5810][5810]; `dump()` runs [#5285][5285];
SSE2 and NEON [#5853][5853], [#5628][5628]; `json_document` with `ensure_ascii` [#5879][5879]; short strings
[#5878][5878], [#5883][5883]; escape table [#5822][5822]; loop shape [#5490][5490]; fraction digits [#5971][5971];
- BSON keys [#5968][5968]; UTF-8 checks of binary strings [#5982][5982]; one SIMD macro set [#5921][5921].
+ BSON keys [#5968][5968]; UTF-8 checks of binary strings [#5982][5982]; one SIMD macro set [#5921][5921]; fewer
+ operations per word and vector [#6033][6033].
- 3 **Own the number conversions:** Eisel-Lemire [#5617][5617];
long numbers [#5738][5738]; one pass over the digits [#5851][5851]; Żmij [#5634][5634]; long integers in `dump()`
[#5964][5964]; MSVC intrinsics for the 128-bit multiplication [#5782][5782].
@@ -2587,13 +2698,15 @@ binary size or memory use.
[#5501][5501]; `ordered_json` growth [#5609][5609]; arrays at their final size [#5882][5882]; moving keys
[#5458][5458], [#5855][5855]; separator check [#5592][5592]; CBOR keys [#5328][5328]; `accept()` without copies
[#5978][5978]; runs of scalars in binary arrays [#5975][5975], [#5980][5980], [#5981][5981], [#6008][6008]; BJData
- marker list [#5555][5555]; `from_json()` into containers [#5833][5833].
+ marker list [#5555][5555]; `from_json()` into containers [#5833][5833]; binary strings without a zero fill
+ [#6028][6028].
- 5 **Read the assembly:** stack canary [#5854][5854];
exceptions and inlining [#5838][5838], [#5840][5840], [#5839][5839], [#5836][5836]; aliasing [#5633][5633]; forced
- inlining [#5878][5878]; branch-free selections [#5908][5908]; code size [#5902][5902].
+ inlining [#5878][5878]; branch-free selections [#5908][5908]; code size [#5902][5902]; the order of the iterator's
+ type tests [#6039][6039].
- 6 **Write through a cursor:** write buffer [#5285][5285];
adapter on the stack [#5449][5449]; binary sinks [#5286][5286]; cursor and runs of numbers [#5824][5824],
- [#5885][5885]; `to_bson()` in one pass [#5979][5979].
+ [#5885][5885]; `to_bson()` in one pass [#5979][5979]; a window instead of zeroing the whole capacity [#6020][6020].
- 7 **Fix the algorithm first:** `diff()` [#5461][5461]; parse
callbacks [#5457][5457]; `operator<` [#5390][5390]; `flatten()` [#5834][5834]; `unflatten()` [#5793][5793]; JSON
Pointer and Patch [#5861][5861], [#5886][5886]; `to_bson()` [#5553][5553]; `JSON_DIAGNOSTICS` [#5832][5832]; bounded
@@ -2681,7 +2794,15 @@ These are the checks behind the themes:
[Appendix F](#f-behavior-changes).
- **Stream windows** (theme 1). A test `streambuf` hands out get
areas of 1–4,096 bytes, so that every token crosses a window edge at every offset, and the result is compared with a
- `streambuf` without a get area, which takes the old path.
+ `streambuf` without a get area, which takes the old path. The `FILE` window is checked the same way, with `FILE`
+ buffers of 1 to 70 bytes, unbuffered files, and characters pushed back with `ungetc()` before the parse, on macOS and
+ glibc.
+- **The fused parser** (theme 1). `contiguous_parser` is
+ compared with the byte path on corner cases of literals, numbers, escapes, surrogates and UTF-8, on 1,500 generated
+ documents, every truncation of the short ones and random mutations, and on nesting across the 64-level words of its
+ stack: the same values with the same number types and float bits, the same exception messages, the same `accept()`
+ verdicts and the same `sax_parse()` events, also for handlers that stop early (242,118 assertions). A fuzzer that
+ compares the `sax_parse()` events of both paths ran 9 million inputs without a finding.
- **Differential fuzzing of the input kinds.** Every fuzz input is also parsed from an `std::istringstream`, from
`std::list` iterators (the byte path) and from a `FILE*`, and the value, the exception id, the message, and the byte
offset must be identical ([#5827][5827]).
@@ -2792,3 +2913,10 @@ These are the checks behind the themes:
[5981]: https://github.com/nlohmann/json/pull/5981
[5982]: https://github.com/nlohmann/json/pull/5982
[6008]: https://github.com/nlohmann/json/pull/6008
+[6020]: https://github.com/nlohmann/json/pull/6020
+[6028]: https://github.com/nlohmann/json/pull/6028
+[6029]: https://github.com/nlohmann/json/pull/6029
+[6030]: https://github.com/nlohmann/json/pull/6030
+[6031]: https://github.com/nlohmann/json/pull/6031
+[6033]: https://github.com/nlohmann/json/pull/6033
+[6039]: https://github.com/nlohmann/json/pull/6039
diff --git a/docs/mkdocs/docs/images/perf_blog_distribution.svg b/docs/mkdocs/docs/images/perf_blog_distribution.svg
index 4760bfd85..313897a5c 100644
--- a/docs/mkdocs/docs/images/perf_blog_distribution.svg
+++ b/docs/mkdocs/docs/images/perf_blog_distribution.svg
@@ -1,34 +1,34 @@