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..270e1e9d1 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
@@ -32,7 +32,8 @@ This post is about *how*. Hardly any of the techniques are 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
+[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
@@ -202,14 +203,16 @@ iterators over a `std::list 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.
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 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. +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 @@ -738,7 +815,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 +1088,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 @@ -2566,19 +2657,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 +2680,14 @@ 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]. - 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 +2775,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 +2894,9 @@ 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