From ea848cd2e8332190da627a96798b75a4515beb01 Mon Sep 17 00:00:00 2001 From: Niels Lohmann Date: Tue, 27 Oct 2026 23:53:37 +0100 Subject: [PATCH] docs: add the blog post "How 3.13.0 got faster" A post for the documentation (Home > Blog) on how 3.13.0 became faster: eight themes with the pull requests behind them, the ideas taken from other libraries, what did not work, how the fast paths are tested, and the compile-time cost. Every number comes from a run on 2026-10-27 (3.12.0 against develop plus the open non-draft PRs, Apple M1 Max) or from the linked PR. - docs/mkdocs/docs/home/blog/how_3_13_0_got_faster.md: the post. - docs/mkdocs/scripts/figures/perf_blog.py: draws its 12 figures (overview, eight timeline panels, the SWAR test, the cliffs, json_view throughput) as inline SVGs in the nj-charts style; data/perf_timeline.csv holds the timeline. - tests/benchmarks/history: history.cpp and history.py time one fixed program against the header of every commit that changed it, interleaved, and write one CSV row per commit with its PR. The timeline of the post comes from it. Signed-off-by: Niels Lohmann --- docs/mkdocs/docs/css/fig-charts.css | 4 + .../docs/home/blog/how_3_13_0_got_faster.md | 774 ++++++++++++++++++ docs/mkdocs/docs/images/perf_blog_cliffs.svg | 67 ++ .../mkdocs/docs/images/perf_blog_overview.svg | 87 ++ docs/mkdocs/docs/images/perf_blog_swar.svg | 60 ++ .../perf_blog_timeline_accept_twitter.svg | 37 + .../images/perf_blog_timeline_dump_canada.svg | 27 + .../perf_blog_timeline_dump_twitter.svg | 35 + .../perf_blog_timeline_from_cbor_twitter.svg | 31 + .../perf_blog_timeline_parse_canada.svg | 37 + .../images/perf_blog_timeline_parse_citm.svg | 37 + ...f_blog_timeline_parse_twitter_ifstream.svg | 25 + .../perf_blog_timeline_to_cbor_twitter.svg | 30 + docs/mkdocs/docs/images/perf_blog_view.svg | 80 ++ docs/mkdocs/mkdocs.yml | 3 + .../scripts/figures/data/perf_timeline.csv | 318 +++++++ docs/mkdocs/scripts/figures/perf_blog.py | 360 ++++++++ tests/benchmarks/README.md | 3 + tests/benchmarks/history/README.md | 43 + tests/benchmarks/history/history.cpp | 88 ++ tests/benchmarks/history/history.py | 151 ++++ 21 files changed, 2297 insertions(+) create mode 100644 docs/mkdocs/docs/home/blog/how_3_13_0_got_faster.md create mode 100644 docs/mkdocs/docs/images/perf_blog_cliffs.svg create mode 100644 docs/mkdocs/docs/images/perf_blog_overview.svg create mode 100644 docs/mkdocs/docs/images/perf_blog_swar.svg create mode 100644 docs/mkdocs/docs/images/perf_blog_timeline_accept_twitter.svg create mode 100644 docs/mkdocs/docs/images/perf_blog_timeline_dump_canada.svg create mode 100644 docs/mkdocs/docs/images/perf_blog_timeline_dump_twitter.svg create mode 100644 docs/mkdocs/docs/images/perf_blog_timeline_from_cbor_twitter.svg create mode 100644 docs/mkdocs/docs/images/perf_blog_timeline_parse_canada.svg create mode 100644 docs/mkdocs/docs/images/perf_blog_timeline_parse_citm.svg create mode 100644 docs/mkdocs/docs/images/perf_blog_timeline_parse_twitter_ifstream.svg create mode 100644 docs/mkdocs/docs/images/perf_blog_timeline_to_cbor_twitter.svg create mode 100644 docs/mkdocs/docs/images/perf_blog_view.svg create mode 100644 docs/mkdocs/scripts/figures/data/perf_timeline.csv create mode 100644 docs/mkdocs/scripts/figures/perf_blog.py create mode 100644 tests/benchmarks/history/README.md create mode 100644 tests/benchmarks/history/history.cpp create mode 100755 tests/benchmarks/history/history.py diff --git a/docs/mkdocs/docs/css/fig-charts.css b/docs/mkdocs/docs/css/fig-charts.css index 742a70bb7..ffd679986 100644 --- a/docs/mkdocs/docs/css/fig-charts.css +++ b/docs/mkdocs/docs/css/fig-charts.css @@ -27,6 +27,10 @@ .md-typeset svg.nj-charts--compile, .md-typeset svg.nj-charts--scaling { max-width: 26rem; } .md-typeset svg.nj-charts--sizes { max-width: 26rem; } +/* home/blog/how_3_13_0_got_faster.md (scripts/figures/perf_blog.py) */ +.md-typeset svg.nj-charts--blog { max-width: 34rem; } +.md-typeset svg.nj-charts--swar { max-width: 34rem; } +.md-typeset svg.nj-charts--timeline { max-width: 22rem; } .nj-charts text { fill: currentColor; font-size: 13px; } .nj-charts .nj-charts-leg { font-size: 13.5px; } 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 new file mode 100644 index 000000000..83f9ab096 --- /dev/null +++ b/docs/mkdocs/docs/home/blog/how_3_13_0_got_faster.md @@ -0,0 +1,774 @@ +# How nlohmann/json 3.13.0 got 2.9× faster + +Since 2016, the README of this library has said this about speed: + +> There are certainly faster JSON libraries out there. However, if your goal is to speed up your development by adding +> JSON support with a single header, then this library is the way to go. + +That sentence is still true, but the gap is a lot smaller now. Swap the 3.12.0 header for the 3.13.0 one and recompile, +and the 182 benchmarks of the library's suite run on average 2.9× as fast, without a line of your code changing. +Parsing the 1.7 MB `citm_catalog.json` takes 1.75 ms instead of 7.20 ms, and `dump()` of documents made of objects +and strings is 4.7 to 6.7× as fast. The new read-only [`json_document`](../../features/json_view.md) parses the +usual test files faster than yyjson and simdjson's DOM on Apple silicon (with a caveat for float-heavy files that we +explain below). + +This post is about *how*. None of the techniques is new: almost every one of them is borrowed from simdjson, yyjson, +fast_float, Żmij, serde_json, SQLite, Boost.JSON or a textbook, and we name the source at each step. What we think is +worth writing about is the combination, and the conditions it had to work under: a C++11 header-only library with a +value type that is a template over its string, number, object and allocator types, input from strings, streams, +`FILE*`, iterator pairs, wide strings and user-defined adapters, output that had to stay byte-for-byte the same (with one deliberate exception, see theme 3), and error messages that had to keep pointing at the same byte. + +We tried to be careful about what we claim. Every number below comes either from a benchmark we ran for this post +(described in [How we measured](#how-we-measured)) or from the description of the pull request that made the change, +which we link. The PR numbers were measured at the time against the state before the PR, often on more than one +machine, and say so. + +## The short version + +Apple M1 Max, Apple clang 21, `-O3 -flto`, the compiler's default standard (C++14); the same benchmark source for both +headers. The parse rows do not include destroying the result (the suite pauses the timer for that): + +| workload | 3.12.0 | 3.13.0 | speedup | +| --- | --: | --: | --: | +| `json::parse()` of `twitter.json` | 3.16 ms | 1.05 ms | 3.0× | +| `json::parse()` of `citm_catalog.json` | 7.20 ms | 1.75 ms | 4.1× | +| `json::parse()` of `canada.json` | 15.1 ms | 4.35 ms | 3.5× | +| `json::parse()` of `canada.json`, indented by 4 spaces | 33.8 ms | 5.02 ms | 6.7× | +| `ordered_json::parse()` of `citm_catalog.json` | 10.8 ms | 1.64 ms | 6.6× | +| `json::parse()` of `twitter.json` from an `std::ifstream` | 3.34 ms | 1.35 ms | 2.5× | +| `json::parse()` of `citm_catalog.json` from a `FILE*` | 34.6 ms | 7.26 ms | 4.8× | +| `json::accept()` of `citm_catalog.json` | 6.36 ms | 0.91 ms | 7.0× | +| `dump()` of `twitter.json` | 1.39 ms | 0.208 ms | 6.7× | +| `dump()` of `canada.json` | 6.63 ms | 2.25 ms | 2.9× | +| `dump(4)` of `citm_catalog.json` | 1.93 ms | 0.416 ms | 4.6× | +| `to_cbor()` of `twitter.json` | 0.638 ms | 0.142 ms | 4.5× | +| `from_cbor()` of `twitter.json` from memory | 3.00 ms | 0.965 ms | 3.1× | +| `from_cbor()` of `twitter.json` from a `FILE*` | 9.71 ms | 1.54 ms | 6.3× | +| `diff()` of `canada.json` with every eighth scalar changed | 38.3 ms | 3.67 ms | 10.4× | +| iterating over every value of `canada.json` | 0.861 ms | 0.320 ms | 2.7× | +| copying `citm_catalog.json` and destroying the copy | 1.61 ms | 1.45 ms | 1.11× | +| `==` of `citm_catalog.json` and a copy | 0.264 ms | 0.254 ms | 1.04× | + +
+--8<-- "images/perf_blog_overview.svg" +
+ +The average over all 182 benchmarks of the suite is 2.93× (geometric mean); none got slower except the time of a +copy alone, 0.94 to 1.03× (copying and destroying together is 1.09 to 1.29× as fast). We have not measured where the +copy's few percent go; one known cost is the counter that bounds its recursion, about ten nanoseconds per copied +container in [#5389][5389]'s measurement. Copying and comparing gained the least. + +One thing to keep in mind about `citm_catalog.json`: as distributed, it is indented, and 71 % of its 1.7 MB are +whitespace. Several changes below help exactly that. A minified copy of it parses 1.9× as fast as with 3.12.0 in our +timeline, against 3.3× for the file as distributed. + +Eight themes did most of the work: + +1. [Ask the input what it is](#1-ask-the-input-what-it-is), instead of reading every source one character at a time. +2. [Work on words, not bytes](#2-work-on-words-not-bytes): 8 bytes per step with plain integer arithmetic, 16 with + SSE2 on x86-64 and NEON on 64-bit Arm. +3. [Own the number conversions](#3-own-the-number-conversions): no `strtod`, no locale, no `snprintf`. +4. [Don't do work nobody reads](#4-dont-do-work-nobody-reads): positions, copies and conversions that only matter on + an error or for a rare caller. +5. [Read the assembly](#5-read-the-assembly): stack canaries, missed inlining and failed store forwarding. +6. [Write through a cursor](#6-write-through-a-cursor): no virtual call per character, no allocation per call, no + branch per integer. +7. [Fix the algorithm first](#7-fix-the-algorithm-first): seven operations were quadratic or worse. +8. [Build a different data structure](#8-build-a-different-data-structure) when you only want to read. + +## 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 +every commit on `develop` that changed `json.hpp` or `json_view.hpp` since 3.12.0, 315 of them, and ran all of them interleaved +(details in [How we measured](#how-we-measured)). Each panel shows the speedup over 3.12.0 after each commit; the +labels name the pull requests behind the largest steps. Each point is one build, and moving code around changes a +build's speed by a few percent on its own, so only the larger steps mean something. When `accept()` of +`twitter.json` got 2.5 % slower over the last 20 commits, we timed the two ends against each other in one binary +with six code layouts: they were equal within 0.2 %. + +
+
+--8<-- "images/perf_blog_timeline_parse_citm.svg" +
+
+--8<-- "images/perf_blog_timeline_parse_canada.svg" +
+
+--8<-- "images/perf_blog_timeline_accept_twitter.svg" +
+
+--8<-- "images/perf_blog_timeline_parse_twitter_ifstream.svg" +
+
+--8<-- "images/perf_blog_timeline_dump_twitter.svg" +
+
+--8<-- "images/perf_blog_timeline_dump_canada.svg" +
+
+--8<-- "images/perf_blog_timeline_from_cbor_twitter.svg" +
+
+--8<-- "images/perf_blog_timeline_to_cbor_twitter.svg" +
+
+ +A few things stand out: + +- **There is no single big step.** Parsing `citm_catalog.json` got faster in about a dozen visible steps; the largest + are a buffer that only error messages read ([#5234][5234], −35 %), the contiguous-input fast paths + ([#5283][5283], −20 %), and two consecutive PRs that each took 12 %: positions derived only on error + ([#5809][5809]) and skipping indentation eight bytes at a time ([#5810][5810]). Most other PRs each moved it by 2 + to 7 %. +- **Writing had a few large steps.** `dump()` of `twitter.json` lost 70 % of its time in one PR ([#5285][5285]: the + write buffer and the bulk string copy), and `dump()` of `canada.json` 65 % in another ([#5634][5634]: Żmij). +- **Some steps come from outside.** Two of the larger ones were contributed: [#5592][5592] by + [@mvanslobbe](https://github.com/mvanslobbe) checks for the expected `:` or `,` before the lexer's token switch + (−7 % on `citm_catalog.json`), and [#5328][5328] by [@alexprabhat99](https://github.com/alexprabhat99) stopped + creating a temporary `basic_json` for every object key that `to_cbor()` writes (−55 %). +- **Regressions happen in between.** [#5185][5185] added UTF-8 validation of every text string to the binary + readers, byte by byte. `from_cbor()` of `twitter.json` then took 37 % longer for 66 commits, until + [#5746][5746] (with [#5741][5741]) made the readers lenient again by default, as in 3.12.0, and the check an + option. That decision was about the format specifications, which don't require a decoder to check, not about + speed; the cost went with it. It never shipped in a release. +- **The parse panels include destroying the result.** The benchmark returns `json::parse(text).size()`, so the + destructor PRs ([#5762][5762], [#5835][5835]) show up in the parse panels too, as steps of up to 6 % in either + direction. +- **The input decides how big a step is.** #5234 measured 8 % on its own benchmark; on the indented + `citm_catalog.json` the same change saved 35 %, because every space went through the buffer it removed. + +## 1. Ask the input what it is + +This is the function the 3.12.0 lexer called for **every** byte of input: + +```cpp +char_int_type get() +{ + ++position.chars_read_total; + ++position.chars_read_current_line; + + if (next_unget) + { + // just reset the next_unget variable and work with current + next_unget = false; + } + else + { + current = ia.get_character(); + } + + if (JSON_HEDLEY_LIKELY(current != char_traits::eof())) + { + token_string.push_back(char_traits::to_char_type(current)); + } + + if (current == '\n') + { + ++position.lines_read; + position.chars_read_current_line = 0; + } + + return current; +} +``` + +Two counters in memory, a branch for a pending `unget()`, a call into the input adapter, a `push_back()` into a +buffer that only an error message reads, and a branch for line breaks. A string or a number then copied each byte a +second time into the token buffer. That is a clean design: one interface works for a `std::string`, an `std::istream`, +a `FILE*`, a pair of iterators over a `std::list`, or a user's own adapter. But it makes every input pay for the +most limited one. + +3.13.0 asks the input what it can do and picks a path at compile time: + +```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::istream"| w["window:
scan the streambuf's get area"] + q -->|"FILE*"| f["one lock per parse,
unlocked reads"] + q -->|"other iterators,
user adapters"| b["byte at a time,
as before"] +``` + +

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.

+ +**Contiguous input** ([#5283][5283]). `std::string`, `std::vector` and the like now go through a pointer-based +adapter in every C++ standard, not only C++20. On top of that, strings, numbers and runs of whitespace are scanned in +bulk (themes 2 and 3). Every fast path defers to the byte path on anything malformed, so error messages and `byte` +positions stay the same, with one deliberate correction (the column after a number at the end of a line); the PR +checked that on 481,000 documents. + +**Positions only when someone asks** ([#5809][5809]). For seekable input, the lexer no longer counts lines and +columns while it reads. The byte offset is the distance from the start; lines and columns are recomputed from the +consumed input when an error message needs them. This is what serde_json's slice reader does. On an M1 Max, it made +parsing `citm_catalog.json` 11.8 % faster and validating an indented `twitter.json` 22 % faster. + +**Streams** ([#5828][5828], [#5884][5884]). An `std::istream` already has a buffer: the get area of its `streambuf` +(`gptr()` to `egptr()`). The lexer now scans that window in bulk, keeps the current token as a pointer into it, and +copies only the part that is about to be refilled. Nothing is read ahead, so the stream is left exactly where it was +before. Reaching the protected get area takes no cast: member pointers formed in a class derived from +`std::streambuf`, so the access rule for protected members applies as intended. Before, parsing from an +`std::ifstream` took 1.8 to 2.5 times as long as reading the file into a string first; after #5828, it takes 11 to +18 % longer. + +**`FILE*`** ([#5828][5828]). `std::fgetc` locks the `FILE` on every call. 3.13.0 locks it once per parse +(`flockfile`, then `getc_unlocked`; `_lock_file` and `_fgetc_nolock` with the Microsoft runtime). On macOS, this made +parsing `citm_catalog.json` from a `FILE*` 4.4 times as fast (34.1 ms to 7.8 ms). With glibc the gain was only 1.1 to +1.3×, because glibc's per-call locking is already cheap. The lock is held for the whole parse, which the documentation +now mentions, and both changes have opt-out macros. + +## 2. Work on words, not bytes + +The core of a JSON lexer is "copy bytes until something interesting happens". In a string, the interesting bytes are +the closing quote, a backslash, a control character, and anything above 0x7F (UTF-8). This test checks eight bytes at +once with ordinary 64-bit arithmetic: + +```cpp +// return a word whose high bit is set in every byte of v that needs +// individual handling; zero if all 8 bytes can be copied as they are +inline std::uint64_t swar_string_special(std::uint64_t v) noexcept +{ + constexpr std::uint64_t ones = 0x0101010101010101ull; + constexpr std::uint64_t high = 0x8080808080808080ull; + const std::uint64_t q = v ^ 0x2222222222222222ull; // '"' + const std::uint64_t b = v ^ 0x5C5C5C5C5C5C5C5Cull; // '\\' + const std::uint64_t has_quote = (q - ones) & ~q & high; + const std::uint64_t has_backslash = (b - ones) & ~b & high; + const std::uint64_t has_control = (v - 0x2020202020202020ull) & ~v & high; // < 0x20 + const std::uint64_t has_non_ascii = v & high; // >= 0x80 + return has_quote | has_backslash | has_control | has_non_ascii; +} +``` + +XOR with the quote turns every quote byte into zero, and `(x - 0x01…01) & ~x & 0x80…80` sets the high bit of the +bytes that were zero: the "determine if a word has a zero byte" trick from Sean Eron Anderson's +[Bit Twiddling Hacks](https://graphics.stanford.edu/~seander/bithacks.html#ZeroInWord). When the result is not +zero, the number of trailing zero bits divided by eight is the index of the first special byte. The subtraction can +also flag a byte *after* a real hit (a borrow), but never one before it, so the lowest flag is always right. + +
+--8<-- "images/perf_blog_swar.svg" +
+ +The same kind of word test, written in standard C++11 with a `memcpy` load and a fallback for big-endian targets, +now runs in many places: + +| where | what it skips | PR | +| --- | --- | --- | +| lexer: strings | runs of ordinary characters, and runs of well-formed multi-byte UTF-8 | [#5283][5283], [#5738][5738] | +| lexer: indentation | eight spaces per step after a newline, an idea from Glaze's `skip_matching_ws` | [#5810][5810] | +| lexer: numbers | eight digits per step, as fast_float does | [#5738][5738], [#5851][5851] | +| `dump()` | runs that need no escaping, with and without `ensure_ascii` | [#5285][5285] | +| `json_view` `dump()` | ASCII runs with `ensure_ascii` | [#5879][5879] | + +Where a vector unit is part of the baseline instruction set (SSE2 on x86-64, NEON on 64-bit Arm), the string paths +use 16 bytes per step ([#5853][5853], [#5628][5628]). Non-ASCII text is validated 16 bytes at a time with simdjson's +"lookup4" UTF-8 check (Keiser and Lemire, 2021). That is the one place that needs more than the baseline: on x86-64 +it uses SSSE3, which is checked once per process with `cpuid`. `JSON_NO_SIMD` turns all vector code off. On +`twitter.json`, #5853 made `json::parse()` 5 % faster with Clang and 9 % with GCC on an M1 Max; on long strings, 19 +to 24 %. + +**Short strings are a different problem.** Most keys and many values are short: `citm_catalog.json` averages 8 bytes +per string and `jeopardy.json` 14. For them, setting up a scan costs more than the scan. [#5878][5878] took the idea +of SQLite's `jsonAppendString()`: check each byte of a string of up to 32 bytes against a 256-entry table of bytes +that can be written unchanged, and copy the string in one go if they all pass. 32 bytes was the best threshold on the +M1; 16 gained less, and 64 lost again. [#5883][5883] then replaced the table, where SSE2 or NEON is available, with two +overlapping loads: a string of 16 to 32 characters is two 16-byte vectors that overlap in the middle, tested once and +stored with two stores, together with its quotes. That cut the instructions of `twitter.json`'s `dump()` by 21 %. +The table stays as the fallback for other targets. + +**Escapes come from a table, too.** When the scanner stops at a quote, a backslash or a control character, +[#5822][5822] copies its escape sequence from a table of 8-byte rows (padded with zeros, length in the last byte) +straight into the write buffer: one 8-byte copy and one add, an idea from orjson's `QUOTE_TAB`. On an M1 Max, that +made dumping 100,000 log lines full of `\n`, `\t` and quotes 23 % faster. Most of that, the PR found, comes from +writing straight into the write buffer instead of going through an intermediate one; the table alone gained 2 to +7 %. + +!!! note "Loop shape beats branch count" + + Not every change in this direction worked the first time. The first version of [#5490][5490] removed one + branch per whitespace byte and made deeply nested `dump(4)` output parse 3.2 times *slower* with Clang. The + rewritten loop no longer let either compiler keep the input's read and end pointers in registers, so both + reloaded them from memory on every character. Restoring the original `do`-`while` shape, guarded by an `if`, + fixed it. + +## 3. Own the number conversions + +3.12.0 converted every floating-point number with `strtod` and every integer with `strtoull` or `strtoll`, after +copying the token into a buffer. `strtod` is correct, but general: it consults the locale for the decimal point, sets +`errno`, and handles any length of input. + +`canada.json`, a GeoJSON file of 111,126 numbers, shows what that costs. 108,528 of its numbers have 16 to 19 +significant digits, like `-65.613616999999977`. The classic shortcut, Clinger's fast path, needs the digits to fit +into the 53 bits of a `double` (at most 9,007,199,254,740,992) and the power of ten to be exact (at most 10²²): then a +single multiplication or division is correctly rounded. For most of `canada.json`'s numbers, the digits don't fit. + +3.13.0 converts numbers itself, without the C library and without the locale: + +```mermaid +flowchart TB + t["number token"] --> d{{"at most 19
significant digits?"}} + d -->|"yes"| c{{"digits and power of ten
exact as double?"}} + c -->|"yes"| cl["Clinger's fast path:
one multiplication or division"] + c -->|"no"| el["Eisel-Lemire:
128-bit integer arithmetic"] + d -->|"no"| el2["Eisel-Lemire on the first 19 digits"] + el2 --> amb{{"w and w+1 round
to the same double?"}} + amb -->|"yes"| done(["done"]) + amb -->|"no"| big["compare with the exact midpoint
(big-integer arithmetic)"] +``` + +

How 3.13.0 converts a decimal token to a `double`. Of `canada.json`'s 111,080 floating-point numbers, 9,742 take Clinger's fast path and 101,338 Eisel-Lemire; none has more than 19 digits.

+ +The Eisel-Lemire algorithm is adapted from fast_float's `compute_float` ([#5617][5617]), and it gives the correctly +rounded result for up to 19 digits without a fallback (Mushtak and Lemire, 2023). For longer numbers, +[#5738][5738] added fast_float's digit comparison. The table of 128-bit powers of five is fast_float's too. Then +[#5851][5851] stopped scanning the digits twice: the lexer now accumulates the value while it validates the token, +eight digits at a time with fast_float's `is_eight_digits` and `parse_eight_digits`, and a float of up to 19 digits +goes straight into the conversion with no call left on the way. + +For output, 3.12.0 used Grisu2, which is fast but sometimes writes a 17th digit where 16 suffice. +[#5634][5634] ported [Żmij](https://github.com/vitaut/zmij) by Victor Zverovich to C++11. It writes the shortest +decimal that reads back as the same `double`, and the closest one if there are several. Its digit conversion, by +Xiang JunBo, turns eight digits into text per step, and the SIMD sequence by Dougall Johnson formats all 16 digits +in one vector register, decimal point included. The PR found one more stall on the way: the old code stored the +digits and immediately loaded them again shifted by one byte, and a load that overlaps two smaller stores can't be +forwarded from the store buffer. On an M1 Max, `dump()` of `canada.json` went from 6,987 µs to 2,405 µs (−66 %), and +on an x86-64 Haswell machine by −70 % (GCC 13) and −72 % (Clang 18). + +That one is not free of visible change. Of one million random `double` bit patterns, 1,425 (0.14 %) are now written +differently: 810 with fewer digits (`5.3165205877497296e+16` becomes `5.31652058774973e+16`), 615 with the same +number of digits but the closer ones. They all read back as the same `double`, and none of one million short +decimals changes. In `canada.json`, 69 of the 111,126 numbers come out differently. + +## 4. Don't do work nobody reads + +A lot of what the 3.12.0 parser did was correct, cheap per call, and read by nobody in the common case. + +- **The text of float tokens** ([#5852][5852]). The lexer copied every float token into a buffer so that a SAX + handler's `number_float(val, s)` could see the original text. That copy was about 10 % of parsing `canada.json`. + The built-in handlers behind `parse()` and `accept()` never look at it, so they now get an empty string, and the + lexer copies the text only for user handlers that might. +- **The token buffer** ([#5234][5234]). `get()` pushed every character into `token_string`, a buffer that only the + "last read" part of an error message uses. For contiguous input, the lexer now remembers where the token started and + rebuilds that text from the input if an error happens. The PR measured 8 % on its own benchmark; in the timeline, + parsing the indented `citm_catalog.json` got 35 % faster at that commit, because every space of the indentation + went through the buffer. [#5828][5828] did the same for streams, where the copy was about 20 % of the remaining + cost. +- **Positions** (#5809, above): lines and columns are counted only when an error message needs them. +- **Copies that should have been moves.** `from_cbor()` and the other binary readers ended with + `return res ? result : basic_json(value_t::discarded);`. The conditional operator's operand is an lvalue, so there + is no implicit move: every successfully parsed value was deep-copied once and then destroyed. Returning it by move + ([#5501][5501]) took decoding a 2 MB CBOR document from 26.99 ms to 14.65 ms. +- **`ordered_json` growth** ([#5609][5609]). `ordered_json` stores its members in a + `std::vector>`. Because the key is `const`, the pair's move constructor copies + the key and can throw, so `std::vector` copies every element when it grows, including the whole subtree of every + value. The library now grows that storage itself: keys copied, values moved. Parsing `twitter.json` into + `ordered_json` dropped from 60,456 to 19,182 allocations and from 3.20 ms to 1.70 ms. +- **Members that are inserted one at a time** ([#5882][5882]). The DOM parser now allocates each array once, at its + final size, and collects the members of an `ordered_json` object until the object is complete; big objects get + a hash table for the duplicate-key check. An object with very many members went + from 43 million instructions to 1.9 million. Since then, `ordered_json` parses 1 to 15 % *faster* than `json` in our suite, whose `std::map` still allocates a node per member. + +!!! note "The same idea, measured twice" + + Moving the key out of the lexer's buffer instead of copying it sounds like a free win. In September, + [#5458][5458] measured it: 3.5 % faster on `citm_catalog.json`, but 1.8 % slower on `jeopardy.json` and 11.9 % + slower on 40,000 short strings, so only string values were moved. In October, with much of the lexer around it + rewritten, [#5855][5855] measured again: `parse()` of `twitter.json` got 3.3 % faster, so keys are moved + now too. An idea that lost once is worth measuring again when the code around it changes. + +## 5. Read the assembly + +A good part of 3.13.0 came from reading the code the compiler produced for the hot loops, with Clang's +`-Rpass-missed=inline`, profiles, and instruction counts. A few things that turned up: + +- **A stack canary on every token** ([#5854][5854]). The function that dispatches on the first byte of a token built + the expected text of `true`, `false` and `null` in local `std::array`s. With `-fstack-protector-strong`, the + default on Ubuntu, any local array gives the whole function a stack canary that is set and checked on every call: + the canary's load alone was 11 % of the function's samples. Now the literal is passed as a string literal with its + length as a template parameter. The same PR replaced the parser's `std::vector` nesting stack, whose + `push_back` GCC called out of line, with `std::vector`. +- **Exceptions that block inlining** ([#5838][5838], [#5840][5840], [#5839][5839]). Building an exception message + inline (`concat(...)`, `create(...)`) made functions like `get_ref`, `operator[]` and the iterator operators too big + to inline. Moving construction and `throw` into never-inlined helpers fixed 27 sites. The same for the lifecycle: + Clang reported about 380 call sites where the copy constructor, the destructor or a comparison was not inlined, + even for a scalar. With the scalar case inline and the rest out of line, `j == 5` went from 3.55 ns to 0.65 ns + with Clang. It is not free: the probe translation unit grew by 13 % at `-O2`, and with GCC, `==` on large + documents got about 7 % slower. +- **Aliasing** ([#5633][5633]). Stores through a `char*` may alias anything, so a write position kept in a member is + reloaded after every store. `json_view`'s writer keeps it in a local variable instead. +- **Inlining that disappears** ([#5878][5878]). Once `dump_escaped()` had grown, GCC 13 and 16 stopped inlining two + small helpers into the pretty-printing loop, and indented output got up to 13 % slower. They are now forced inline. +- **Code size as a cost** ([#5902][5902]). `get>()` used a pack expansion that compiled to one + function of about 28,000 instructions: a thousand inlined copies of the bounds and type checks. A loop needs 49. + +!!! note "Instruction counts, not only time" + + On the x86-64 machine we used for checking, moving code by a few bytes changed the time of unrelated benchmarks + by up to ±5 %. Many decisions in these PRs were therefore made on instruction counts (callgrind on Linux, + `/usr/bin/time -l` on macOS), checked against timings of builds with all functions and loops aligned to 64 + bytes, and timed interleaved in one binary with several code layouts. The target, applied strictly in the later + PRs, was: at least 3 % faster, and no benchmark more than 1 % slower. Some PRs accepted a small loss elsewhere + and say so, such as the 7 % on GCC's `==` above. + +## 6. Write through a cursor + +In 3.12.0, every `dump()` created its output adapter with `std::make_shared`, and the serializer wrote every +structural character through a virtual call: + +```cpp +o->write_characters("{\n", 2); +o->write_characters(indent_string.c_str(), new_indent); +o->write_character('\"'); +``` + +The writing side changed in four steps: + +```mermaid +flowchart LR + subgraph before["3.12.0"] + direction LR + d1["dump()"] --> a1["make_shared
output adapter"] --> v1["virtual call
per write"] --> s1[("std::string")] + end + subgraph after["3.13.0"] + direction LR + d2["dump()"] --> a2["adapter on
the stack"] --> b2["1 KiB buffer,
bulk append"] --> s2[("std::string")] + end +``` + +

The text serializer's output path before and after [#5285][5285] and [#5449][5449].

+ +1. **A buffer in the serializer** ([#5285][5285]): all writes go into a 1 KiB buffer that is flushed in bulk, so the + virtual call happens once per KiB. +2. **No allocation per call** ([#5449][5449]): the adapter lives on the stack. Together with an indentation string + that [#5285][5285] creates only when needed, a compact `dump()` of a small object went from 4 heap allocations to + 1, the returned string. +3. **Binary writers get a concrete sink** ([#5286][5286]): `to_cbor()` and friends write into a `std::vector` through + a value-type sink, with no virtual call. The same PR replaced `std::reverse` for byte order with + `__builtin_bswap*`, after finding that Clang had emitted no `bswap` instruction at all in the CBOR number path. +4. **A cursor instead of `vector::insert()`** ([#5824][5824], [#5885][5885]). After step 3, writing an array of + integers as CBOR was still no faster than in 3.12.0, and slower with GCC. The profile said why: each number's + one to eight bytes were appended with `vector::insert()`, an out-of-line insert-anywhere routine, which took 30 % + of the time. The sink now reserves space ahead and writes through a pointer, and runs of numbers are encoded in a + loop without a call per element. + +The last step also took the branches out of encoding an integer. The size of each integer in a stream is hard to +predict, so the encoder looks the size class up from the number of leading zero bits and stores the head as one byte +plus one 8-byte word, of which the caller keeps only as many bytes as the class needs: + +```cpp +// 1, 2, 3 or 4 if x fits 1, 2, 4 or 8 bytes, without a branch +static unsigned integer_size_class(const std::uint64_t x) noexcept +{ + static constexpr std::array size_class_by_leading_zeros = { /* ... */ }; + return size_class_by_leading_zeros[count_leading_zeros(x | 1u)]; +} + +std::size_t encode_first_byte_and_tail(CharType* out, std::uint8_t first_byte, + unsigned size_class, std::uint64_t value) const noexcept +{ + out[0] = to_char_type(first_byte); + const unsigned unused_bits = 64u - ((8u << size_class) >> 1u); + const std::uint64_t tail = is_little_endian ? byteswap(value) >> unused_bits : value << unused_bits; + std::memcpy(out + 1, &tail, sizeof(tail)); // always 8 bytes; the caller keeps the first 0, 1, 2, 4 or 8 + return ((std::size_t{1} << size_class) >> 1u) + 1; +} +``` + +Against 3.12.0, writing an array of small signed integers as CBOR went from 0.97× to 4.19× with Clang on an M1 Max, +and from 0.94× to 2.77× with GCC 13 on x86-64 ([#5824][5824]). + +## 7. Fix the algorithm first + +None of the micro-optimizations above matter much next to an algorithm that is quadratic. These are the ones found and +fixed in this release cycle, measured on the same machine: + +| operation | 3.12.0 | 3.13.0 | speedup | cause | PR | +| --- | --: | --: | --: | --- | --- | +| `diff()` of an array of 64,000 numbers against an empty array | 4,102 ms | 9.98 ms | 411× | each `remove` was inserted at the front of the patch | [#5461][5461] | +| `diff()` of an array of 20,000 objects against every second one of them | 119 ms | 3.85 ms | 31× | the same | [#5461][5461] | +| `parse()` with a callback that rejects half of 32,000 objects | 1,334 ms | 11.5 ms | 116× | each rejected value was searched for from the start of its parent | [#5457][5457] | +| `<` on two equal values nested 24 levels deep (C++11 to C++17) | 62.6 ms | 0.002 ms | exponential | `std::vector`'s `<` asks `a < b` and `b < a`, and both recurse | [#5390][5390] | +| `<` on `citm_catalog.json` and a copy, 10× (C++11 to C++17) | 274 ms | 3.81 ms | 72× | the same | [#5390][5390] | +| `flatten()` of `citm_catalog.json` as `ordered_json` | 253 ms | 1.37 ms | 184× | every new key was looked up linearly | [#5834][5834] | +| `to_bson()` of a value nested 200 levels deep, 100× | 19.5 ms | 0.566 ms | 34× | each document's size was computed by walking everything below it again | [#5553][5553] | +| 100,000 × `push_back()` on an `ordered_json` with `JSON_DIAGNOSTICS` | 1,514 ms | 4.98 ms | 304× | every insert re-pointed the parent pointers of all elements | [#5832][5832] | +| 10,000 × `at(i)` on an `ordered_json` array with `JSON_DIAGNOSTICS` | 43.2 ms | 0.081 ms | 533× | the same | [#5832][5832] | + +
+--8<-- "images/perf_blog_cliffs.svg" +
+ +The `<` case matters to anyone who keeps `json` values in a `std::set`, uses them as `std::map` keys, or sorts them +before C++20. Each level of nesting doubled the work: two equal values nested 30 levels deep took 3.8 seconds to +compare in [#5390][5390]'s measurement, and nothing about such a value is unusual. Since C++20, `<` is derived from +`<=>`, which asks once. + +The other side of this theme is recursion. A value nested 100,000 levels deep used to crash `dump()`, the copy +constructor, `==`, `hash`, `diff()`, `flatten()` and the binary writers with a stack overflow. The obvious fix, +making every walk iterative, measured 2 % to 20 % slower on ordinary documents ([#5285][5285]), 3 % to 9 % for +copying ([#5389][5389]), and 10 % to 14 % for merging ([#5547][5547]). RapidJSON keeps recursion as its default for +the same reason. 3.13.0 therefore bounds the recursion instead: the first 128 levels run the old recursive code, and +only below that does an explicit stack take over. A document nested less deeply pays only for counting the levels. + +Destruction got its own trick ([#5762][5762]). Instead of a heap-allocated stack, the destructor uses pointer +reversal, the idea behind the Schorr-Waite graph marking algorithm: while it empties a container, that container's +last child slot stores the link back to its parent. Destroying any value now allocates nothing and never recurses. +Against 3.12.0, destroying a parsed document is 1.25 to 1.56 times as fast in our suite, and destroying an array of +one million small objects 1.39 times (125 ms to 90 ms). + +## 8. Build a different data structure + +After all of the above, `json::parse()` is still bound by what it builds: one heap allocation per string, array and +object, and a `std::map` node per member. Many programs only parse a document, read some fields and throw it away. +For them, 3.13.0 adds [`json_document`](../../features/json_view.md) and `json_view` in a new header, +``. + +
+--8<-- "images/json_view_layout.svg" +
+ +

Top: the tree of separate heap allocations that `json::parse()` builds. Bottom: `json_document::parse()` keeps the source text and builds one flat index of 16-byte nodes that point back into it, plus a buffer of decoded strings.

+ +The design is not ours either. simdjson's tape and yyjson's immutable document are both flat arrays of fixed-size +entries in document order instead of trees of separate allocations, and yyjson's entries are 16 bytes, too. What +`json_document` does: + +- **One 16-byte node per value and per key, in document order** ([#5620][5620]). Strings stay in the input; only + strings with escapes are decoded into an arena. Integers are converted while their digits are in the cache. A + container stores the size of its subtree, so a reader can step over it in constant time. A document makes a + handful of allocations, however many values it has. +- **Floats stay text until you read them.** The parser records where the integer digits, fraction and exponent are, + and converts on access with the same Clinger/Eisel-Lemire code as `json::parse`. We prototyped converting them + during the parse and dropped it ([#5908][5908]): reading every number of `canada.json` got 4 to 7 % faster, but + parsing it 36 to 90 % slower. +- **SIMD where the baseline has it** ([#5628][5628]): strings scanned 16 bytes at a time with SSE2 or NEON, and + non-ASCII text validated with simdjson's lookup4 check. `\u` escapes are decoded with a table, as yyjson's + `read_hex_u16` does. +- **A hash index for big objects** ([#5628][5628]): an object with 128 or more members gets an open-addressing + table after parsing, an idea from Boost.JSON. Looking up every key of a 10,000-member object went from 59.8 ms to + 0.16 ms. The hash is not seeded, so the probe length is capped; if crafted keys exceed the cap, that object falls + back to a linear search, and parsing can't become quadratic. +- **First duplicate wins.** Lookups return the first of duplicate keys, as yyjson and simdjson do, because + returning the last one meant scanning every object to its end and made lookups in small objects 1.6 to 3.4 times + slower ([#5622][5622]). + +Parse times in µs on the M1 Max (C++17, `-O3`, best of 30 interleaved rounds; a fresh `json_document` and yyjson +document each time, simdjson's parser reused as its documentation recommends): + +| file | `json_document` | yyjson 0.13.0 | simdjson 4.6.11 DOM | Boost.JSON 1.92.0 | `json::parse` | +| --- | --: | --: | --: | --: | --: | +| `twitter.json` (632 KB) | **182** | 192 | 216 | 866 | 1,368 | +| `citm_catalog.json` (1.7 MB) | **360** | 478 | 490 | 1,480 | 2,409 | +| `canada.json` (2.3 MB) | **960** | 1,489 | 2,098 | 2,501 | 5,890 | +| `jeopardy.json` (56 MB) | **22,687** | 30,528 | 26,875 | 88,900 | 167,664 | + +
+--8<-- "images/perf_blog_view.svg" +
+ +On the same four files, reading every value takes yyjson 1.06 to 1.30 times as long as `json_document`, writing +them back with `dump()` 1.06 to 1.35 times, and parsing, editing a few values and writing them 1.34 to 1.85 times (1.0 to 1.57 times for an edit in every +record). Integers are +already converted when you read them, and the writer copies strings and integer tokens straight from the source +([#5633][5633]). + +It is not faster everywhere, and the harness ([#5627][5627], [#5914][5914]) is built to show where: + +- **Reading a few fields.** simdjson's On-Demand API builds nothing and can only read forward. Picking a few fields + out of `twitter.json`, `canada.json` and `jeopardy.json` takes it 0.67 to 0.80 times as long as `json_document`. +- **Floats are converted later.** `json_document` converts a float when you read it; yyjson and simdjson convert + every number while parsing. For `canada.json`, which is almost all floats, the parse row therefore flatters + `json_document` (1.55× ahead of yyjson); reading every value after parsing is the fair comparison, and there it + is 1.07× ahead. +- **Other CPUs.** On the x86-64 machine with GCC, simdjson's DOM with AVX2 was 19 % ahead on parsing + `twitter.json` and 7 % on `citm_catalog.json` ([#5627][5627]), and yyjson's `dump()` of `canada.json` still is + ahead: `json_document` takes 1.4 times as long there ([#5908][5908]). Our tables are from one Apple silicon + machine. +- **Small documents.** For a 2.5 KB status message, all three engines parse in about a microsecond, and yyjson is + 6 % ahead. For a 94-byte message, the engines trade places between runs. +- **`json::parse` itself.** Boost.JSON builds a mutable tree, as `json` does, so it is the fair comparison for + `json::parse`, not for `json_document`. It is still 1.6 to 2.4 times as fast. + +## What didn't work + +The PRs record the ideas that measured worse, which is as useful as the ones that made it: + +| idea | result | where | +| --- | --- | --- | +| make every walk iterative | 2–20 % slower on ordinary documents | [#5285][5285], [#5389][5389], [#5547][5547] | +| move the key out of the lexer's reused buffer | 11.9 % slower (later a win, see above) | [#5458][5458] | +| inline `scan_literal()` | Clang's dispatch grew from 233 to 278 instructions and got slower | [#5854][5854] | +| short-string table for strings up to 64 bytes | lost against 32 | [#5878][5878] | +| leave the serializer's buffers uninitialized | timings moved 18 % apart in opposite directions on two similar workloads, so dropped; done later in [#5883][5883], where instruction counts showed 170 instructions saved per `dump()` | [#5285][5285] | +| out-of-line throw helpers for JSON Pointer lookups | no measurable gain, dropped | [#5836][5836] | +| `json_view`: an "all ASCII" flag per string | `dump()` with `ensure_ascii` faster, but parsing 1–13 % slower | [#5879][5879] | +| `json_view`: convert floats while parsing | `canada.json` parse 36–90 % slower | [#5908][5908] | +| `json_view`: note a large object inline in the parse loop | `canada.json` 37 % slower | [#5628][5628] | +| `json_view`: vector-scan every string in `set()` | 14–16 % slower on 16-byte strings | [#5926][5926] | + +## Making fast paths safe + +Fast paths are where the bugs are: they read in 8- and 16-byte steps, they skip validation that the slow path did, and +they are taken only for some inputs. A large part of these PRs is checking them: + +- **Guard pages** ([#5826][5826]): the input is copied so that its last byte is the last readable byte of a page, and + the next page is unmapped. AddressSanitizer misses an over-read into the slack of the same heap allocation, and + fuzzers rarely end an input exactly at a page boundary; this test crashes on any over-read by one byte. +- **Differential fuzzing of the input kinds** ([#5827][5827]): 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. +- **Differential checks at scale**: 6.5 million float tokens against `strtod` including 60,000 exact halfway cases + ([#5617][5617]); `json_view`'s batched float output byte-identical to the general writer for 10⁹ generated tokens + ([#5908][5908]). For this post, we checked that 3.13.0 writes the same bytes as 3.12.0 in all five binary formats + for 39 test files, and the same JSON text for `twitter.json` and `citm_catalog.json`. +- **Mutation checks**: break the fast path on purpose (skip one byte too many, mark `"` as copyable) and make sure the + tests notice. In [#5878][5878], marking the quote as copyable fails 4,551 assertions. + +## The price + +None of this is free at compile time. `json.hpp` grew from 25,526 to 45,582 lines (`json_view.hpp` is a separate +header of 11,310 lines that you only pay for if you include it). Apple clang 21 at `-O2`, median of 9 compilations: + +| translation unit | 3.12.0 | 3.13.0 | 3.13.0 with `JSON_NO_AUTOMATIC_UDLS` | +| --- | --: | --: | --: | +| only includes `json.hpp` | 528 ms | 709 ms (+34 %) | 508 ms (−4 %) | +| parses, dumps and writes CBOR | 1,175 ms | 1,885 ms (+60 %) | 1,858 ms (+58 %) | + +We have not broken the second row down by change. [`JSON_NO_AUTOMATIC_UDLS`](../../api/macros/json_no_automatic_udls.md) moves the +`_json` literals into their own header and more than makes up for the first row; the +[compile-time page](../../integration/compile_times.md) lists the other options. + +## How we measured + +**What is compared.** "3.12.0" is the released single header. "3.13.0" is `develop` at +[84c542164](https://github.com/nlohmann/json/commit/84c5421644) with seven of the maintainer's open, non-draft pull requests that touch code +merged in ([#5890][5890], [#5908][5908], [#5914][5914], [#5926][5926], [#5936][5936], [#5945][5945], +[#5947][5947]) and amalgamated. Merging #5890 onto today's `develop` left one duplicate member declaration, which we +removed; nothing else was changed. Both are compiled from the *same* benchmark source, the Google Benchmark suite in +[`tests/benchmarks`](https://github.com/nlohmann/json/tree/develop/tests/benchmarks), with the BON8 benchmarks +removed because 3.12.0 has no BON8. + +| | | +| --- | --- | +| machine | Apple M1 Max laptop on AC power, macOS 27.0.1 | +| compiler | Apple clang 21, libc++ | +| flags | `-O3 -flto -DNDEBUG`, the compiler's default standard, C++14 (the timeline and `json_view` comparisons: C++17, `-O3`, no LTO) | +| suite | 2 rounds × 4 repetitions per version, random interleaving, each round started only when the load average was below 4; the median of all 8 | +| noise | the coefficient of variation was 3 % or less for 176 of the 182 rows with 3.12.0 and 178 with 3.13.0; the largest was 12 % | +| inputs | `twitter.json`, `citm_catalog.json` and `canada.json` from [nativejson-benchmark](https://github.com/miloyip/nativejson-benchmark), a 56 MB `jeopardy.json`, and number-heavy files | + +The cliffs come from small extra programs (C++17, `-O3`, median of 3 interleaved runs). The timeline is +[`tests/benchmarks/history`](https://github.com/nlohmann/json/tree/develop/tests/benchmarks/history): one program, +compiled against `single_include` of 3.12.0, of each of the 315 commits on `develop` that changed `json.hpp` or `json_view.hpp` since, and of the candidate, 4 rounds in random order, median of +the rounds. The `json_view` comparison is +[`tests/benchmarks/json_view/compare.py`](https://github.com/nlohmann/json/tree/develop/tests/benchmarks/json_view) +with pinned downloads of yyjson 0.13.0, simdjson 4.6.11 and Boost.JSON 1.92.0, 30 interleaved rounds, best round; +every engine must produce the same values, strings and sums of numbers before anything is timed. + +**What we did not measure.** All of our numbers are from one Apple silicon laptop with Clang. The PRs quote numbers +from an x86-64 Haswell server with GCC 13 and Clang 18 where they had them; that server shares its cores with other +work, so we did not run a second set of tables there and quote its timings only where a PR did. 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 test Windows for speed at all. + +## Thanks + +Most of what is in this post was invented by other people. The libraries and papers we took ideas or code from, in +the order they appear above: + +- [serde_json](https://github.com/serde-rs/json): deriving positions from the offset only on error. +- Sean Eron Anderson's [Bit Twiddling Hacks](https://graphics.stanford.edu/~seander/bithacks.html): the zero-byte + test behind every word-at-a-time scan. +- [Glaze](https://github.com/stephenberry/glaze): skipping indentation in 8-byte steps (`skip_matching_ws`). +- [simdjson](https://github.com/simdjson/simdjson) by Daniel Lemire, Geoff Langdale, John Keiser and contributors: + the "lookup4" UTF-8 check (J. Keiser and D. Lemire, "Validating UTF-8 In Less Than One Instruction Per Byte", + 2021), and the tape that showed what a flat index can do. +- [SQLite](https://sqlite.org)'s `jsonAppendString()`: writing short strings with a table loop. +- [orjson](https://github.com/ijl/orjson)'s `QUOTE_TAB`: escape sequences as fixed-size table rows. +- [fast_float](https://github.com/fastfloat/fast_float) by Daniel Lemire and contributors: the Eisel-Lemire + algorithm, its table of powers of five, its digit comparison, and eight digits at a time (N. Mushtak and D. Lemire, + "Fast number parsing without fallback", 2023). W. D. Clinger for the fast path, M. Eisel for the algorithm. +- [Żmij](https://github.com/vitaut/zmij) by Victor Zverovich, with the digit conversion of Xiang JunBo and the SIMD + sequence of Dougall Johnson. +- [Boost.JSON](https://github.com/boostorg/json): switching to a hash table for large objects. +- [yyjson](https://github.com/ibireme/yyjson) by YaoYuan: table-driven `\u` decoding, fixed-offset unrolled checks, + and a 16-byte flat document to measure against. +- [RapidJSON](https://github.com/Tencent/rapidjson): the measured reason to keep recursion as the default. + +And the people who sent or shaped pull requests: [@gregmarr](https://github.com/gregmarr) reviewed most of them and +suggested the sized-sentinel fast path, the peek design for exact stream positions, one SIMD switch for the whole +library, and the vector check of `json_view`'s strings on edit; [@veselink1](https://github.com/veselink1) and +[@michaelsam94](https://github.com/michaelsam94) wrote the allocator and exception-safety fixes for the destructor +that [#5762][5762] took over (the non-recursive destructor itself goes back to +[#1436](https://github.com/nlohmann/json/pull/1436) by [@nickaein](https://github.com/nickaein) in 2019); +[@commit-msuyog](https://github.com/commit-msuyog) gave the 128-bit multiplication behind Eisel-Lemire MSVC +intrinsics ([#5782][5782]); [@ayush-singh-0601](https://github.com/ayush-singh-0601) started the fix for deep values in the binary writers; +[@avionicharshit-byte](https://github.com/avionicharshit-byte) made `diff()` linear for shrinking arrays +([#5461][5461]); [@mvanslobbe](https://github.com/mvanslobbe) sped up the parser's separator check ([#5592][5592]); +and [@alexprabhat99](https://github.com/alexprabhat99) removed the temporary values behind CBOR and MessagePack +object keys ([#5328][5328]). +`twitter.json`, `citm_catalog.json` and `canada.json` come from Milo Yip's +[nativejson-benchmark](https://github.com/miloyip/nativejson-benchmark). + +[5185]: https://github.com/nlohmann/json/pull/5185 +[5234]: https://github.com/nlohmann/json/pull/5234 +[5283]: https://github.com/nlohmann/json/pull/5283 +[5285]: https://github.com/nlohmann/json/pull/5285 +[5286]: https://github.com/nlohmann/json/pull/5286 +[5328]: https://github.com/nlohmann/json/pull/5328 +[5389]: https://github.com/nlohmann/json/pull/5389 +[5390]: https://github.com/nlohmann/json/pull/5390 +[5449]: https://github.com/nlohmann/json/pull/5449 +[5457]: https://github.com/nlohmann/json/pull/5457 +[5458]: https://github.com/nlohmann/json/pull/5458 +[5461]: https://github.com/nlohmann/json/pull/5461 +[5490]: https://github.com/nlohmann/json/pull/5490 +[5501]: https://github.com/nlohmann/json/pull/5501 +[5547]: https://github.com/nlohmann/json/pull/5547 +[5553]: https://github.com/nlohmann/json/pull/5553 +[5592]: https://github.com/nlohmann/json/pull/5592 +[5609]: https://github.com/nlohmann/json/pull/5609 +[5617]: https://github.com/nlohmann/json/pull/5617 +[5620]: https://github.com/nlohmann/json/pull/5620 +[5622]: https://github.com/nlohmann/json/pull/5622 +[5627]: https://github.com/nlohmann/json/pull/5627 +[5628]: https://github.com/nlohmann/json/pull/5628 +[5633]: https://github.com/nlohmann/json/pull/5633 +[5634]: https://github.com/nlohmann/json/pull/5634 +[5738]: https://github.com/nlohmann/json/pull/5738 +[5741]: https://github.com/nlohmann/json/pull/5741 +[5746]: https://github.com/nlohmann/json/pull/5746 +[5762]: https://github.com/nlohmann/json/pull/5762 +[5809]: https://github.com/nlohmann/json/pull/5809 +[5810]: https://github.com/nlohmann/json/pull/5810 +[5822]: https://github.com/nlohmann/json/pull/5822 +[5824]: https://github.com/nlohmann/json/pull/5824 +[5826]: https://github.com/nlohmann/json/pull/5826 +[5827]: https://github.com/nlohmann/json/pull/5827 +[5828]: https://github.com/nlohmann/json/pull/5828 +[5832]: https://github.com/nlohmann/json/pull/5832 +[5834]: https://github.com/nlohmann/json/pull/5834 +[5835]: https://github.com/nlohmann/json/pull/5835 +[5836]: https://github.com/nlohmann/json/pull/5836 +[5838]: https://github.com/nlohmann/json/pull/5838 +[5839]: https://github.com/nlohmann/json/pull/5839 +[5840]: https://github.com/nlohmann/json/pull/5840 +[5851]: https://github.com/nlohmann/json/pull/5851 +[5852]: https://github.com/nlohmann/json/pull/5852 +[5853]: https://github.com/nlohmann/json/pull/5853 +[5854]: https://github.com/nlohmann/json/pull/5854 +[5855]: https://github.com/nlohmann/json/pull/5855 +[5878]: https://github.com/nlohmann/json/pull/5878 +[5879]: https://github.com/nlohmann/json/pull/5879 +[5882]: https://github.com/nlohmann/json/pull/5882 +[5883]: https://github.com/nlohmann/json/pull/5883 +[5884]: https://github.com/nlohmann/json/pull/5884 +[5885]: https://github.com/nlohmann/json/pull/5885 +[5890]: https://github.com/nlohmann/json/pull/5890 +[5902]: https://github.com/nlohmann/json/pull/5902 +[5908]: https://github.com/nlohmann/json/pull/5908 +[5914]: https://github.com/nlohmann/json/pull/5914 +[5926]: https://github.com/nlohmann/json/pull/5926 +[5936]: https://github.com/nlohmann/json/pull/5936 +[5945]: https://github.com/nlohmann/json/pull/5945 +[5947]: https://github.com/nlohmann/json/pull/5947 diff --git a/docs/mkdocs/docs/images/perf_blog_cliffs.svg b/docs/mkdocs/docs/images/perf_blog_cliffs.svg new file mode 100644 index 000000000..4fb0ac1a5 --- /dev/null +++ b/docs/mkdocs/docs/images/perf_blog_cliffs.svg @@ -0,0 +1,67 @@ + +Operations that grew quadratically or worse: time with 3.12.0 and 3.13.0, log scale +Dot plot on a logarithmic time axis. diff() 64k numbers vs []: 4102 ms to 9.98 ms; diff() 20k objects vs half: 119 ms to 3.85 ms; parse() callback, 32k objects: 1334 ms to 11.5 ms; < nested 24 deep: 62.6 ms to 0.002 ms; < citm, 10x: 274 ms to 3.81 ms; flatten() citm, ordered_json: 253 ms to 1.37 ms; to_bson() 200 deep, 100x: 19.5 ms to 0.566 ms; push_back() 100k, diagnostics: 1514 ms to 4.98 ms; at(i) 10k, diagnostics: 43.2 ms to 0.081 ms. + +1 µs + +10 µs + +0.1 ms + +1 ms + +10 ms + +0.1 s + +1 s + +10 s +3.12.0 +3.13.0 +diff() 64k numbers vs [] + +diff() 64k numbers vs [], 3.12.0: 4102 ms +diff() 64k numbers vs [], 3.13.0: 9.98 ms +411× +diff() 20k objects vs half + +diff() 20k objects vs half, 3.12.0: 119 ms +diff() 20k objects vs half, 3.13.0: 3.85 ms +31× +parse() callback, 32k objects + +parse() callback, 32k objects, 3.12.0: 1334 ms +parse() callback, 32k objects, 3.13.0: 11.5 ms +116× +< nested 24 deep + +< nested 24 deep, 3.12.0: 62.6 ms +< nested 24 deep, 3.13.0: 0.002 ms +31300× +< citm, 10x + +< citm, 10x, 3.12.0: 274 ms +< citm, 10x, 3.13.0: 3.81 ms +72× +flatten() citm, ordered_json + +flatten() citm, ordered_json, 3.12.0: 253 ms +flatten() citm, ordered_json, 3.13.0: 1.37 ms +185× +to_bson() 200 deep, 100x + +to_bson() 200 deep, 100x, 3.12.0: 19.5 ms +to_bson() 200 deep, 100x, 3.13.0: 0.566 ms +34× +push_back() 100k, diagnostics + +push_back() 100k, diagnostics, 3.12.0: 1514 ms +push_back() 100k, diagnostics, 3.13.0: 4.98 ms +304× +at(i) 10k, diagnostics + +at(i) 10k, diagnostics, 3.12.0: 43.2 ms +at(i) 10k, diagnostics, 3.13.0: 0.081 ms +533× + diff --git a/docs/mkdocs/docs/images/perf_blog_overview.svg b/docs/mkdocs/docs/images/perf_blog_overview.svg new file mode 100644 index 000000000..aaa35f4a5 --- /dev/null +++ b/docs/mkdocs/docs/images/perf_blog_overview.svg @@ -0,0 +1,87 @@ + +Speedup of 3.13.0 over 3.12.0 per workload, Apple M1 Max, Apple clang +Horizontal bars, longer is faster. twitter.json 3.0x; citm_catalog.json 4.1x; canada.json 3.5x; canada.json, indented 6.7x; citm, ordered_json 6.6x; twitter, std::ifstream 2.5x; citm, FILE* 4.8x; citm, accept() 7.0x; dump() twitter 6.7x; dump() canada 3.0x; dump(4) citm 4.6x; to_cbor() twitter 4.5x; twitter, memory 3.1x; twitter, FILE* 6.3x; diff() canada 10x; iterate canada 2.7x; copy + destroy citm 1.11x; == citm 1.04x. +Parsing +Writing +Reading C​BOR +Values + +1× + +2× + +3× + +4× + +5× + +6× + +7× + +8× + +9× + +10× + +11× +twitter.json +twitter.json: 3.0× +3.0× +citm_catalog.json +citm_catalog.json: 4.1× +4.1× +canada.json +canada.json: 3.5× +3.5× +canada.json, indented +canada.json, indented: 6.7× +6.7× +citm, ordered_json +citm, ordered_json: 6.6× +6.6× +twitter, std::ifstream +twitter, std::ifstream: 2.5× +2.5× +citm, FILE* +citm, FILE*: 4.8× +4.8× +citm, accept() +citm, accept(): 7.0× +7.0× +dump() twitter +dump() twitter: 6.7× +6.7× +dump() canada +dump() canada: 3.0× +3.0× +dump(4) citm +dump(4) citm: 4.6× +4.6× +to_cbor() twitter +to_cbor() twitter: 4.5× +4.5× +twitter, memory +twitter, memory: 3.1× +3.1× +twitter, FILE* +twitter, FILE*: 6.3× +6.3× +diff() canada +diff() canada: 10× +10× +iterate canada +iterate canada: 2.7× +2.7× +copy + destroy citm +copy + destroy citm: 1.11× +1.11× +== citm +== citm: 1.04× +1.04× + + +speedup over 3.12.0 (the line at 1× is 3.12.0) + diff --git a/docs/mkdocs/docs/images/perf_blog_swar.svg b/docs/mkdocs/docs/images/perf_blog_swar.svg new file mode 100644 index 000000000..506c2dfc9 --- /dev/null +++ b/docs/mkdocs/docs/images/perf_blog_swar.svg @@ -0,0 +1,60 @@ + +Finding the end of a run of ordinary string characters eight bytes at a time +The eight bytes w, i, t, t, e, r, quote and comma are loaded as one word. The test sets the high bit only in byte 6, the quote. Counting trailing zero bits gives 55, divided by eight gives 6: bytes 0 to 5 are copied at once, then the quote ends the string. +eight input bytes, loaded as one 64-bit word (byte 0 is the lowest) + +w +0x77 +byte 0 + +i +0x69 +byte 1 + +t +0x74 +byte 2 + +t +0x74 +byte 3 + +e +0x65 +byte 4 + +r +0x72 +byte 5 + + +" +0x22 +byte 6 + +, +0x2C +byte 7 +swar_string_special(): the high bit of each byte that needs handling + +0x00 + +0x00 + +0x00 + +0x00 + +0x00 + +0x00 + + +0x80 + +0x00 +count_trailing_zeros(0x0080000000000000) = 55 +55 / 8 = 6: copy bytes 0 to 5 in one go, then handle the quote +No branch per byte: a few subtractions, ANDs and ORs per word, +then one count of trailing zeros. + diff --git a/docs/mkdocs/docs/images/perf_blog_timeline_accept_twitter.svg b/docs/mkdocs/docs/images/perf_blog_timeline_accept_twitter.svg new file mode 100644 index 000000000..a274c4377 --- /dev/null +++ b/docs/mkdocs/docs/images/perf_blog_timeline_accept_twitter.svg @@ -0,0 +1,37 @@ + +accept() twitter.json: speedup over 3.12.0 by commit +accept() twitter.json: speedup over 3.12.0 at each of 317 commits, ending at 5.0x. Labeled steps: #5234, #5283, #5592, #5809, #5810, #5853. +accept() twitter.json + +1× + +2× + +4× + +8× + +3.12.0 + +Sep 2026 + +Oct + +#5234: 1.22× + +#5234 +#5283: 2.6× + +#5283 +#5592: 2.9× + +#5592 +#5809: 3.7× +#5810: 4.0× + +#5809, #5810 +#5853: 4.9× + +#5853 + + diff --git a/docs/mkdocs/docs/images/perf_blog_timeline_dump_canada.svg b/docs/mkdocs/docs/images/perf_blog_timeline_dump_canada.svg new file mode 100644 index 000000000..7ebc45833 --- /dev/null +++ b/docs/mkdocs/docs/images/perf_blog_timeline_dump_canada.svg @@ -0,0 +1,27 @@ + +dump() canada.json: speedup over 3.12.0 by commit +dump() canada.json: speedup over 3.12.0 at each of 317 commits, ending at 3.3x. Labeled steps: #5285, #5634. +dump() canada.json + +1× + +2× + +4× + +8× + +3.12.0 + +Sep 2026 + +Oct + +#5285: 1.09× + +#5285 +#5634: 3.1× + +#5634 + + diff --git a/docs/mkdocs/docs/images/perf_blog_timeline_dump_twitter.svg b/docs/mkdocs/docs/images/perf_blog_timeline_dump_twitter.svg new file mode 100644 index 000000000..b82f2cc81 --- /dev/null +++ b/docs/mkdocs/docs/images/perf_blog_timeline_dump_twitter.svg @@ -0,0 +1,35 @@ + +dump() twitter.json: speedup over 3.12.0 by commit +dump() twitter.json: speedup over 3.12.0 at each of 317 commits, ending at 6.7x. Labeled steps: #5285, #5738, #5878, #5883. +dump() twitter.json + +1× + +2× + +4× + +8× + +16× + +3.12.0 + +Sep 2026 + +Oct + +#5285: 3.3× + +#5285 +#5738: 3.8× + +#5738 +#5878: 4.5× + +#5878 +#5883: 6.8× + +#5883 + + diff --git a/docs/mkdocs/docs/images/perf_blog_timeline_from_cbor_twitter.svg b/docs/mkdocs/docs/images/perf_blog_timeline_from_cbor_twitter.svg new file mode 100644 index 000000000..dd75ce0a4 --- /dev/null +++ b/docs/mkdocs/docs/images/perf_blog_timeline_from_cbor_twitter.svg @@ -0,0 +1,31 @@ + +from_cbor() twitter: speedup over 3.12.0 by commit +from_cbor() twitter: speedup over 3.12.0 at each of 317 commits, ending at 2.6x. Labeled steps: #5233, #5501, #5185, #5746. +from_cbor() twitter + +1× + +2× + +4× + +3.12.0 + +Sep 2026 + +Oct + +#5233: 1.47× + +#5233 +#5501: 2.2× + +#5501 +#5185: 1.76× + +#5185 +#5746: 2.3× + +#5746 + + diff --git a/docs/mkdocs/docs/images/perf_blog_timeline_parse_canada.svg b/docs/mkdocs/docs/images/perf_blog_timeline_parse_canada.svg new file mode 100644 index 000000000..5460432d7 --- /dev/null +++ b/docs/mkdocs/docs/images/perf_blog_timeline_parse_canada.svg @@ -0,0 +1,37 @@ + +parse() canada.json: speedup over 3.12.0 by commit +parse() canada.json: speedup over 3.12.0 at each of 317 commits, ending at 2.8x. Labeled steps: #5234, #5283, #5617, #5738, #5851, #5882. +parse() canada.json + +1× + +2× + +4× + +3.12.0 + +Sep 2026 + +Oct + +#5234: 1.11× + +#5234 +#5283: 1.47× + +#5283 +#5617: 1.48× + +#5617 +#5738: 1.95× + +#5738 +#5851: 2.2× + +#5851 +#5882: 2.8× + +#5882 + + diff --git a/docs/mkdocs/docs/images/perf_blog_timeline_parse_citm.svg b/docs/mkdocs/docs/images/perf_blog_timeline_parse_citm.svg new file mode 100644 index 000000000..f81b44fa8 --- /dev/null +++ b/docs/mkdocs/docs/images/perf_blog_timeline_parse_citm.svg @@ -0,0 +1,37 @@ + +parse() citm_catalog.json: speedup over 3.12.0 by commit +parse() citm_catalog.json: speedup over 3.12.0 at each of 317 commits, ending at 3.3x. Labeled steps: #5234, #5283, #5592, #5809, #5810, #5882. +parse() citm_catalog.json + +1× + +2× + +4× + +8× + +3.12.0 + +Sep 2026 + +Oct + +#5234: 1.55× + +#5234 +#5283: 1.95× + +#5283 +#5592: 2.2× + +#5592 +#5809: 2.4× +#5810: 2.8× + +#5809, #5810 +#5882: 3.3× + +#5882 + + diff --git a/docs/mkdocs/docs/images/perf_blog_timeline_parse_twitter_ifstream.svg b/docs/mkdocs/docs/images/perf_blog_timeline_parse_twitter_ifstream.svg new file mode 100644 index 000000000..e3efdf150 --- /dev/null +++ b/docs/mkdocs/docs/images/perf_blog_timeline_parse_twitter_ifstream.svg @@ -0,0 +1,25 @@ + +parse() twitter.json, std::ifstream: speedup over 3.12.0 by commit +parse() twitter.json, std::ifstream: speedup over 3.12.0 at each of 317 commits, ending at 2.2x. Labeled steps: #5828, #5884. +parse() twitter.json, std::ifstream + +1× + +2× + +4× + +3.12.0 + +Sep 2026 + +Oct + +#5828: 1.97× + +#5828 +#5884: 2.2× + +#5884 + + diff --git a/docs/mkdocs/docs/images/perf_blog_timeline_to_cbor_twitter.svg b/docs/mkdocs/docs/images/perf_blog_timeline_to_cbor_twitter.svg new file mode 100644 index 000000000..b091ac5ba --- /dev/null +++ b/docs/mkdocs/docs/images/perf_blog_timeline_to_cbor_twitter.svg @@ -0,0 +1,30 @@ + +to_cbor() twitter: speedup over 3.12.0 by commit +to_cbor() twitter: speedup over 3.12.0 at each of 317 commits, ending at 5.0x. Labeled steps: #5328, #5824, #5885. +to_cbor() twitter + +1× + +2× + +4× + +8× + +3.12.0 + +Sep 2026 + +Oct + +#5328: 2.2× + +#5328 +#5824: 3.5× + +#5824 +#5885: 5.1× + +#5885 + + diff --git a/docs/mkdocs/docs/images/perf_blog_view.svg b/docs/mkdocs/docs/images/perf_blog_view.svg new file mode 100644 index 000000000..7cebec3df --- /dev/null +++ b/docs/mkdocs/docs/images/perf_blog_view.svg @@ -0,0 +1,80 @@ + +Parse throughput of json_document, yyjson, simdjson's D​OM, Boost.J​SON and json::parse, Apple M1 Max +Grouped horizontal bars in GB/s. twitter.json json_document 3.48 GB/s; twitter.json yyjson 3.29 GB/s; twitter.json simdjson D​OM 2.92 GB/s; twitter.json Boost.J​SON 0.73 GB/s; twitter.json json::parse 0.46 GB/s; citm_catalog.json json_document 4.80 GB/s; citm_catalog.json yyjson 3.61 GB/s; citm_catalog.json simdjson D​OM 3.52 GB/s; citm_catalog.json Boost.J​SON 1.17 GB/s; citm_catalog.json json::parse 0.72 GB/s; canada.json json_document 2.35 GB/s; canada.json yyjson 1.51 GB/s; canada.json simdjson D​OM 1.07 GB/s; canada.json Boost.J​SON 0.90 GB/s; canada.json json::parse 0.38 GB/s; jeopardy.json json_document 2.45 GB/s; jeopardy.json yyjson 1.82 GB/s; jeopardy.json simdjson D​OM 2.07 GB/s; jeopardy.json Boost.J​SON 0.62 GB/s; jeopardy.json json::parse 0.33 GB/s. + +json_document + +yyjson + +simdjson D​OM + +Boost.J​SON + +json::parse + +0.5 + +1 + +1.5 + +2 + +2.5 + +3 + +3.5 + +4 + +4.5 + +5 +GB/s, longer is faster +twitter.json +json_document, twitter.json: 3.48 GB/s +3.48 +yyjson, twitter.json: 3.29 GB/s +3.29 +simdjson D​OM, twitter.json: 2.92 GB/s +2.92 +Boost.J​SON, twitter.json: 0.73 GB/s +0.73 +json::parse, twitter.json: 0.46 GB/s +0.46 +citm_catalog.json +json_document, citm_catalog.json: 4.80 GB/s +4.80 +yyjson, citm_catalog.json: 3.61 GB/s +3.61 +simdjson D​OM, citm_catalog.json: 3.52 GB/s +3.52 +Boost.J​SON, citm_catalog.json: 1.17 GB/s +1.17 +json::parse, citm_catalog.json: 0.72 GB/s +0.72 +canada.json +json_document, canada.json: 2.35 GB/s +2.35 +yyjson, canada.json: 1.51 GB/s +1.51 +simdjson D​OM, canada.json: 1.07 GB/s +1.07 +Boost.J​SON, canada.json: 0.90 GB/s +0.90 +json::parse, canada.json: 0.38 GB/s +0.38 +jeopardy.json +json_document, jeopardy.json: 2.45 GB/s +2.45 +yyjson, jeopardy.json: 1.82 GB/s +1.82 +simdjson D​OM, jeopardy.json: 2.07 GB/s +2.07 +Boost.J​SON, jeopardy.json: 0.62 GB/s +0.62 +json::parse, jeopardy.json: 0.33 GB/s +0.33 + + diff --git a/docs/mkdocs/mkdocs.yml b/docs/mkdocs/mkdocs.yml index 08c46ff83..fd4265458 100644 --- a/docs/mkdocs/mkdocs.yml +++ b/docs/mkdocs/mkdocs.yml @@ -104,6 +104,8 @@ nav: - 'v2.0.0': home/releases/v2.0.0.md - 'v1.1.0': home/releases/v1.1.0.md - 'v1.0.0': home/releases/v1.0.0.md + - Blog: + - 'How 3.13.0 got faster': home/blog/how_3_13_0_got_faster.md - home/design_goals.md - home/architecture.md - home/customers.md @@ -609,6 +611,7 @@ plugins: Home: - index.md - home/*.md + - home/blog/*.md Features: - features/*.md - features/binary_formats/*.md diff --git a/docs/mkdocs/scripts/figures/data/perf_timeline.csv b/docs/mkdocs/scripts/figures/data/perf_timeline.csv new file mode 100644 index 000000000..063733cec --- /dev/null +++ b/docs/mkdocs/scripts/figures/data/perf_timeline.csv @@ -0,0 +1,318 @@ +index,commit,date,pr,subject,parse_twitter,parse_citm,parse_canada,parse_citm_compact,parse_twitter_ifstream,accept_twitter,dump_twitter,dump_citm,dump_canada,dump4_citm,from_cbor_twitter,to_cbor_twitter,copy_destroy_citm,equal_citm,view_parse_twitter,view_parse_citm,view_parse_canada +0,55f93686c0,2025-04-11,,Merge branch 'release/3.12.0',3571.487,8219.609,17049.073,4366.792,3846.006,2394.213,1441.213,1375.446,7510.989,1917.809,3463.281,695.524,1643.327,1956.891,,, +1,1705bfe914,2025-04-11,4727,:bookmark: set version to 3.12.0 (#4727),3589.331,8245.422,17079.250,4422.711,3894.787,2397.597,1440.546,1377.745,7514.698,1922.371,3498.997,695.559,1655.240,1967.301,,, +2,4cca3b9cb2,2025-04-13,4738,Fix warning and add emscripten CI step (#4738),3559.657,8195.105,17066.291,4373.011,3794.269,2388.900,1419.182,1355.724,7420.703,1912.038,3468.667,664.214,1631.610,1939.475,,, +3,96c1b52f1c,2025-04-15,4742,Fix conversion to std::optional (#4742),3573.838,8157.276,16993.208,4383.656,3858.927,2395.092,1438.169,1372.208,7497.776,1925.481,3482.185,667.895,1633.619,1946.687,,, +4,88c92e605c,2025-04-16,4744,Fix compilation failure and warnings with NVHPC (#4744),3563.149,8039.911,16922.739,4381.927,3768.560,2385.086,1407.564,1345.578,7370.063,1897.474,3462.010,662.955,1623.286,1925.804,,, +5,c67d538274,2025-04-17,4749,Fix C4702 warning and extend MSVC CI job (#4749),3585.422,8251.057,17050.448,4400.542,3896.586,2390.655,1440.391,1371.722,7453.474,1921.277,3478.523,667.911,1636.206,1939.531,,, +6,756ca22ec5,2025-04-23,4736,Provide fallback for missing `char8_t` support (#4736),3559.146,8139.703,17085.114,4387.658,3856.745,2392.849,1424.997,1370.599,7477.537,1907.661,3484.427,665.541,1634.156,1957.401,,, +7,6b9199382b,2025-04-27,4763,Add missing header (#4763),3541.323,8077.761,17011.636,4359.183,3842.380,2396.686,1421.116,1363.737,7431.375,1902.909,3474.250,664.442,1625.644,1934.732,,, +8,3b02afb9d9,2025-04-28,4760,Specialize char_traits for std::byte to fix from_msgpack (fixes #4756) (#4760),3583.070,8198.146,17134.698,4381.743,3899.872,2409.185,1440.233,1380.810,7504.838,1921.781,3500.125,666.078,1657.207,1966.036,,, +9,eef76c200e,2025-04-28,4764,Make library work with C++20 modules (#4764),3528.529,8080.458,16936.656,4330.570,3764.287,2363.848,1407.851,1358.079,7356.239,1897.423,3455.287,665.194,1632.255,1937.675,,, +10,dff2b4756c,2025-04-29,4765,fix: BJData size calculation overflow (#4765),3553.706,8141.375,17047.511,4360.144,3797.070,2389.756,1417.737,1364.718,7454.313,1921.962,3479.878,666.456,1644.760,1944.696,,, +11,9110918cf8,2025-05-04,4748,Fix typos (#4748),3563.682,8176.427,17239.354,4398.055,3891.669,2399.782,1441.669,1387.133,7516.073,1925.323,3483.271,669.348,1647.760,1957.747,,, +12,46e7cd3dc2,2025-05-16,4775,Replace deprecated std::is_trivial in C++26 (#4775),3565.242,8195.396,17135.646,4373.479,3851.604,2392.258,1418.425,1370.958,7464.854,1909.844,3485.046,665.606,1643.155,1952.329,,, +13,828c891427,2025-05-16,4786,Extend type_name() to invalid type (#4786),3549.573,8103.761,17066.198,4359.911,3804.849,2389.121,1428.584,1366.405,7424.838,1914.434,3452.497,667.452,1632.750,1928.324,,, +14,4b17f90f65,2025-05-22,4609,Add `ignore_trailing_commas` option (#4609),3544.662,8132.052,16997.375,4344.592,3825.765,2382.755,1408.435,1367.468,7460.448,1924.522,3434.062,733.089,1631.356,1891.572,,, +15,b19f058465,2025-06-01,4802,Encode infinity and NaN as float for MsgPack and CBOR (#4802),3550.020,8126.438,17350.885,4327.596,3825.419,2389.356,1409.422,1376.649,7363.802,1921.226,3441.081,683.190,1624.144,1886.388,,, +16,cf16c5ab9f,2025-06-02,4805,Use binary_t::value_type (#4805),3551.735,8143.521,17153.291,4309.477,3829.229,2387.640,1428.550,1385.585,7415.271,1932.347,3425.838,657.735,1631.586,1888.954,,, +17,d70e46bc65,2025-06-25,4830,:bug: add missing header to input_adapters.hpp (#4830),3546.448,8157.469,17062.364,4335.300,3805.221,2382.796,1422.863,1382.581,7411.401,1928.676,3433.190,691.842,1632.972,1892.453,,, +18,30d27df61c,2025-07-31,4871,Fix CI (#4871),3570.015,8150.859,17070.625,4330.078,3806.505,2400.329,1434.947,1381.065,7425.718,1931.556,3429.632,656.545,1633.515,1892.916,,, +19,efcf9efb4f,2025-08-02,4873,Fixes #4854 Explicitly handle nullptr in sax_parse (#4873),3520.381,8050.719,16883.042,4331.820,3861.906,2368.069,1408.435,1375.954,7363.651,1898.466,3432.992,653.988,1634.141,1894.948,,, +20,4106af8d92,2025-08-20,4896,":rotating_light: suppress cppcoreguidelines-c-copy-assignment-signature,misc-unconventional-assign-operator (#4896)",3540.599,8175.265,17010.438,4345.182,3866.833,2393.935,1435.100,1389.496,7453.724,1945.791,3433.169,657.446,1638.434,1900.360,,, +21,a63795caf1,2025-10-22,4791,:rotating_light: avoid deprecation warning (#4791),3550.057,8174.646,16983.177,4363.521,3888.336,2393.305,1423.434,1376.334,7469.407,1936.013,3447.279,666.706,1645.354,1903.707,,, +22,1cc56b2dcd,2025-10-23,4957,Address CWG issue 2521 (#4957),3557.396,8119.651,17072.730,4351.881,3834.739,2386.097,1442.100,1393.245,7474.463,1932.315,3446.883,660.187,1638.983,1896.000,,, +23,54be9b04f0,2025-10-23,4960,:page_facing_up: update REUSE (#4960),3554.625,8098.287,17115.219,4324.578,3809.950,2390.791,1424.488,1376.715,7422.828,1913.208,3424.693,659.030,1623.405,1892.756,,, +24,29913ca760,2025-10-25,4963,Add char8_t* overload for _json and _json_pointer (#4963),3544.899,8169.203,17007.844,4353.594,3797.630,2392.108,1423.032,1375.216,7349.787,1909.897,3428.771,655.625,1635.273,1891.018,,, +25,515d994acb,2026-01-01,5044,:page_facing_up: adjust year (#5044),3524.610,8059.521,16949.416,4313.508,3773.750,2364.742,1410.427,1372.988,7382.438,1915.502,3403.159,643.968,1618.972,1884.213,,, +26,8f75700141,2026-01-11,4958,support types convertible to string_view as JSON keys (#4958),3546.757,8142.599,17103.386,4327.739,3825.990,2388.540,1420.515,1379.309,7413.062,1924.540,3433.677,658.266,1628.079,1890.759,,, +27,457bc283ff,2026-01-12,5039,fix(cbor): reject overflowing negative integers (#5039),3508.883,8077.291,16930.011,4305.982,3785.698,2378.693,1425.526,1366.111,7407.042,1929.142,3478.909,657.722,1636.304,1944.681,,, +28,e3014f162a,2026-01-13,5052,Fix conversion to std::optional (#5052),3547.995,8140.052,17010.031,4321.914,3797.807,2385.856,1430.414,1386.202,7407.260,1930.403,3462.213,657.829,1629.579,1932.066,,, +29,5ed07097fa,2026-01-13,5050,Fix -Wtautological-constant-out-of-range-compare in serializer (#5050),3544.136,8180.208,17033.209,4339.591,3799.779,2391.940,1427.870,1387.871,7368.765,1922.321,3454.938,659.367,1630.707,1959.722,,, +30,25f48639c7,2026-01-23,5016,Reference tuple from json (#5016),3563.180,8209.037,17030.969,4388.675,3895.086,2392.088,1430.666,1393.853,7528.567,1934.347,3481.351,677.234,1645.418,1957.591,,, +31,f8eee1bb79,2026-02-07,5071,Annotate unreachable comment-scanner switch paths to satisfy C26819 (#5071),3508.000,8128.750,16958.959,4315.235,3797.237,2371.963,1425.146,1374.867,7453.381,1930.211,3456.508,658.568,1629.188,1909.736,,, +32,fd17b0889e,2026-05-08,5139,Remove nullptr safety check from sax_parse functions (#5139),3544.101,8033.021,16963.292,4334.102,3800.736,2382.583,1419.050,1361.745,7357.073,1907.153,3429.315,657.041,1618.815,1937.829,,, +33,62f3b41b30,2026-05-08,5090,fix: treat single-element brace-init as copy/move instead of wrapping in array (#5074) (#5090),3550.323,8168.713,17060.031,4349.417,3794.110,2380.583,1416.822,1361.182,7369.651,1897.717,3466.731,659.297,1631.463,1941.905,,, +34,4fad446897,2026-05-10,5162,docs: add IDs to @requirement tags to fix Doxygen autolinking (#5162),3545.440,8031.891,17069.448,4324.698,3802.862,2383.513,1422.768,1384.602,7325.776,1917.606,3469.787,659.011,1631.296,1940.370,,, +35,a0a4e7cc0b,2026-05-13,5152,Add front methods to json_pointers (implements #4889) (#5152),3582.898,8215.010,17034.448,4345.515,3891.513,2396.572,1432.733,1395.714,7554.119,1934.008,3484.859,659.298,1642.314,1957.356,,, +36,93e49decbd,2026-05-14,5167,Fix incomplete-type error in set_parents with ordered_json (#5167),3555.224,8147.771,17017.073,4320.846,3831.318,2386.750,1435.840,1385.578,7436.864,1927.483,3466.508,658.409,1632.724,1939.528,,, +37,7192763f15,2026-05-14,5163,Fix compile error when using nlohmann ordered_map with WITH_DEFAULT macros (#5163),3520.675,8039.145,16892.291,4303.448,3769.198,2363.337,1410.030,1364.679,7380.797,1913.285,3437.278,660.130,1630.738,1943.235,,, +38,5a05627b1f,2026-05-14,5169,:rotating_light: fix warning (#5169),3600.555,8160.854,16979.031,4324.070,3887.823,2389.157,1430.983,1387.106,7509.984,1912.601,3473.713,663.279,1652.982,1958.774,,, +39,bb5404bb86,2026-05-15,5164,Fix GCC C++20 modules compilation #5103 (#5164),3543.823,8120.266,17015.395,4349.300,3794.880,2364.178,1414.408,1374.548,7484.218,1920.495,3444.266,699.326,1626.241,1954.688,,, +40,b1bb9fce0c,2026-05-15,3929,Fix for printing long doubles bug in dump_float (#3929),3549.378,8089.891,16934.927,4313.310,3824.802,2386.958,1412.164,1385.425,7417.869,1930.513,3461.151,651.557,1618.735,1944.450,,, +41,cba5dc0ed8,2026-05-16,4563,New macros for the named JSON convertor generation (#4563),3544.657,8168.182,16990.886,4327.688,3806.963,2370.975,1421.629,1390.206,7454.151,1919.284,3452.456,677.413,1623.622,1941.921,,, +42,47202c804a,2026-05-18,5171,Add missing overload to ordered_map::find (#5171),3527.542,8168.312,17141.989,4409.456,3887.378,2397.007,1428.765,1387.033,7495.604,1935.710,3485.088,660.155,1648.977,1951.430,,, +43,58cfecf7f7,2026-05-18,5151,Serialize enum (#5151),3521.302,8100.104,16805.489,4288.463,3763.971,2381.494,1408.713,1383.059,7328.261,1915.202,3440.372,648.726,1617.661,1922.608,,, +44,584e6b1cfb,2026-05-22,5187,Fix: update() parent pointers not updated after recursive merge with JSON_DIAGNOSTICS (#5187),3550.639,8083.463,17055.198,4326.833,3804.257,2381.519,1432.039,1381.986,7377.844,1915.159,3460.750,656.873,1631.527,1940.953,,, +45,02dfbea39d,2026-06-17,5209,added explicit instantiations of 'has_from_json' and 'has_to_json' for std_fs::path (#5209),3552.209,8215.474,17167.760,4338.404,3845.144,2398.026,1425.012,1387.629,7458.614,1931.388,3475.343,658.183,1644.329,1958.324,,, +46,fc1df0b7db,2026-06-23,5172,:recycle: remove unnecessary if (#5172),3562.088,8175.531,17044.042,4336.583,3863.651,2404.695,1425.872,1389.434,7523.855,1914.128,3481.000,653.259,1639.515,1957.649,,, +47,c5b2b26fdc,2026-06-29,5217,:memo: fix docs (#5217),3526.825,8052.307,17000.011,4343.963,3793.412,2388.194,1416.706,1374.177,7361.646,1904.008,3463.919,658.322,1627.765,1921.887,,, +48,b7566c6293,2026-06-30,5161,Harden JSON_HAS_RANGES detection for incomplete C++20 ranges implementations (#5161),3538.995,8071.787,17120.751,4318.797,3772.445,2384.008,1421.440,1385.239,7419.292,1898.929,3431.466,647.948,1602.258,1928.644,,, +49,adf78d3a76,2026-06-30,5215,Minor: unique_ptr template resolution workaround for MSVC (#5215),3550.380,8138.026,17036.802,4366.062,3842.430,2389.536,1426.658,1385.609,7428.787,1915.233,3467.188,658.332,1636.158,1942.556,,, +50,272411c5e6,2026-06-30,5218,Overwork project infrastructure (#5218),3547.487,8087.250,16962.198,4341.969,3793.831,2387.509,1420.888,1381.072,7450.078,1930.003,3466.193,659.676,1631.329,1943.043,,, +51,730b57775d,2026-07-01,5222,:bug: avoid assertion in patch (#5222),3539.912,8169.771,17027.229,4345.151,3790.753,2376.552,1421.263,1386.717,7399.985,1923.237,3455.722,653.609,1630.914,1934.028,,, +52,ca49ab6123,2026-07-02,5223,Extend value to arrays when using JSON pointers (#5223),3550.341,8113.787,16951.313,4346.805,3793.097,2391.224,1409.695,1380.075,7374.073,1926.401,3458.438,659.382,1629.387,1941.306,,, +53,8d7e0046f4,2026-07-02,5224,Add std::format and fmt support (#5224),3546.935,8170.406,16968.927,4346.906,3814.956,2387.612,1424.861,1386.542,7350.765,1900.376,3461.177,662.476,1633.858,1930.434,,, +54,31dd15b258,2026-07-02,5221,Fix ambiguous static_cast (#5221),3551.945,8213.000,17131.271,4373.620,3845.763,2393.074,1438.633,1385.769,7471.146,1927.804,3486.568,662.015,1631.810,1958.901,,, +55,eed1587000,2026-07-05,5234,Reconstruct lexer diagnostics lazily for seekable input (#120) (#5234),3145.909,5311.094,15381.843,4292.961,3851.260,1966.833,1422.598,1365.461,7443.276,1920.467,3467.060,702.832,1637.587,1905.758,,, +56,83c87cb9e0,2026-07-05,5233,Read binary strings/blobs in bulk chunks with a memcpy fast path (#5233),3141.867,5259.485,15454.959,4326.664,3797.198,1987.623,1406.492,1372.765,7424.729,1899.726,2363.895,667.502,1630.097,1888.392,,, +57,acf076a677,2026-07-06,5238,Fix ADL leak of nlohmann::detail through basic_json's default base class (#5238),3152.153,5277.073,15291.407,4338.828,3839.331,1995.524,1429.960,1365.917,7456.776,1907.928,2398.016,670.257,1627.590,1897.997,,, +58,c05c5e229b,2026-07-07,5241,Add missing copyright notices to SBOM (#5241),3147.485,5268.865,15319.584,4321.823,3796.972,1981.167,1432.689,1371.264,7465.416,1918.083,2388.154,667.119,1630.745,1891.583,,, +59,366f3d26e5,2026-07-08,5235,Replace snprintf with a branch-free writer for \uXXXX escapes (#5235),3127.641,5272.302,15425.239,4340.459,3850.357,1997.957,1459.603,1356.461,7509.703,1899.916,2366.966,668.383,1624.981,1889.145,,, +60,f8e99e856c,2026-07-09,5248,Fix nvcc CUDA 12.0/12.1 C++20 ranges parse error (#3907) (#5248),3132.099,5226.375,15164.510,4301.901,3769.651,1967.647,1456.759,1365.810,7530.724,1908.635,2381.610,683.815,1616.699,1879.795,,, +61,855f511db4,2026-07-09,5250,Fix stale Clang -Weverything suppression comments; drop -Wno-missing-noreturn (#5250),3126.792,5208.338,15352.520,4293.596,3800.544,1969.497,1458.601,1329.851,7372.849,1876.404,2368.894,662.173,1603.881,1874.673,,, +62,d0a43141ea,2026-07-09,5253,Fix #3868: Remove operator!= to enable P2468R2 rewritten candidate synthesis (#5253),3165.182,5270.458,15479.031,4302.729,3845.646,1989.231,1474.135,1343.312,7443.282,1887.909,2392.523,670.395,1634.871,1894.474,,, +63,b630f5e9c7,2026-07-10,5260,Fix container input_adapter SFINAE for lvalue-only ADL begin/end (#5260),3128.117,5263.807,15375.677,4330.544,3819.953,1982.196,1454.088,1339.062,7364.683,1877.699,2361.482,666.486,1616.514,1920.626,,, +64,c60217e801,2026-07-11,5205,fix: support constructing json from C++20 range views (#4916) (#5205),3117.539,5206.552,15308.166,4320.513,3798.713,1983.458,1447.072,1350.950,7357.213,1875.632,2340.731,662.905,1633.357,1934.261,,, +65,fe2bcc080f,2026-07-11,5266,Custom BinaryType direct assignment (#5266),3174.528,5284.562,15383.645,4328.120,3895.086,1996.913,1478.427,1355.569,7512.115,1899.236,2398.490,672.118,1642.910,1967.660,,, +66,ca76c37650,2026-07-11,5265,Add iterator+sentinel tests and docs for binary deserializers (#5265),3134.265,5218.021,15352.521,4306.284,3791.612,1967.824,1451.953,1358.685,7508.828,1891.697,2377.848,665.474,1631.495,1934.907,,, +67,c197feff81,2026-07-12,5268,Extend memcpy fast path to sized sentinels (e.g. std::counted_iterator) (#5268),3116.271,5207.146,15415.771,4272.443,3815.851,1984.966,1451.103,1341.093,7328.037,1865.385,2383.169,664.494,1617.967,1915.428,,, +68,1c5a953de5,2026-07-19,5276,reject unpaired UTF-16 surrogates in wide-string input (#5276),3155.360,5299.631,15419.812,4302.089,3853.104,1987.259,1468.653,1349.665,7549.432,1911.365,2457.555,680.311,1653.282,1955.673,,, +69,3565f40229,2026-07-20,5284,reject negative UBJSON/BJData string length (#5284),3170.456,5301.698,15404.416,4351.226,3893.594,1997.034,1470.907,1358.245,7491.480,1909.601,2401.384,671.260,1639.865,1963.899,,, +70,9a3ebb9456,2026-07-23,5287,validate BSON document size against the bytes read (#5287),3157.646,5264.062,15305.271,4338.672,3839.643,1987.196,1461.356,1351.630,7444.791,1890.626,2398.503,665.262,1655.798,1930.268,,, +71,8ec98e2c9e,2026-07-27,5313,adding cleanups to bson writer (#5313),3150.448,5268.948,15445.927,4308.612,3805.005,1989.643,1466.348,1349.591,7326.453,1895.842,2393.278,708.164,1644.025,1946.652,,, +72,2e23687092,2026-07-28,5314,to_bson() silently emits corrupt documents when a length exceeds INT32_MAX (#5314),3146.534,5254.547,15445.250,4304.008,3798.620,1986.260,1459.735,1345.534,7517.651,1903.996,2395.155,674.344,1644.565,1952.682,,, +73,868506dcc0,2026-07-30,5335,Fix CBOR half-float assertion bounds (#5335),3167.086,5267.068,15397.979,4351.011,3883.135,2003.345,1468.939,1360.483,7455.230,1909.055,2406.305,679.942,1643.646,1945.639,,, +74,dd24e2dffd,2026-07-30,5332,check all BSON reads and add an EOF check for booleans (#5332),3138.315,5284.833,15399.166,4310.513,3847.630,1980.682,1464.104,1344.108,7445.146,1901.512,2400.532,670.850,1635.196,1900.946,,, +75,fd72ecfc8c,2026-07-31,5301,validate ndarray element types in write_bjdata_ndarray (#5301),3164.662,5262.141,15399.010,4287.271,3849.318,1988.186,1460.688,1353.767,7458.380,1893.669,2398.485,668.466,1644.018,1921.615,,, +76,bc48951128,2026-07-31,5323,Fix UBJSON high-precision floating-point overflow handling (#5323),3155.628,5289.864,15474.375,4324.253,3888.966,2015.068,1477.846,1354.622,7480.730,1905.566,2415.428,668.997,1641.776,1911.819,,, +77,d94cbd99dc,2026-07-31,5274,reject CBOR array/map length equal to the indefinite-length marker (#5274),3144.041,5272.193,15466.980,4338.735,3848.849,1999.231,1459.129,1358.119,7507.307,1901.807,2394.972,667.016,1635.960,1899.878,,, +78,2222d386c9,2026-07-31,5339,fix: check CBOR tagged subtype reads (#5339),3095.768,5137.792,15064.146,4258.977,3755.229,1965.900,1454.890,1338.066,7399.068,1883.624,2358.193,660.190,1618.172,1939.713,,, +79,585929bff9,2026-08-03,5289,Fix Clang deprecation warning for json_pointer operator== with ordered_json (#5289),3142.341,5259.360,15258.271,4275.479,3807.672,1969.704,1453.688,1337.633,7451.037,1879.447,2364.966,661.560,1635.617,1945.709,,, +80,5f121d8c50,2026-08-03,5336,avoid sign extension in char_traits::to_int_type (#5336),3145.000,5264.880,15441.521,4269.632,3796.781,1983.497,1440.810,1325.232,7427.750,1880.047,2361.899,663.772,1621.595,1912.378,,, +81,68f0722a19,2026-08-03,5342,remove discarded array from parent object in end_array (#5342),3161.273,5269.907,15312.469,4335.433,3840.510,1995.965,1465.745,1355.934,7474.901,1895.483,2401.197,657.397,1634.791,1901.902,,, +82,dca9d49a33,2026-08-05,5348,reject out-of-range code points in UTF-32 wide-string input (#5348),3119.753,5210.979,15209.260,4273.984,3799.034,1995.297,1459.456,1342.982,7431.646,1891.478,2370.364,653.618,1633.585,1882.753,,, +83,9a091d2b82,2026-08-05,5362,Do not write BJData ndarrays whose size overflows std::size_t (#5362),3137.381,5257.312,15286.084,4308.500,3805.325,1995.140,1468.138,1354.729,7510.088,1893.036,2384.577,659.394,1628.260,1890.245,,, +84,d5647e6a3b,2026-08-05,5355,Resolve the TODO(niels) in get_ubjson_string (#5355),3121.278,5249.927,15246.011,4292.737,3800.490,1967.087,1443.762,1333.226,7368.953,1862.131,2347.852,644.647,1623.909,1889.958,,, +85,bacdabd176,2026-08-05,5361,Fix start_pos() for strings containing escape sequences (#5361),3114.653,5251.619,15253.907,4331.818,3786.156,1960.676,1442.388,1330.247,7354.724,1863.306,2351.588,650.781,1623.543,1886.671,,, +86,1c136a66c4,2026-08-06,5363,Move the CBOR doc block to the function it describes (#5363),3124.274,5209.615,15411.897,4293.320,3812.156,1982.854,1457.653,1345.680,7422.343,1884.848,2387.050,656.076,1627.620,1878.737,,, +87,6285225fd0,2026-08-12,5211,Fix integer comparison bug (#5211),3159.557,5291.323,15395.395,4329.836,3867.200,1989.476,1456.052,1359.795,7526.860,1879.642,2396.137,659.077,1638.176,1900.416,,, +88,b5378e8deb,2026-08-19,5331,Fix CBOR tag handlers not recognizing tags 0-5 and 21-23 (#5331),3172.476,5239.812,15479.917,4295.539,3871.289,2003.439,1473.447,1343.742,7516.875,1888.412,2400.572,645.588,1641.621,1886.531,,, +89,2f025f401e,2026-08-25,5380,Throw other_error.502 when UBJSON use_type is set without use_size (#5380),3155.873,5190.312,15144.406,4286.388,3793.805,1968.199,1442.240,1340.929,7373.610,1860.543,2370.570,648.906,1631.207,1893.750,,, +90,35705d79d8,2026-08-28,5414,Fix update(merge_objects=true) throwing on primitive-to-object merge (#5414),3169.029,5252.578,15387.292,4340.690,3893.646,1979.616,1472.286,1361.543,7491.302,1903.909,2401.404,659.396,1641.697,1912.409,,, +91,137a40b9aa,2026-09-05,5459,Compare integers with floats exactly instead of widening the integer (#5459),3134.229,5259.349,15404.541,4300.146,3846.646,1992.155,1467.919,1351.774,7467.146,1887.255,2391.467,660.347,1631.735,1891.751,,, +92,3c875683a5,2026-09-05,5461,Make diff() linear when an array shrinks (#5461),3141.068,5259.000,15271.166,4319.659,3797.088,1983.450,1455.354,1354.676,7444.860,1891.687,2389.333,657.163,1632.998,1887.016,,, +93,09b6b6b5ba,2026-09-05,5455,Fix to_bjdata() emitting unparsable output when _ArraySize_ is not an array (#5455),3161.542,5253.047,15311.718,4291.388,3834.264,1992.744,1472.685,1357.680,7409.479,1889.287,2389.501,698.680,1628.237,1888.705,,, +94,faa35cc647,2026-09-09,5484,Skip integer conversion in accept()/SAX validation when the value is unused (#5484),3103.654,5222.453,15458.573,4215.765,3804.320,1935.161,1468.660,1353.499,7452.052,1883.191,2383.524,654.441,1631.700,1885.664,,, +95,6d86cc0f6b,2026-09-09,5490,Speed up whitespace skipping in the lexer (#5490),3181.328,5276.318,15307.312,4262.771,3877.401,1977.062,1465.196,1356.119,7594.177,1906.814,2388.615,684.926,1636.950,1893.526,,, +96,c05347dd54,2026-09-09,5475,Undefine the four JSON_HEDLEY_* macros that leak after including json.hpp (#5475),3147.932,5156.625,15347.990,4217.505,3871.812,1982.807,1458.787,1354.934,7639.328,1879.823,2370.342,674.625,1637.257,1903.903,,, +97,d6660cf718,2026-09-09,5485,Route hand-rolled diagnostic pragmas through Hedley (#5485),3116.151,5215.531,15257.499,4189.979,3835.023,1974.111,1459.363,1342.996,7553.802,1887.698,2379.999,669.934,1635.350,1877.492,,, +98,c9477ccf91,2026-09-09,5496,Throw when JSON Patch remove's target path resolves through a primitive or null parent (#5496),3138.841,5258.401,15315.417,4201.008,3874.682,1967.630,1446.393,1335.060,7542.708,1870.111,2390.141,668.484,1634.138,1886.981,,, +99,8c38e270b5,2026-09-09,5497,Reject JSON Patch move when from is a proper prefix of path (#5497),3183.820,5280.000,15346.969,4222.164,3839.596,1970.753,1470.428,1351.838,7515.344,1895.809,2389.731,673.696,1633.247,1889.546,,, +100,a316738cfa,2026-09-09,5471,Add JSON_HEDLEY_WARN_UNUSED_RESULT to the current accept() overloads (#5471),3175.891,5281.302,15397.062,4240.576,3934.333,1981.765,1475.890,1353.387,7625.104,1896.181,2401.097,669.729,1646.133,1895.638,,, +101,c9f1d2d646,2026-09-09,5477,Broaden JSON_HEDLEY_WARN_UNUSED_RESULT coverage to pure query functions (#5477),3171.706,5286.880,15430.844,4264.242,3880.943,1979.990,1472.289,1357.283,7569.854,1904.026,2407.487,671.673,1641.521,1889.053,,, +102,8e26b9d4b3,2026-09-09,5495,Make contains(json_pointer) return false instead of throwing on unrepresentable array-index tokens (#5495),3163.789,5240.755,15337.698,4226.154,3880.956,1971.912,1475.322,1357.125,7632.281,1904.060,2401.190,678.863,1638.371,1903.385,,, +103,b17c272f46,2026-09-09,5472,Reserve capacity in from_json() object conversion when the target container supports it (#5472),3152.899,5257.645,15401.000,4210.846,3885.123,1989.159,1476.637,1359.851,7639.396,1898.981,2408.243,670.075,1633.528,1894.086,,, +104,3e2de32226,2026-09-10,5516,Remove unused iomanip include (#5516),3141.349,5253.657,15364.074,4203.969,3847.620,1968.422,1455.035,1352.754,7582.494,1882.051,2371.814,663.504,1630.172,1870.223,,, +105,d69fb8654d,2026-09-10,5501,Return the parsed value by move from from_cbor() and friends (#5501),3133.485,5231.662,15309.926,4183.200,3835.662,1971.125,1462.447,1349.563,7479.136,1904.202,1595.991,661.519,1630.278,1883.994,,, +106,4a93aa4e2f,2026-09-10,5283,"Speed up parsing of contiguous input (numbers, strings, UTF-8) (#5283)",1963.879,4221.308,11609.531,3278.497,3857.880,927.779,1483.726,1357.793,7483.343,1906.916,1481.419,676.438,1643.746,1895.551,,, +107,bfb07786cd,2026-09-10,5473,Fix to_bjdata() silently truncating out-of-range _ArrayData_ elements (#5473),1941.894,4204.971,11726.688,3264.055,3808.784,923.797,1453.071,1351.760,7458.125,1891.470,1473.508,662.164,1633.043,1888.548,,, +108,ff80ed3295,2026-09-11,5285,"Speed up dump(), and keep it from overflowing the stack (#5285)",1939.289,4175.541,11781.291,3233.302,3816.101,914.071,441.200,540.473,6895.041,852.214,1478.197,656.635,1632.675,1885.303,,, +109,da42a627dc,2026-09-11,5449,Stop dump() from heap-allocating its output adapter per call (#5449),1937.658,4200.966,11639.198,3235.493,3827.695,909.901,448.948,537.008,6909.036,839.027,1479.968,659.210,1643.621,1878.065,,, +110,1dc1d09fc6,2026-09-11,5457,Do not search a container for the value the callback rejected (#5457),1937.459,4140.078,11576.803,3271.838,3845.193,924.316,450.148,539.031,6848.776,844.624,1470.333,671.563,1645.624,1900.347,,, +111,fae364db24,2026-09-11,5458,Move the scanned string into the value instead of copying it (#5458),1986.146,4110.787,11757.157,3150.932,3830.550,918.517,442.629,539.606,6913.104,843.920,1417.037,655.259,1628.224,1892.991,,, +112,dd50f0eb16,2026-09-11,5502,Stop CBOR indefinite-length strings from recursing per chunk (#5502),1987.853,4033.182,11613.834,3116.378,3841.164,915.943,442.366,539.480,6864.130,836.458,1431.035,656.146,1599.574,1885.858,,, +113,4bb2de8cc5,2026-09-11,5503,Reject a nested BJData ndarray dimension vector where it is read (#5503),1981.298,4086.276,11569.240,3141.539,3829.870,923.087,444.038,539.863,6800.724,835.434,1429.637,655.448,1628.999,1879.300,,, +114,44f8ec30e9,2026-09-11,5504,Bound UBJSON optimized arrays of a valueless type (#5504),2005.265,4092.265,11730.198,3130.635,3863.992,925.033,448.222,542.323,6893.078,849.901,1437.855,662.171,1638.306,1896.324,,, +115,0dd8ca9023,2026-09-11,5505,Read MessagePack containers without recursing per nesting level (#5505),2019.983,4105.305,11862.979,3142.581,3891.601,927.092,447.942,543.936,6939.963,845.634,1442.889,656.578,1637.512,1897.815,,, +116,91ab3e81f5,2026-09-11,5506,Read CBOR containers and tags without recursing per nesting level (#5506),1986.334,4039.961,11554.094,3118.761,3783.101,912.504,440.410,534.894,6831.943,834.661,1439.061,655.287,1615.619,1869.912,,, +117,a14619b354,2026-09-11,5507,Read UBJSON and BJData containers without recursing per nesting level (#5507),2011.959,4095.868,11782.688,3135.264,3873.604,926.014,447.638,541.304,6905.838,846.051,1468.512,664.161,1629.083,1898.772,,, +118,0452641c18,2026-09-11,5508,Read BSON documents without recursing per nesting level (#5508),1974.160,4043.887,11662.938,3095.011,3832.744,916.007,439.914,534.149,6904.453,835.615,1450.792,648.298,1616.917,1874.133,,, +119,aa391dc0a5,2026-09-11,5520,Fix two develop CI regressions: dump() nodiscard warning and binary-reader const-correctness (#5520),2003.010,4068.010,11771.364,3137.266,3825.247,924.668,442.416,539.478,6847.083,841.996,1460.659,649.378,1633.738,1885.897,,, +120,a50c2537eb,2026-09-12,5476,Reserve capped array capacity in json_sax_dom_parser::start_array() for definite-length binary arrays (#5476),1986.488,4063.883,11588.333,3077.995,3827.479,908.419,440.959,539.114,6806.177,833.555,1452.851,662.422,1627.995,1915.388,,, +121,c0b2878a44,2026-09-13,5493,Swap diagnostic positions in basic_json::swap() (#5493),2010.881,4081.057,11734.730,3130.919,3882.258,925.490,450.092,542.953,6925.188,848.486,1480.051,698.636,1643.508,1933.941,,, +122,1da2f68992,2026-09-14,5522,Only reserve array capacity if the array type supports it (#5522),1972.122,4048.622,11775.062,3107.938,3803.528,909.385,441.204,535.885,6793.927,842.843,1446.330,658.787,1619.239,1899.164,,, +123,6487678bc5,2026-09-15,5464,Fix swap(array_t&)/swap(object_t&) to update parent pointers under JSON_DIAGNOSTICS (#5464),2023.028,4116.169,11737.438,3165.172,3893.266,924.912,448.959,541.529,6970.438,852.039,1483.231,689.631,1635.240,1937.356,,, +124,c72f37a40d,2026-09-15,5443,Support custom object/array types and improve template parameter handling (#5443),2001.617,4076.547,11666.583,3109.756,3918.206,923.197,450.592,542.266,6901.703,846.558,1468.284,660.116,1641.586,1905.840,,, +125,502e9d66f6,2026-09-16,5479,Fix to_bjdata() emitting the Draft-3-only 'B' marker in default Draft-2 mode (#5479),1970.935,4018.060,11445.500,3079.409,3809.050,910.459,444.217,539.291,6851.407,833.755,1444.697,698.040,1626.601,1889.024,,, +126,c41152e620,2026-09-16,5494,Fall back to plain-object encoding when to_bjdata()'s _ArrayType_ annotation is not a string (#5494),1979.481,4054.688,11734.292,3085.542,3819.351,916.992,445.126,539.870,6853.005,833.190,1457.166,665.391,1629.586,1888.734,,, +127,e564136c22,2026-09-22,5465,Make diff() account for member order in ordered_json objects (#5465),1970.519,4063.744,11605.198,3087.497,3848.867,915.695,447.051,539.711,6835.172,841.402,1452.004,664.560,1640.751,1890.473,,, +128,e485441123,2026-09-22,5466,Restore a duplicate key's prior value when the callback rejects its new value (#5466),2012.920,4099.265,11796.895,3156.818,3913.812,931.464,451.403,541.419,6981.041,846.536,1469.521,704.138,1633.327,1905.709,,, +129,a2b19d6158,2026-09-22,5467,Honor allow_exceptions=false for excessive array/object size (out_of_range.408) (#5467),1996.478,4084.422,11734.208,3129.099,3850.854,923.499,446.555,539.379,6949.427,842.029,1453.166,694.200,1631.133,1888.865,,, +130,d2c1a6a272,2026-09-22,5468,"Reject array insert(pos, first, last) iterators not pointing into an array (#5468)",1983.767,4066.651,11618.667,3128.505,3848.427,914.900,447.603,541.973,6915.265,844.307,1458.762,677.699,1638.789,1892.443,,, +131,0b20b7e622,2026-09-22,5469,Reject MessagePack/BSON binary subtypes that don't fit their wire format (#5469),2010.548,4097.476,11681.218,3121.010,3866.146,925.619,448.533,540.786,6957.000,843.349,1463.518,658.302,1630.857,1902.522,,, +132,1054b2097e,2026-09-23,5286,Speed up binary writing: value-type output sink + byte-swap number encoding (#5286),1989.739,4101.393,12034.438,3157.240,3853.987,910.115,449.084,555.970,6884.386,864.526,1471.905,656.635,1661.806,1926.233,,, +133,ed513715a8,2026-09-24,5534,Document that a NUL byte in the input is treated as end of input (#5534),2016.236,4104.763,11906.323,3141.138,3936.240,916.611,455.397,562.053,6987.917,871.110,1475.622,657.877,1676.934,1936.557,,, +134,56b3ee566c,2026-09-24,5389,Fix stack overflow when copying a deeply nested value (#5387) (#5389),1998.356,4051.206,11660.865,3101.107,3834.672,910.072,452.943,544.464,6908.974,849.212,1460.691,638.644,1650.749,1914.423,,, +135,918da64657,2026-09-24,5542,Keep BJData ndarray annotations that would not survive a round trip as objects (#5542),2003.009,4042.969,11517.375,3095.122,3815.360,908.444,448.415,539.423,6911.041,840.044,1449.543,671.530,1651.709,1908.408,,, +136,8699de3064,2026-09-24,5555,Stop allocating the BJData excluded-marker list per container (#5555),2001.718,4071.333,11686.427,3122.065,3796.757,900.895,447.123,539.918,6860.765,844.752,1444.435,646.608,1644.943,1879.295,,, +137,2e91641de2,2026-09-24,5544,"Test JSON_BRACE_INIT_COPY_SEMANTICS for real, and fix one-element tuples under it (#5544)",2004.785,4083.914,11662.270,3119.602,3917.914,922.465,454.869,547.631,6987.234,853.010,1469.612,649.946,1672.246,1913.098,,, +138,7c90ec2323,2026-09-24,5546,Hash deeply nested values without recursing per nesting level (#5546),1952.912,4071.159,11594.927,3117.497,3793.052,900.729,444.245,539.787,6854.406,841.264,1457.530,643.139,1638.578,1890.482,,, +139,4daca40d7b,2026-09-24,5547,Merge deeply nested objects without recursing per nesting level (#5547),2008.226,4104.399,11619.542,3148.604,3918.175,921.207,457.191,547.553,6942.265,855.352,1464.974,651.924,1665.232,1915.869,,, +140,01b53c8c15,2026-09-25,5552,Keep JSON_DIAGNOSTICS parent pointers of ordered_json members after erase() and update() (#5552),1979.354,4061.378,11603.480,3098.586,3875.258,919.125,455.911,546.269,6940.026,849.511,1461.751,648.841,1660.978,1917.254,,, +141,abbe52d6de,2026-09-25,5344,Add JSON_PRECISE_STREAM_POSITION to leave the character that terminates a number in the stream (#5344),2001.170,4081.617,11653.531,3131.331,3876.208,915.153,453.436,546.720,6971.864,848.900,1463.182,647.946,1662.943,1906.246,,, +142,02dd3e67f2,2026-09-25,5390,Fix stack overflow and exponential runtime when comparing nested values (#5390),1987.861,4074.039,11596.000,3161.284,3901.497,919.541,447.553,543.948,6910.162,845.668,1455.759,664.224,1652.074,1930.575,,, +143,465407f3ce,2026-09-25,2818,Improve error message for const fields (#2818),1975.274,4118.565,11863.448,3130.541,3880.764,910.767,452.904,545.363,6906.890,843.118,1459.728,645.770,1654.265,1934.900,,, +144,632a5812a8,2026-09-25,5272,Support zero-member types in NLOHMANN_DEFINE_TYPE_* macros (#4041) (#5272),1963.323,4021.526,11747.959,3102.006,3784.896,904.545,444.355,543.748,6840.604,841.416,1438.306,639.223,1649.816,1930.274,,, +145,d19f7f5dce,2026-09-25,5185,Fix BSON conformance issue (#5185),1958.606,4048.536,11541.011,3086.805,3817.875,900.452,446.001,544.071,6824.323,837.078,1971.245,641.271,1651.209,1934.647,,, +146,c60a0bc336,2026-09-25,5573,Allocate the deep copy's key scratch space with the provided allocator (#5573),1965.216,4021.459,11630.855,3074.078,3807.258,902.782,444.394,542.562,6919.969,840.942,1975.668,645.827,1637.429,1918.607,,, +147,1e44262091,2026-09-25,5560,Make JSON_STRICT_NUL_HANDLING part of the ABI tag (#5560),1961.862,4069.617,11594.083,3117.500,3817.758,909.186,448.820,544.420,6845.802,844.016,1977.909,645.864,1651.269,1937.217,,, +148,95e9a5931c,2026-09-25,5553,"Write BSON in linear time, without recursing per nesting level (#5553)",1965.832,4033.394,11589.364,3120.422,3792.091,905.110,446.984,543.328,6794.244,844.943,1991.659,640.188,1647.675,1928.562,,, +149,fe4a544c7e,2026-09-27,5515,handled when size exceed uint32 (#5515),1998.244,4047.208,11755.209,3091.776,3815.000,908.733,447.990,546.295,6903.084,850.946,1993.351,645.487,1654.343,1933.385,,, +150,4fa95d9810,2026-09-27,5583,Remove unreachable branches from the binary writer (#5583),1983.323,4038.331,11666.480,3092.841,3844.935,910.149,449.697,541.110,6840.583,830.604,1979.750,653.542,1650.670,1928.883,,, +151,6178982b8d,2026-09-27,5582,Compare unordered objects by key below the nesting bound (#5582),1971.706,4073.498,11600.646,3132.700,3830.323,910.231,448.982,538.812,6836.770,846.149,2007.607,657.180,1653.089,1934.675,,, +152,f7972970a4,2026-09-27,5584,Throw instead of writing MessagePack lengths beyond UINT32_MAX (#5584),1969.417,4013.021,11602.447,3083.552,3842.495,904.223,447.444,542.170,6862.735,839.628,2000.766,655.034,1653.006,1939.922,,, +153,6bd106893a,2026-09-27,5559,Fix CBOR tag handling in cbor_tag_handler_t::store for non-binary items (#5559),1969.250,4034.693,11691.500,3119.171,3827.860,916.053,455.026,542.577,6886.360,839.718,2012.919,653.031,1651.090,1931.698,,, +154,1e101ecac1,2026-09-27,2998,Add BON8 support (#2998),1975.804,4082.375,11658.542,3192.828,3758.141,907.029,444.920,556.470,6844.698,849.276,1977.153,649.181,1645.306,1920.836,,, +155,9e1a09eec0,2026-09-28,5594,Name the key type when rejecting non-string CBOR/MessagePack map keys (#5594),2006.582,4151.784,11788.532,3156.156,3845.052,915.647,454.027,557.846,6814.282,864.094,2021.072,654.095,1663.051,1947.078,,, +156,fc03b9912e,2026-09-28,5597,"Look up the locale decimal point at conversion time, not lexer construction (#5597)",2020.431,4172.916,12716.958,3196.958,3909.774,910.001,449.314,547.334,6900.968,852.306,2048.019,665.843,1696.937,1982.586,,, +157,5b9ffae8bb,2026-09-30,5616,Move the float conversion chain out of the lexer (#5616),1992.332,4167.328,12638.573,3212.515,3842.885,906.649,446.941,539.674,6952.672,839.042,2021.057,695.813,1684.689,1967.216,,, +158,d268eaa693,2026-09-30,5617,Convert floats with Eisel-Lemire when std::from_chars is unavailable (#5617),1987.608,4086.125,11507.062,3170.013,3830.425,898.856,445.077,542.854,6919.568,839.210,2021.918,666.925,1701.466,1951.211,,, +159,65af260117,2026-09-30,5609,Move instead of deep-copy ordered_json values when an object grows (#5609),1998.981,4134.057,11592.855,3192.779,3863.005,904.702,446.575,539.685,6850.886,840.845,2035.646,680.331,1682.755,1968.744,,, +160,6a8a7735ed,2026-09-30,5614,Do not throw in contains() for an empty array reference token (#5614),2008.447,4144.024,11612.176,3165.468,3842.060,905.045,443.941,539.755,6846.042,846.248,1998.713,669.752,1684.717,1953.487,,, +161,0050d0f7a4,2026-09-30,5637,Share one nesting depth limit between all bounded descents (#5637),2021.078,4170.581,11589.886,3224.963,3937.021,906.411,448.033,541.177,6911.391,848.641,2040.978,670.005,1703.593,1984.639,,, +162,68beba727c,2026-09-30,5679,Fix from_json() for enums with underlying type bool (#5679),1986.468,4139.033,11632.833,3201.351,3798.006,888.464,438.471,535.903,6843.110,836.030,1996.109,685.053,1672.430,1940.598,,, +163,fc4c9c3446,2026-09-30,5680,Fix clear() to also reset the subtype of a binary value (#5680),1972.561,4094.867,11540.157,3155.722,3833.925,893.243,445.764,539.739,6848.562,843.068,2016.293,664.788,1685.759,1965.342,,, +164,678fd3017b,2026-09-30,5681,Fix element path for map/unordered_map JSON_DIAGNOSTICS errors (#5681),2014.783,4164.471,11563.614,3219.865,3891.917,892.727,445.945,542.044,6900.807,844.422,2034.323,669.901,1688.051,1981.601,,, +165,6d7845d207,2026-09-30,5683,Fix deprecated json_pointer/string operator== warning in value() (#5683),1965.197,4105.789,11501.364,3157.544,3800.289,898.122,445.399,539.685,6900.433,834.541,2000.133,652.354,1670.945,1960.876,,, +166,42f89e3130,2026-09-30,5684,Let to_json(std::optional) propagate exceptions from T's to_json (#5684),2016.899,4156.430,11476.489,3216.315,3895.245,908.714,444.300,541.764,6923.422,839.459,2018.934,672.859,1689.306,1987.428,,, +167,6218aa212b,2026-09-30,5687,Throw std::length_error for operator[](SIZE_MAX) instead of corrupting the array (#5687),1989.158,4149.156,11547.229,3193.015,3845.187,905.308,446.389,541.290,6853.266,843.028,2037.245,661.213,1681.390,1966.649,,, +168,1151826508,2026-09-30,5691,Take diff()'s fast path unless the object type reorders members (#5691),1991.280,4147.341,11570.312,3171.182,3847.333,904.510,441.250,539.803,6885.776,836.303,2018.693,665.307,1685.990,1970.057,,, +169,e444a66276,2026-09-30,5693,Copy values before inserting an initializer list into an array (#5693),2000.774,4175.507,11546.979,3189.357,3841.169,897.455,442.384,539.493,6839.688,836.213,2014.519,667.390,1685.418,1968.991,,, +170,bfea6f36d3,2026-09-30,5694,Fix to_msgpack() reading the inactive number union member (#5694),1997.465,4144.279,11549.781,3224.287,3855.120,899.475,441.995,539.924,6903.776,839.550,2019.396,664.487,1688.249,1954.211,,, +171,5b11a0282c,2026-09-30,5697,Exchange the CustomBaseClass subobject in basic_json::swap() (#5697),1987.983,4074.349,11483.114,3174.632,3841.604,893.256,438.279,535.885,6866.641,834.328,1996.396,660.828,1676.300,1936.210,,, +172,44a88d85be,2026-09-30,5698,Fix NLOHMANN_JSON_SERIALIZE_ENUM_STRICT's from_json message (#5698),1972.237,4125.945,11373.917,3158.716,3810.039,890.097,438.075,534.577,6867.432,842.045,2028.150,664.206,1681.353,1936.231,,, +173,bfe0f32d71,2026-09-30,5699,Fix std::terminate and null pointer access in input_stream_adapter (#5699),2014.371,4159.779,11667.416,3200.425,3876.135,911.716,448.788,543.198,6932.657,852.397,2056.197,668.764,1699.573,1984.479,,, +174,4bb1b14b06,2026-09-30,5702,Trim the compiler-appended NUL from wide/UTF string literals too (#5702),1979.723,4105.685,11603.562,3180.932,3846.698,906.188,443.969,534.534,6837.479,840.588,2006.043,654.851,1672.289,1965.408,,, +175,5bd766aa50,2026-09-30,5703,Move to_bson's binary subtype check into calc_bson_sizes (#5703),1999.606,4183.386,11588.646,3192.771,3844.425,900.169,446.795,541.406,6888.114,847.107,2022.637,663.480,1686.702,1974.777,,, +176,6ae17630a4,2026-09-30,5705,"Reject integral keys for contains(), find(), and count() at compile time (#5705)",2000.405,4157.120,11551.656,3197.417,3891.646,903.558,448.544,541.553,6914.203,848.561,2042.578,663.661,1691.910,1974.341,,, +177,7fd6895788,2026-09-30,5706,Hide a discarded container's content from the parser callback (#5706),1971.987,4075.362,11463.219,3164.909,3797.453,889.534,443.974,535.026,6857.323,835.793,2025.343,651.970,1687.670,1951.428,,, +178,66877675b1,2026-09-30,5719,"Check the iterator range for binary values in basic_json(first, last) (#5719)",1970.369,4104.886,11534.635,3168.601,3796.291,897.271,444.655,538.917,6826.990,842.445,2016.962,650.099,1673.940,1953.693,,, +179,2ea6d8c127,2026-09-30,5720,Require the found key to equal the looked-up key when comparing objects (#5720),2015.556,4188.886,11567.125,3251.890,3863.818,903.970,448.691,540.422,6912.255,843.774,2044.479,653.832,1676.634,1951.959,,, +180,67435c9c7e,2026-09-30,5721,Give a deep copy its type only after its container exists (#5721),1994.257,4160.658,11600.553,3218.367,3880.128,900.769,449.260,541.607,6899.448,841.635,2043.909,666.759,1694.646,1974.861,,, +181,6fc0d501f3,2026-09-30,5610,Move the user-defined string literals to and add JSON_NO_AUTOMATIC_UDLS (#5610),2021.715,4187.888,11647.594,3209.388,3925.060,911.591,450.961,544.303,6975.265,845.109,2049.853,670.295,1697.019,1987.428,,, +182,fdcc569eee,2026-09-30,5692,Use only documented StringType members in json_pointer (#5692),1965.264,4147.273,11378.188,3159.364,3787.758,889.399,447.397,535.062,6809.698,835.116,2015.202,656.241,1689.072,1955.238,,, +183,6a073dbae4,2026-09-30,5695,Give operator>> a strong exception-safety guarantee (#5695),2010.979,4211.982,11698.218,3205.401,3897.172,898.499,445.961,545.225,6898.870,847.042,2051.316,664.716,1706.401,1981.247,,, +184,3b6ae43c53,2026-09-30,5598,Add JSON_DISABLE_TUPLE_REFERENCE_CONVERSION to fix std::tuple conversions (#5598),2025.122,4195.468,11682.199,3204.529,3924.068,904.766,448.209,544.669,7041.276,849.103,2047.488,665.592,1695.987,1984.755,,, +185,7d7055ec50,2026-09-30,5723,Fix stack overflow converting deep values between specializations (#5723),1985.310,4117.099,11457.052,3157.591,3832.175,895.910,442.225,535.237,6864.375,833.653,2001.760,679.826,1674.819,1956.998,,, +186,35802e78d6,2026-09-30,5735,Remove dead Makefile targets; document macro_builder; tidy serve_header (#5735),2004.498,4163.065,11590.594,3184.810,3902.229,906.494,447.394,539.542,6923.969,845.596,2044.457,666.187,1692.019,1977.030,,, +187,c261578431,2026-09-30,5730,Deduplicate binary reader/writer helpers and fix stale comments (#5730),1968.723,4154.789,11441.718,3160.367,3870.578,889.545,443.139,535.314,6850.886,837.281,2024.650,660.842,1691.628,1978.000,,, +188,1d675cdb46,2026-09-30,5733,Fix CI jobs that check less than they claim; move arm64 to GitHub (#5733),1995.704,4206.185,11691.125,3227.622,3966.104,909.748,449.770,541.566,6951.938,852.506,2009.592,663.701,1686.265,1983.285,,, +189,a32f61eb98,2026-09-30,5678,Fix update() and merge_patch() when the argument is *this or one of its members (#5678),2014.810,4157.101,11636.135,3210.326,3924.659,899.344,448.133,541.371,6913.401,841.487,2038.263,667.515,1697.503,1980.686,,, +190,18dd5663b0,2026-10-01,5548,Diff deeply nested values without recursing per nesting level (#5548),1990.931,4152.500,11469.697,3204.740,3985.593,903.248,448.981,540.974,6929.740,847.493,2040.504,665.352,1698.903,1981.438,,, +191,3926fcaac3,2026-10-01,5729,"Deduplicate serializer dump code; fix stale includes, docs, and lint (#5729)",1999.556,4178.489,11581.000,3208.657,3876.755,891.669,452.445,538.525,6893.078,846.721,2042.010,662.154,1689.653,1975.503,,, +192,cff0a61369,2026-10-01,5731,Share DOM SAX position handling; fix stale parser and lexer comments (#5731),2017.231,4112.604,11588.291,3157.047,3890.343,902.329,458.713,539.539,7020.568,847.995,2073.873,654.562,1696.823,1978.381,,, +193,e400780533,2026-10-01,5745,Re-amalgamate single_include (#5745),1980.338,4100.659,11418.854,3166.896,3835.432,885.298,452.886,538.053,6888.203,824.666,2031.410,650.846,1668.952,1949.107,,, +194,6aec1a085f,2026-10-02,5677,Report BON8 input that ends after a UTF-8 lead byte as truncated (#5677),2003.321,4099.066,11614.364,3159.029,3849.857,899.995,459.542,539.436,6904.474,843.221,2071.503,652.303,1685.702,1972.556,,, +195,4d46bce4e1,2026-10-02,5750,Fix CI on develop (#5750),1962.685,4025.677,11277.718,3103.052,3785.213,894.610,448.583,533.887,6870.062,834.856,2020.231,658.339,1674.874,1954.242,,, +196,63c10a51fc,2026-10-02,5638,"Review and extend the documentation, and check it in CI (#5638)",1968.954,4036.851,11425.135,3104.359,3797.852,902.519,451.067,537.668,6910.375,852.438,2043.889,674.938,1701.314,1971.711,,, +197,6d0867a85f,2026-10-02,5751,Make comparisons with scalars noexcept only when the conversion is (#5751),2008.360,4091.453,11376.562,3144.575,3820.110,903.850,455.980,538.035,6851.885,841.549,2010.128,667.182,1690.586,1970.957,,, +198,d11e89471b,2026-10-04,5606,Assert on missing array indices in const operator[] and document the JSON pointer case (#5606),2000.121,4075.695,11475.520,3139.456,3837.211,900.437,453.703,538.895,6921.432,845.064,2027.417,663.969,1679.148,1964.082,,, +199,df27cc3d4c,2026-10-04,5682,Add scalar-on-left overloads for legacy discarded comparisons in C++20 (#5682),2010.838,4092.432,11495.625,3125.726,3873.167,911.281,456.807,538.386,6911.630,844.451,2033.630,688.219,1691.407,1971.697,,, +200,6f2048cd5d,2026-10-04,5685,Accept lvalues in ordered_map::emplace's value parameter (#5685),1999.816,4095.006,11445.740,3136.685,3924.146,918.692,458.336,542.423,6968.520,848.783,2037.096,667.772,1708.274,2002.449,,, +201,0d01d6ae90,2026-10-04,5686,Classify leaves with operator<=> itself past the nesting bound (#5686),1983.320,4116.981,11466.250,3192.969,3923.208,914.596,456.625,539.558,6975.537,849.252,2056.035,701.221,1716.493,1983.908,,, +202,b730946432,2026-10-04,5689,Fix key types convertible to std::string_view breaking lookups (#5689),1990.288,4060.487,11377.709,3107.537,3803.857,892.191,451.727,538.045,6896.480,844.989,2018.884,658.649,1676.566,2023.638,,, +203,49cd427196,2026-10-04,5690,"Copy-construct the base class of a deep copy's elements, not assign it (#5690)",1983.320,4033.156,11333.843,3138.547,3882.878,894.785,454.473,538.337,6918.619,841.205,2072.151,655.193,1693.871,1977.288,,, +204,756b28c2b8,2026-10-04,5696,Keep a NUL byte ending a // comment as the end of input (#5696),1985.931,4099.331,11579.875,3148.560,3872.422,896.276,451.436,536.774,6901.156,845.631,2014.820,648.054,1685.199,1971.762,,, +205,1edf0ef041,2026-10-04,5700,"Fix value(json_pointer, default) aborting under JSON_NOEXCEPTION (#5700)",1987.233,4076.133,11442.625,3124.099,3888.688,898.266,454.349,539.423,6950.140,852.803,2030.279,654.586,1691.751,1978.197,,, +206,a212d3b2e4,2026-10-04,5722,Preserve the object comparator's state in a deep copy past the nesting bound (#5722),2000.138,4113.471,11391.854,3151.006,3866.904,905.695,455.384,539.504,6929.818,842.213,2029.619,674.438,1697.044,1983.856,,, +207,1df1e8a845,2026-10-04,5591,Make cross-string-type basic_json conversion explicit without implicit conversions (#5591),2000.533,4107.994,11370.989,3160.854,3881.635,897.877,455.201,540.307,6927.167,840.130,2053.458,664.118,1697.618,1982.876,,, +208,38a2db260c,2026-10-04,5728,Remove dead metaprogramming and duplicated code in traits and pointers (#5728),1971.450,4040.828,11319.270,3109.521,3819.505,903.606,451.137,538.725,6825.640,843.951,2016.652,663.160,1684.596,1958.896,,, +209,c5650eaa3c,2026-10-04,5704,Reject malformed UTF-16/UTF-32 units in wide-string input (#5704),2006.689,4133.594,11469.209,3180.930,3881.633,902.924,456.572,540.557,6992.385,850.474,2025.267,652.367,1680.025,1957.468,,, +210,40021f38fb,2026-10-04,5589,Add basic_json::as_base_class and document name conflicts with custom base classes (#5589),2012.673,4147.193,11453.250,3167.448,3892.542,910.149,457.238,541.642,6998.084,852.247,2049.759,652.868,1688.229,1972.508,,, +211,73e9eae3c1,2026-10-04,5746,Add an error_handler parameter for UTF-8 to the binary readers and writers (#5746),2013.845,4130.237,11456.041,3135.688,3818.956,892.111,449.408,538.308,6825.213,835.872,1495.468,697.054,1679.760,1970.285,,, +212,d8d47be4a5,2026-10-04,5754,Fix CI on develop after merging the ready-to-merge PRs (#5754),2013.191,4119.948,11573.656,3173.029,3941.703,912.201,459.208,541.264,7013.364,848.908,1514.681,699.273,1685.391,1984.652,,, +213,6b4b825af2,2026-10-04,5607,Handle numbers that do not fit narrow number types in the binary readers (#5607),2009.190,4044.127,11362.677,3127.688,3827.841,892.131,449.386,534.731,6915.360,829.562,1487.640,697.206,1688.237,1952.157,,, +214,8c1f60a45e,2026-10-04,5600,Store maps with enum keys as objects (opt-in) (#5600),2008.560,4128.059,11523.011,3187.763,3843.177,912.520,456.427,537.514,6859.010,839.685,1501.515,696.508,1688.848,1968.910,,, +215,c5a7a4b46d,2026-10-04,5688,"Add deprecated from_bon8/from_bjdata(ptr, len) overloads (#5688)",2030.310,4108.505,11412.907,3141.836,3899.435,904.756,455.298,541.069,6926.151,846.240,1504.193,700.820,1693.983,1987.713,,, +216,23d3b373e1,2026-10-04,5755,Add JSON_DELETE_DEPRECATED_FUNCTIONS to delete the deprecated functions (#5755),2018.708,4106.015,11533.729,3134.945,3826.409,899.623,452.833,538.544,6846.443,830.555,1509.060,705.724,1692.819,1971.476,,, +217,8a142bc436,2026-10-06,5764,"Fix CI on develop after #5600, #5607, and #5755 (#5764)",2017.533,4151.854,11526.334,3195.240,3884.953,907.457,456.697,540.114,6891.136,844.145,1517.241,699.051,1690.344,1972.104,,, +218,5379e04ce4,2026-10-06,5761,Throw type_error.321 when serializing discarded values to binary formats (#5761),1999.156,4055.300,11427.521,3153.581,3836.060,891.927,455.413,540.938,6883.093,853.094,1510.099,701.888,1686.695,1982.806,,, +219,0a365865f9,2026-10-06,5762,Make basic_json destruction allocation-free and non-recursive (#5762),2024.289,4081.646,10719.428,3112.958,3900.503,924.900,462.081,540.765,6965.453,850.019,1511.381,669.802,1620.269,1912.078,,, +220,5ecb704f6b,2026-10-06,5592,Speedup; check for the expected separator before the lexer's token switch (#5592),1920.428,3801.979,10139.250,2891.862,3833.159,823.859,451.951,540.310,6885.672,843.570,1512.778,668.793,1608.501,1885.206,,, +221,6d4543e743,2026-10-06,5767,Fix destroy() for ObjectTypes without reverse iteration (#5767),1929.030,3843.266,10232.541,2911.458,3926.867,830.030,450.002,542.043,6938.890,846.589,1515.211,675.264,1622.245,1904.006,,, +222,9b179cee1e,2026-10-06,5770,Handle all value_t enumerators in has_no_children() (#5770),1931.275,3794.497,10155.125,2891.927,3871.831,829.432,446.176,535.126,6909.625,833.114,1498.945,677.571,1615.785,1884.043,,, +223,69874e4544,2026-10-06,5760,Fix NUL bytes in UBJSON/BJData high-precision numbers (#5760),1924.896,3853.081,10247.750,2926.515,3872.041,835.227,453.025,541.002,6919.037,842.240,1521.169,674.487,1607.441,1880.884,,, +224,162e13b86f,2026-10-06,5758,Add with_*_t alias templates to create basic_json types with changed template parameters (#5758),1938.268,3842.126,10289.968,2914.104,3962.414,834.967,455.536,543.630,6969.266,850.082,1530.064,678.054,1635.303,1916.763,,, +225,21a69230bd,2026-10-06,5585,Create a value before giving it its type (#5585),1918.165,3829.354,10247.656,2914.619,3874.909,832.287,451.252,543.791,6873.995,839.514,1521.088,699.604,1615.742,1894.449,,, +226,ff6f3d7d4a,2026-10-07,5766,Reject nested indefinite-length CBOR string chunks (#5766),1926.074,3868.810,10256.823,2919.802,3957.807,832.129,457.241,543.640,7024.745,847.770,1522.612,705.623,1634.212,1918.402,,, +227,1c754cfe31,2026-10-07,5701,Copy a pair-shaped array value under JSON_BRACE_INIT_COPY_SEMANTICS (#5701),1930.652,3828.195,10263.927,2921.570,3901.404,832.628,455.652,541.701,6909.037,843.563,1512.309,704.354,1623.254,1909.917,,, +228,24dd7c63f0,2026-10-07,5777,Re-amalgamate after #5740 (#5777),1908.632,3833.930,10278.146,2905.443,3935.422,838.465,454.491,545.605,6905.047,839.087,1519.816,709.223,1618.643,1886.706,,, +229,30e542e52a,2026-10-07,5776,Declare the namespace-scope constants in to_chars.hpp inline (#5776),1934.934,3823.724,10229.636,2916.698,3929.565,829.102,452.279,543.038,6951.995,837.059,1524.049,720.722,1619.823,1903.938,,, +230,367336c83d,2026-10-07,5779,Fix CI on develop after #5585 (#5779),1928.921,3780.927,10153.395,2883.604,3877.750,824.488,451.999,543.662,6927.395,835.495,1506.387,694.058,1622.903,1907.732,,, +231,069ace74af,2026-10-07,5707,"Round-trip BJData ND-array annotations exactly (single precision, key order) (#5707)",1937.387,3843.716,10184.302,2923.323,3936.570,827.610,456.123,543.169,6907.756,847.020,1515.229,696.202,1626.001,1901.142,,, +232,43fe8928e1,2026-10-07,5781,Stop binary writers overflowing the stack on deep values (#5781),1961.046,3868.274,10300.573,2899.792,3982.526,845.594,461.395,543.298,7059.093,846.696,1522.728,697.717,1623.846,1921.370,,, +233,a5f5d3059b,2026-10-07,5772,Make std::hash consistent with operator== for numbers (#5772),1949.220,3857.159,10170.427,2890.052,3981.349,840.234,459.334,541.947,6953.786,843.495,1509.713,703.339,1627.086,1911.653,,, +234,a269794db7,2026-10-08,5782,Use MSVC intrinsics for full multiplication (#5782),1931.869,3817.742,10159.666,2902.820,3903.263,840.740,455.827,540.621,6884.562,843.272,1513.776,702.028,1614.459,1901.008,,, +235,69a0c1b82c,2026-10-09,5328,Avoid allocating temporary basic_json for cbor and msgpack object keys (#5328),1909.579,3777.560,10065.682,2852.732,3897.659,833.289,459.856,540.631,6882.354,833.998,1498.288,314.059,1609.343,1906.714,,, +236,374dfe4f0f,2026-10-09,5791,Keep converted object keys alive while writing UBJSON and BJData (#5791),1911.274,3827.188,10159.553,2886.568,3890.388,833.632,459.800,542.649,6954.547,838.507,1509.324,314.357,1613.655,1892.838,,, +237,44e8597701,2026-10-09,5792,Flatten deeply nested values without recursing per nesting level (#5792),1940.103,3811.779,10139.396,2865.050,3874.896,835.001,459.686,544.065,6866.943,836.862,1496.298,314.617,1614.837,1891.741,,, +238,2913e96433,2026-10-09,5793,Unflatten in time and memory linear in the pointer depth (#5793),1908.453,3785.099,10262.147,2878.961,3920.657,845.619,459.806,543.998,6874.813,832.659,1486.901,312.175,1589.620,1863.981,,, +239,84ee12a67c,2026-10-09,5797,Make UTF8_ACCEPT/UTF8_REJECT inline variables to avoid TU-local exposure in modules (#5797),1922.691,3792.849,10162.656,2871.562,3887.719,836.308,456.522,542.577,6912.807,840.369,1497.447,313.683,1609.988,1899.591,,, +240,1649d9eda4,2026-10-11,5783,Use and extend the detail helpers to remove duplicated code (#5783),1916.258,3804.242,10297.344,2913.118,3847.662,836.519,453.270,538.750,6953.015,832.774,1506.068,324.715,1596.974,1875.878,,, +241,2cca04ae6f,2026-10-11,5800,Report one-character non-numeric array indices like longer ones (#5800),1932.327,3803.750,10135.438,2881.041,3894.622,840.016,458.569,538.037,6842.948,829.444,1499.722,324.811,1607.194,1875.851,,, +242,74a03dec84,2026-10-11,5802,Throw type_error.321 for discarded values in to_bon8 (#5802),1924.695,3814.854,10084.239,2873.180,3848.498,839.406,450.575,534.181,6893.005,834.300,1503.369,324.389,1607.323,1887.871,,, +243,e8b747e4bc,2026-10-11,5808,Re-amalgamate single_include after #5795 and #5806 (#5808),1937.995,3840.622,10221.990,2896.024,3885.568,834.394,458.874,539.771,6906.182,839.793,1517.689,324.800,1609.838,1890.885,,, +244,40a62947d3,2026-10-12,4965,support JSON Merge Patch (RFC 7396) diff creation (#4965),1932.021,3822.188,10248.416,2924.560,3980.331,842.462,459.010,540.237,6978.526,838.941,1529.508,326.435,1636.153,1917.472,,, +245,f23830b65e,2026-10-13,5809,Derive the lexer position lazily for seekable input (#5809),1814.075,3368.028,9974.755,2807.532,3943.229,647.639,457.018,551.206,6954.166,843.355,1522.762,315.716,1618.953,1903.816,,, +246,aa9620fb15,2026-10-13,5810,Skip the indentation after a newline 8 bytes at a time (#5810),1719.629,2959.344,10139.771,2799.555,3939.292,592.867,465.821,545.858,6968.068,849.167,1519.774,315.385,1629.981,1907.253,,, +247,e839bbd1c0,2026-10-13,5813,"Export client-facing headers from json.hpp for IWYU / include-cleaner, and check clients in CI (#5813)",1725.742,2949.070,10136.603,2821.591,3919.927,589.957,463.642,544.851,6987.427,843.151,1517.562,314.425,1619.671,1903.613,,, +248,5b4c8f9b00,2026-10-14,5738,"Speed up the lexer: own float parser, string scan, and \u table (#5738)",1615.668,2934.565,8763.651,2794.503,3928.286,559.107,374.921,516.391,6937.140,815.776,1513.238,325.043,1625.226,1912.905,,, +249,91a7a94cc6,2026-10-14,5634,"Write doubles with the shortest digits (Żmij), digits in registers (#5634)",1613.126,2929.599,8708.599,2783.954,3874.248,548.449,363.632,468.127,2421.176,745.466,1511.406,323.743,1614.195,1900.392,,, +250,a6e9785ab6,2026-10-15,5831,Look up the key only once in the callback parser's key() (#5831),1589.309,2938.557,8798.823,2767.872,3882.154,547.301,364.257,467.833,2395.851,744.716,1506.830,316.192,1623.771,1899.458,,, +251,0c4e7c103c,2026-10-15,5834,Make flatten() linear for ordered_json (#5834),1625.754,2936.203,8755.088,2793.081,3879.740,557.529,366.745,468.359,2418.388,746.210,1524.325,315.954,1614.677,1898.544,,, +252,0a4b3a84ee,2026-10-15,5836,"Speed up JSON pointer parsing, formatting, comparison and lookup (#5836)",1622.914,2915.901,8819.073,2783.990,3922.758,558.429,366.216,470.019,2427.606,747.168,1526.881,316.972,1625.611,1912.601,,, +253,6ba53e63dd,2026-10-15,5833,Speed up array-to-container from_json (#5833),1626.155,2920.747,8777.989,2782.870,3988.206,560.154,367.695,469.892,2427.262,749.301,1529.474,316.399,1631.810,1915.217,,, +254,ca7740fef2,2026-10-15,5837,Move elements when converting rvalue containers to JSON (#5837),1615.747,2928.778,8765.792,2773.062,3926.456,561.034,367.498,469.807,2424.344,748.423,1523.215,316.339,1624.374,1894.455,,, +255,4530d9681c,2026-10-15,5832,Fix quadratic parent updates for ordered_json with JSON_DIAGNOSTICS (#5832),1618.937,2911.378,8794.094,2763.191,3886.974,555.789,366.475,469.275,2415.231,746.755,1512.590,315.856,1618.226,1899.284,,, +256,91869983a0,2026-10-15,5840,Make the value lifecycle inline-friendly (#5840),1595.075,2832.274,8420.390,2690.050,3876.841,555.209,366.118,469.783,2424.335,747.931,1485.914,314.048,1549.787,1775.823,,, +257,6a27b9acb2,2026-10-15,5838,Construct remaining hot-path exceptions out of line so core functions inline (#5838),1589.535,2824.360,8466.755,2699.690,3888.990,557.917,364.879,471.601,2422.995,746.543,1498.371,314.978,1539.656,1797.752,,, +258,4f11512641,2026-10-15,5835,"Speed up container destruction, iterator access and diff() (#5835)",1515.750,2714.756,8639.213,2574.698,3805.860,561.827,366.793,469.680,2421.769,748.727,1383.744,317.248,1446.525,1705.789,,, +259,ed86019a21,2026-10-15,5839,Move binary reader error construction out of line (#5839),1507.907,2766.774,8597.870,2622.289,3767.638,553.026,362.644,468.482,2408.731,744.936,1362.080,324.833,1445.531,1703.216,,, +260,6261d1e432,2026-10-15,5842,Construct the JSON Patch exceptions out of line (#5842),1532.015,2756.867,8670.677,2617.989,3869.133,558.865,365.834,469.932,2435.279,750.295,1380.628,327.010,1453.340,1705.232,,, +261,ce28c722bc,2026-10-15,5815,Allow the same type for number_integer_t and number_unsigned_t (#5815),1533.045,2775.807,8543.755,2615.412,3776.721,553.127,365.866,471.174,2433.384,745.309,1372.372,328.405,1452.848,1716.649,,, +262,91b9ee2602,2026-10-15,5829,Update cppcheck to 1.5.2 (Cppcheck 2.20) and fix its new findings (#5829),1516.450,2748.961,8571.521,2599.870,3807.744,557.303,367.220,472.209,2421.389,743.889,1379.405,326.087,1451.926,1710.506,,, +263,dcb96e82fb,2026-10-15,5822,Escape characters below 0x60 from a table straight into the write buffer (#5822),1525.777,2734.963,8524.771,2588.289,3790.726,557.907,360.057,463.574,2430.626,743.382,1375.816,325.308,1439.947,1695.307,,, +264,b41228c227,2026-10-15,5843,Fix AppVeyor failures from the Zmij double writer (#5843),1497.922,2690.614,8406.906,2582.482,3726.195,548.374,352.932,459.663,2398.953,737.639,1347.507,322.827,1429.640,1682.775,,, +265,4da73dc5ff,2026-10-15,5858,Fix compiler warnings on develop from the API performance stack (#5858),1523.138,2731.825,8474.073,2593.149,3782.769,553.758,359.180,462.915,2380.246,739.049,1346.755,324.909,1441.408,1698.043,,, +266,5683f5e024,2026-10-15,5859,Define less_general() only without three-way comparison (#5859),1504.689,2739.172,8603.693,2605.159,3797.057,560.213,358.865,464.429,2423.456,742.207,1374.000,326.702,1447.488,1712.379,,, +267,cd6b428ef0,2026-10-16,5860,Fix CI failures on develop from the API performance stack (#5860),1510.160,2732.695,8564.510,2603.242,3810.518,556.015,362.409,466.047,2437.364,745.153,1377.439,325.753,1444.892,1699.255,,, +268,cc8334113c,2026-10-19,5866,Do not use Qt's keyword macros as identifiers (#5866),1498.858,2732.644,8535.328,2587.258,3787.993,548.267,362.607,464.487,2403.147,735.633,1368.831,323.997,1442.709,1686.963,,, +269,4da932f518,2026-10-19,5828,Make parsing from std::istream and FILE* fast (#5828),1499.289,2723.605,8397.787,2597.237,1956.330,553.182,363.501,458.898,2397.342,733.054,1362.816,320.346,1429.314,1698.294,,, +270,122d718330,2026-10-21,5620,"Add json_document and json_view: node index, parser, and document (#5620)",1520.651,2751.099,8527.135,2588.494,1994.466,560.384,362.257,461.917,2393.611,741.198,1362.167,328.620,1441.211,1691.238,230.700,389.375,976.277 +271,43e97b629b,2026-10-21,5622,"Add element access, iteration, values, and JSON pointers to json_view (#5622)",1493.875,2687.307,8383.354,2567.078,1986.311,547.934,353.938,462.380,2400.331,737.098,1374.560,322.012,1445.504,1686.421,231.353,386.679,958.441 +272,4c3a3eb5b5,2026-10-21,5624,Add dump() and comparisons to json_view (#5624),1502.926,2714.747,8522.161,2608.565,2001.856,552.014,359.510,462.656,2393.110,732.091,1366.524,324.313,1442.119,1682.564,228.836,393.082,976.976 +273,3ff37ead75,2026-10-21,5628,Scan json_view strings with SIMD and index large objects (#5628),1514.580,2728.619,8406.562,2609.758,2041.176,556.017,357.330,462.573,2383.158,733.939,1368.057,324.391,1442.711,1700.884,177.833,367.806,950.071 +274,823eee3b59,2026-10-21,5630,"Add editable json_documents: set, push_back, insert, and erase (#5630)",1498.134,2754.810,8537.062,2588.474,1994.441,556.670,353.931,463.646,2392.987,740.023,1357.930,324.129,1441.622,1698.022,181.404,376.470,962.253 +275,4971200b54,2026-10-21,5632,Add images of json_documents: save() and load() (#5632),1494.231,2681.211,8425.834,2543.276,1984.849,548.146,354.762,460.440,2414.747,734.679,1363.841,321.349,1421.806,1670.134,177.777,368.631,949.292 +276,4155e2938f,2026-10-21,5633,Write json_view's dump() without a call or conversion per token (#5633),1500.247,2712.357,8612.193,2597.211,2012.860,558.559,359.889,462.371,2412.896,739.048,1372.456,324.070,1441.546,1697.571,180.320,371.514,954.088 +277,08d7fbb0a1,2026-10-21,5874,Read FILE* input with std::fgetc under MemorySanitizer (#5874),1543.124,2758.211,8565.730,2584.453,2046.255,558.522,358.865,464.542,2421.435,743.715,1374.005,324.687,1462.572,1700.819,180.714,372.601,965.361 +278,0cbe95ffa2,2026-10-21,5824,"Speed up writing CBOR and MessagePack, above all integer arrays (#5824)",1501.550,2706.534,8707.120,2584.807,2039.728,551.688,360.692,466.024,2397.182,742.777,1361.847,201.488,1429.223,1682.436,178.641,371.038,946.389 +279,bd5227c2ec,2026-10-21,5805,Fix silent wrong results for custom number types (#5805),1511.036,2733.325,8786.406,2588.906,2000.483,553.456,360.458,465.989,2398.381,743.338,1370.524,201.168,1444.051,1701.345,179.776,372.557,954.733 +280,e13e976801,2026-10-21,5851,Accumulate number digits while scanning them (#5851),1501.737,2667.570,7762.365,2523.289,1987.518,545.368,359.336,462.056,2407.652,739.226,1368.104,200.632,1440.390,1688.117,178.721,374.278,956.353 +281,5e13f834c1,2026-10-21,5852,Copy the text of float tokens only when a SAX handler asks for it (#5852),1512.318,2655.312,7314.354,2515.044,2019.288,547.993,362.038,463.788,2410.262,745.839,1364.898,208.969,1450.422,1713.902,179.198,371.996,968.699 +282,e9970066c5,2026-10-21,5853,"Scan strings with SSE2, SSSE3, or NEON (#5853)",1488.705,2654.404,7339.589,2551.050,2015.716,486.719,341.381,475.591,2434.369,756.973,1376.754,208.797,1449.981,1706.044,182.358,379.153,968.355 +283,fa37b952df,2026-10-21,5854,Match literals without a stack array and keep parser states in bytes (#5854),1471.894,2619.164,7285.000,2485.883,1949.380,471.534,347.785,475.287,2413.552,751.487,1374.254,207.522,1443.177,1699.611,181.643,377.971,977.268 +284,52fecc1fab,2026-10-21,5855,Move object keys into the value when parsing (#5855),1392.599,2628.688,7362.562,2485.293,1882.663,471.968,339.711,473.663,2414.903,751.376,1319.857,200.185,1448.057,1705.675,179.410,371.865,963.306 +285,6aefd8c662,2026-10-21,5878,Copy short printable-ASCII strings without starting the scanner (#5878),1399.384,2664.401,7232.557,2535.425,1922.431,470.185,320.230,418.440,2446.082,696.786,1317.889,201.955,1447.217,1701.135,180.860,376.217,968.215 +286,7c26e99ac5,2026-10-21,5879,Copy ASCII strings in the view's dump with ensure_ascii (#5879),1361.839,2610.182,7187.115,2471.708,1853.592,464.274,313.821,414.316,2388.840,687.851,1318.253,200.399,1434.986,1697.487,178.601,368.595,947.799 +287,3e031b06a5,2026-10-21,5876,"Use standard library helpers where available, with C++11 fallbacks (#5876)",1381.599,2592.122,7336.719,2499.994,1848.434,467.895,315.257,415.788,2430.769,692.250,1311.164,200.424,1444.303,1698.311,180.149,374.484,961.091 +288,9bf531436f,2026-10-21,5861,"Speed up flatten(), unflatten(), diff() and patch() (#5861)",1379.301,2613.219,7170.433,2472.628,1853.035,467.161,315.217,412.303,2406.597,683.062,1298.512,198.681,1434.089,1682.912,177.511,368.116,947.255 +289,9855db81f3,2026-10-22,5881,Silence macro-to-enum for the endianness macros (#5881),1369.150,2613.547,7209.787,2476.232,1841.347,467.780,316.011,416.187,2415.504,686.920,1304.263,200.868,1441.243,1694.603,178.739,368.123,948.579 +290,aa0b371f7c,2026-10-23,5884,"Faster std::istream input: inline get-area path, binary reads, unbuffered streambufs (#5884)",1375.030,2569.362,7218.375,2484.425,1743.476,470.240,319.245,403.750,2436.034,679.759,1319.508,201.531,1443.354,1698.057,180.133,372.206,946.488 +291,86d5c56c54,2026-10-23,5886,"Speed up flatten(), unflatten(), diff() and patch() further (#5886)",1386.169,2622.331,7281.551,2495.923,1700.794,472.317,317.282,403.416,2409.097,682.507,1320.319,200.963,1444.908,1697.678,179.138,375.751,954.573 +292,dec0e9b749,2026-10-23,5883,"Write strings, scalars and indentation in dump() with fewer calls (#5883)",1383.235,2621.847,7151.412,2491.644,1732.469,473.034,211.016,288.685,2276.953,416.068,1318.490,200.574,1452.099,1701.256,180.064,372.324,960.583 +293,6fa14d8f95,2026-10-23,5885,Write binary formats through reserved sink space and number runs (#5885),1366.956,2600.914,7176.994,2465.343,1691.333,462.351,210.662,285.259,2301.553,417.589,1324.173,136.601,1429.240,1687.947,179.319,368.881,962.518 +294,0b924f5375,2026-10-23,5887,Split and print JSON pointers in one pass (#5887),1377.526,2597.394,7242.688,2464.445,1739.841,466.382,211.716,288.202,2319.771,418.288,1318.592,138.744,1441.983,1706.639,179.684,372.087,976.078 +295,e9c4fe3cdf,2026-10-24,5889,json_view: indented and ensure_ascii dumps through the fast writer (#5889),1393.007,2627.226,7248.287,2491.551,1769.642,469.976,211.665,291.327,2305.655,420.168,1319.431,139.577,1457.965,1712.927,182.274,377.741,983.284 +296,dd7686a269,2026-10-24,5888,"json_view: faster reading, indexing and C-stream parsing (#5888)",1382.444,2607.026,7161.365,2472.635,1701.277,468.438,209.963,287.564,2276.329,415.511,1311.194,137.875,1439.700,1696.646,177.624,367.131,977.802 +297,9c50bdd140,2026-10-24,5882,Make the DOM parser cheaper per container and per member (#5882),1392.673,2461.082,6068.625,2334.706,1696.950,475.550,209.313,291.776,2300.287,416.349,1317.581,138.194,1441.209,1707.720,178.751,367.339,978.031 +298,01139dc192,2026-10-24,5905,Silence an Infer false positive in json_view materialize (#5905),1381.470,2463.013,6078.010,2335.216,1709.088,475.718,211.023,291.212,2275.302,418.098,1320.441,137.572,1443.434,1696.436,175.969,365.658,975.272 +299,fe5e401870,2026-10-25,5901,Consolidate duplicated detail helpers and bodies (#5891) (#5901),1395.881,2462.765,6066.698,2327.708,1706.745,474.184,212.683,289.321,2283.842,417.657,1314.153,138.084,1442.796,1696.588,177.144,364.092,961.847 +300,3eae5680d6,2026-10-25,5902,"from_json: number-first dispatch for arithmetic types, loop for std::array (#5902)",1397.343,2477.060,6080.864,2340.781,1711.774,481.523,213.618,289.360,2289.503,417.517,1313.541,138.271,1440.822,1695.785,178.731,367.293,983.555 +301,2a56afc714,2026-10-25,5916,Fix VS 2015 build of the DOM parser (#5882) (#5916),1373.929,2476.139,6056.797,2341.890,1749.167,482.883,215.107,290.053,2292.495,417.441,1320.640,137.868,1445.227,1702.064,177.912,367.669,984.794 +302,d6431a9104,2026-10-26,5918,Throw out_of_range.411 for JSON Patch add into a null parent (#5917) (#5918),1371.635,2445.905,6017.636,2314.178,1696.447,478.134,210.996,287.397,2278.718,414.186,1291.197,137.374,1436.212,1682.197,175.790,361.130,962.070 +303,81d66291c5,2026-10-26,5358,Create objects for pointer tokens that cannot be array indices (#5358),1382.909,2465.649,6052.114,2332.556,1711.721,479.927,213.359,289.913,2289.412,417.201,1311.695,137.572,1437.168,1696.302,177.495,367.526,963.924 +304,d5da84249f,2026-10-26,5927,"Skip CBOR tags in front of map keys, validate indefinite text chunks, fix CBOR docs (#5927)",1374.553,2476.464,6076.834,2346.075,1744.224,483.375,211.423,291.148,2287.800,420.247,1320.015,137.369,1449.068,1704.785,177.459,365.004,963.908 +305,0c3570d44a,2026-10-26,5929,Validate the UBJSON/BJData container type marker for count 0; fix UBJSON docs (#5929),1381.462,2462.578,6055.183,2329.970,1713.086,484.962,210.683,290.024,2260.371,416.895,1319.093,137.782,1453.495,1706.801,177.031,361.025,962.952 +306,dc768a7d5d,2026-10-26,5930,Fix BSON binary subtype 0x02 and check the declared document size earlier (#5930),1386.856,2463.400,6054.255,2337.105,1746.441,485.425,214.508,290.954,2314.702,418.769,1325.164,139.255,1452.125,1718.313,179.005,367.327,977.054 +307,60107eda3f,2026-10-26,5931,"Read BJData ND-arrays of float16, restrict ND-array char to 0..127, correct BJData docs (#5931)",1389.051,2464.457,6049.084,2336.727,1710.473,483.159,213.007,288.990,2285.073,417.663,1317.537,137.893,1443.301,1697.126,178.706,364.051,962.202 +308,731f1b5bfb,2026-10-26,5919,"json_view: view_mode enum instead of bool Editable, node_kind for node::kind (#5919)",1381.664,2428.529,5978.713,2333.485,1746.571,479.163,211.036,288.957,2281.592,416.990,1308.904,137.486,1447.665,1691.427,175.453,363.704,963.371 +309,4cdfa6d0f4,2026-10-26,5920,json_view: no save() and load() on big-endian targets (#5920),1390.901,2477.841,6102.474,2337.925,1777.003,485.382,214.373,290.946,2303.147,421.178,1321.976,139.942,1446.726,1704.816,179.928,367.825,976.532 +310,5a2819c940,2026-10-26,5921,json_view: one SIMD macro set with the library (#5921),1393.409,2479.841,6073.011,2350.336,1785.087,485.658,214.651,292.044,2304.448,418.026,1326.212,138.942,1468.981,1719.505,179.174,368.704,982.764 +311,e5fe4ebdd2,2026-10-26,5922,json_view: from_json() of the macros reads views directly (#5922),1384.612,2467.078,6056.328,2337.309,1742.751,482.554,213.389,289.543,2299.118,417.521,1309.673,138.237,1448.628,1704.550,177.925,365.163,984.351 +312,9e67bfa115,2026-10-26,5923,json_view: the library's std conversions read views directly (#5923),1395.103,2482.517,6066.604,2350.538,1751.603,482.504,213.399,290.185,2306.188,420.406,1317.415,138.730,1444.367,1708.720,178.016,369.111,975.875 +313,21ec9bd801,2026-10-27,5915,json_view: fewer allocations when parsing small documents (#5915),1388.282,2469.118,6066.495,2340.391,1744.901,478.745,214.934,292.970,2309.264,422.842,1323.237,138.541,1454.151,1707.314,178.058,366.514,958.110 +314,2bb2feeef4,2026-10-27,5942,"Fix the red CI on develop (json_view pointer tokens, bson, from_json tests, Infer, IWYU) (#5942)",1391.981,2468.266,6068.495,2335.707,1751.527,482.908,214.285,291.291,2300.453,417.876,1328.089,138.010,1454.934,1701.845,178.250,367.182,964.917 +315,78a0687147,2026-10-27,5940,Strong exception guarantee for operator[](json_pointer) (#5940),1379.414,2467.492,6109.136,2335.309,1723.090,480.260,216.197,292.075,2313.798,415.444,1314.530,138.314,1449.629,1698.792,177.628,364.844,965.314 +316,candidate,,,develop + open pull requests,1383.171,2465.866,6049.771,2336.394,1725.095,480.882,214.967,292.621,2289.324,418.010,1320.853,137.793,1439.615,1710.333,176.356,364.229,962.166 diff --git a/docs/mkdocs/scripts/figures/perf_blog.py b/docs/mkdocs/scripts/figures/perf_blog.py new file mode 100644 index 000000000..48c45925a --- /dev/null +++ b/docs/mkdocs/scripts/figures/perf_blog.py @@ -0,0 +1,360 @@ +"""Draws the figures of home/blog/how_3_13_0_got_faster.md as inline SVGs into docs/images/. + +Generated files: + +- perf_blog_overview.svg: speedup of 3.13.0 over 3.12.0 per workload (Google Benchmark suite) +- perf_blog_timeline_.svg: speedup over 3.12.0 of eight workloads at every develop commit that changed + json.hpp or json_view.hpp since 3.12.0, one panel each +- perf_blog_swar.svg: the eight-byte string test on one word of input +- perf_blog_cliffs.svg: operations that were quadratic or worse, 3.12.0 against 3.13.0 (log scale) +- perf_blog_view.svg: parse throughput of json_document, yyjson, simdjson's DOM, Boost.JSON and json::parse + +The numbers are copied from the tables of the post (the timeline from data/perf_timeline.csv, which +`tests/benchmarks/history/history.py summarize` writes); the tables stay on the page as the accessible fallback. Update both together. +Like the figures of charts.py, the SVGs are not images: the page includes their text with +`--8<-- "images/.svg"`, so they follow the colours and fonts of the page (`nj-charts` rules in +docs/css/fig-charts.css, `nj-fig` rules in docs/css/brand.css). Run `python3 scripts/figures/perf_blog.py` from +docs/mkdocs after changing data or drawing, and commit the generated files. +""" +import csv +import html +import math +import re +from pathlib import Path + +HERE = Path(__file__).resolve().parent +OUT = HERE.parents[1] / "docs" / "images" +MINUS = "−" +TIMES = "×" + +# words the abbreviation extension (includes/glossary.md) would wrap in inside the SVG text +ABBR = re.compile(r"(?' % (cls, W, H, tid, tid), + '%s' % (tid, esc(title)), + '%s' % (tid, esc(desc))] + s += body + s.append('') + text = protect("\n".join(s)) + "\n" + assert "\n\n" not in text # a blank line would end the raw HTML block + (OUT / name).write_text(text) + + +def hbar(x0, x1, y, h, r=4): + """Horizontal bar from the baseline x0 to x1, rounded only at the data end.""" + if abs(x1 - x0) < 0.5: + return "M%g %g h0.5 v%g h-0.5 Z" % (x0, y, h) + r = min(r, h / 2, abs(x1 - x0)) + s = 1 if x1 > x0 else -1 + return ("M%g %g H%g Q%g %g %g %g V%g Q%g %g %g %g H%g Z" + % (x0, y, x1 - s * r, x1, y, x1, y + r, y + h - r, x1, y + h, x1 - s * r, y + h, x0)) + + +def fx(v): + """speedup factor for labels: 2 significant digits below 10, integers above""" + if v >= 100: + return "%d%s" % (round(v), TIMES) + if v >= 10: + return "%.0f%s" % (v, TIMES) + return "%.1f%s" % (v, TIMES) if v >= 1.995 else "%.2f%s" % (v, TIMES) + + +# ---------- SWAR: one word of a string ---------- +def swar(): + text = 'witter",' + W, H = 640, 312 + L, cw, top = 40, 70, 34 + b = [] + b.append('eight input bytes, loaded as one 64-bit word (byte 0 is the lowest)' % L) + for i, ch in enumerate(text): + x = L + i * cw + special = ch == '"' + b.append('' % (x, top, cw - 6)) + if special: + b.append('' % (x, top, cw - 6)) + b.append('%s' % (x + (cw - 6) / 2, top + 26, esc(ch))) + b.append('0x%02X' % (x + (cw - 6) / 2, top + 58, ord(ch))) + b.append('byte %d' % (x + (cw - 6) / 2, top + 74, i)) + # result row + y2 = top + 112 + b.append('swar_string_special(): the high bit of each byte that needs handling' % (L, y2 - 14)) + for i, ch in enumerate(text): + x = L + i * cw + special = ch == '"' + b.append('' % (x, y2, cw - 6)) + if special: + b.append('' % (x, y2, cw - 6)) + b.append('%s' + % (x + (cw - 6) / 2, y2 + 23, "0x80" if special else "0x00")) + # conclusion + y3 = y2 + 70 + b.append('count_trailing_zeros(0x0080000000000000) = 55' % (L, y3)) + b.append('55 / 8 = 6: copy bytes 0 to 5 in one go, then handle the quote' % (L, y3 + 26)) + b.append('No branch per byte: a few subtractions, ANDs and ORs per word,' % (L, y3 + 56)) + b.append('then one count of trailing zeros.' % (L, y3 + 74)) + write("perf_blog_swar.svg", W, H, "nj-charts--swar", "nj-blog-swar", + "Finding the end of a run of ordinary string characters eight bytes at a time", + "The eight bytes w, i, t, t, e, r, quote and comma are loaded as one word. The test sets the high bit only in " + "byte 6, the quote. Counting trailing zero bits gives 55, divided by eight gives 6: bytes 0 to 5 are copied " + "at once, then the quote ends the string.", b) + + +# ---------- overview: speedup per workload (Google Benchmark rows, 3.12.0 time / 3.13.0 time) ---------- +# (group, label, speedup), copied from the table "The short version" +OVERVIEW = [ + ("Parsing", "twitter.json", 3.02), + ("Parsing", "citm_catalog.json", 4.13), + ("Parsing", "canada.json", 3.46), + ("Parsing", "canada.json, indented", 6.74), + ("Parsing", "citm, ordered_json", 6.58), + ("Parsing", "twitter, std::ifstream", 2.46), + ("Parsing", "citm, FILE*", 4.76), + ("Parsing", "citm, accept()", 6.99), + ("Writing", "dump() twitter", 6.67), + ("Writing", "dump() canada", 2.95), + ("Writing", "dump(4) citm", 4.63), + ("Writing", "to_cbor() twitter", 4.50), + ("Reading CBOR", "twitter, memory", 3.11), + ("Reading CBOR", "twitter, FILE*", 6.31), + ("Values", "diff() canada", 10.44), + ("Values", "iterate canada", 2.69), + ("Values", "copy + destroy citm", 1.11), + ("Values", "== citm", 1.04), +] + + +def overview(): + W, L, R = 560, 190, 520 + hi = max(v for _, _, v in OVERVIEW) + hi = math.ceil(hi + 0.5) + X = lambda v: L + v / hi * (R - L) + pitch, bh, top = 24, 15, 34 + b, desc = [], [] + y = top + group = None + rows = [] + for g, label, v in OVERVIEW: + if g != group: + if group is not None: + y += 10 + b.append('%s' % (8, y + 12, esc(g))) + y += 22 + group = g + rows.append((label, v, y)) + y += pitch + axis_y = y + 2 + for t in range(1, hi + 1): + b.append('' % (X(t), top, X(t), axis_y)) + b.append('%d%s' % (X(t), axis_y + 16, t, TIMES)) + for label, v, yy in rows: + b.append('%s' % (L - 8, yy + bh - 3, esc(label))) + b.append('%s: %s' % (hbar(X(0), X(v), yy, bh), esc(label), fx(v))) + b.append('%s' % (X(v) + 5, yy + bh - 3, fx(v))) + desc.append("%s %s" % (label, fx(v).replace(TIMES, "x"))) + b.append('' % (X(1), top, X(1), axis_y)) + b.append('' % (X(0), top, X(0), axis_y)) + b.append('speedup over 3.12.0 (the line at 1%s is 3.12.0)' % (L, 18, TIMES)) + write("perf_blog_overview.svg", W, axis_y + 28, "nj-charts--blog", "nj-blog-overview", + "Speedup of 3.13.0 over 3.12.0 per workload, Apple M1 Max, Apple clang", + "Horizontal bars, longer is faster. " + "; ".join(desc) + ".", b) + + +# ---------- timeline: one panel per workload ---------- +# (column in perf_timeline.csv, panel title, [PR numbers to label]) +TIMELINE = [ + ("parse_citm", "parse() citm_catalog.json", [5234, 5283, 5592, 5809, 5810, 5882]), + ("parse_canada", "parse() canada.json", [5234, 5283, 5617, 5738, 5851, 5882]), + ("accept_twitter", "accept() twitter.json", [5234, 5283, 5592, 5809, 5810, 5853]), + ("parse_twitter_ifstream", "parse() twitter.json, std::ifstream", [5828, 5884]), + ("dump_twitter", "dump() twitter.json", [5285, 5738, 5878, 5883]), + ("dump_canada", "dump() canada.json", [5285, 5634]), + ("from_cbor_twitter", "from_cbor() twitter", [5233, 5501, 5185, 5746]), + ("to_cbor_twitter", "to_cbor() twitter", [5328, 5824, 5885]), +] +MONTHS = [("2025-04", "3.12.0"), ("2026-09", "Sep 2026"), ("2026-10", "Oct")] + + +def timeline(): + rows = list(csv.DictReader((HERE / "data" / "perf_timeline.csv").open())) + n = len(rows) + W, H = 340, 252 + L, R, T, B = 44, 330, 62, 208 + for col, title, labels in TIMELINE: + vals = [float(r[col]) if r[col] else None for r in rows] + base = vals[0] + rel = [base / v if v else None for v in vals] # speedup over 3.12.0 + hi = max(x for x in rel if x) + top = 2 ** math.ceil(math.log2(hi * 1.3)) + Y = lambda s: B - math.log2(s) / math.log2(top) * (B - T) + X = lambda i: L + i / (n - 1) * (R - L) + b = ['%s' % (8, esc(title))] + t = 1 + while t <= top: + b.append('' % (L, Y(t), R, Y(t))) + b.append('%d%s' % (L - 6, Y(t) + 4, t, TIMES)) + t *= 2 + # month ticks: first commit of the month + seen = set() + for i, r in enumerate(rows): + m = r["date"][:7] + for key, name in MONTHS: + if m == key and key not in seen: + seen.add(key) + b.append('' % (X(i), B, X(i), B + 4)) + b.append('%s' % (X(i), B + 17, name)) + # a step line: each commit's value holds until the next commit + pts, prev = [], None + for i, s in enumerate(rel): + if s is None: + continue + if prev is not None: + pts.append("%.1f,%.1f" % (X(i), Y(prev))) + pts.append("%.1f,%.1f" % (X(i), Y(s))) + prev = s + b.append('' % " ".join(pts)) + # labels for named steps: a dot at the commit and its PR number above it; PRs a few commits apart share a + # label, and a label that would overlap one already placed moves up + groups = [] + for pr in labels: + i = next(k for k, r in enumerate(rows) if r["pr"] == str(pr)) + if groups and X(i) - X(groups[-1][-1][1]) < 6: + groups[-1].append((pr, i)) + else: + groups.append([(pr, i)]) + # labels in two rows above the plot, each with a guide down to its dot + placed = [] + for g in groups: + i = g[-1][1] + x, y = X(i), Y(rel[i]) + for pr, k in g: + b.append('#%s: %s' + % (X(k), Y(rel[k]), pr, fx(rel[k]))) + text = ", ".join("#%s" % pr for pr, _ in g) + w = 7.2 * len(text) + tx = min(max(x, L + w / 2), R - w / 2) + row = 0 + while any(r == row and abs(tx - px) < (w + pw) / 2 + 4 for px, r, pw in placed): + row += 1 + placed.append((tx, row, w)) + ty = 50 - 13 * row + b.append('' % (x, ty + 4, x, y - 5)) + b.append('%s' % (tx, ty, text)) + b.append('' % (L, B, R, B)) + final = rel[-1] + desc = ("%s: speedup over 3.12.0 at each of %d commits, ending at %s. Labeled steps: %s." + % (title, n, fx(final).replace(TIMES, "x"), ", ".join("#%s" % p for p in labels))) + write("perf_blog_timeline_%s.svg" % col, W, H, "nj-charts--timeline", "nj-blog-tl-" + col, + "%s: speedup over 3.12.0 by commit" % title, desc, b) + + +# ---------- cliffs: 3.12.0 and 3.13.0 times on a log axis ---------- +# (operation, 3.12.0 ms, 3.13.0 ms), copied from the table in "Fix the algorithm first" +CLIFFS = [ + ("diff() 64k numbers vs []", 4102, 9.98), + ("diff() 20k objects vs half", 119, 3.85), + ("parse() callback, 32k objects", 1334, 11.5), + ("< nested 24 deep", 62.6, 0.002), + ("< citm, 10x", 274, 3.81), + ("flatten() citm, ordered_json", 253, 1.37), + ("to_bson() 200 deep, 100x", 19.5, 0.566), + ("push_back() 100k, diagnostics", 1514, 4.98), + ("at(i) 10k, diagnostics", 43.2, 0.081), +] + + +def cliffs(): + W, L, R = 600, 250, 585 + lo, hi = -3, 4 # 1 µs .. 10 s, in log10 ms + X = lambda ms: L + (math.log10(ms) - lo) / (hi - lo) * (R - L) + top, pitch = 40, 34 + b, desc = [], [] + axis_y = top + pitch * len(CLIFFS) - 10 + for e in range(lo, hi + 1): + b.append('' % (X(10 ** e), top - 10, X(10 ** e), axis_y)) + lab = {-3: "1 µs", -2: "10 µs", -1: "0.1 ms", 0: "1 ms", 1: "10 ms", 2: "0.1 s", 3: "1 s", 4: "10 s"}[e] + b.append('%s' % (X(10 ** e), axis_y + 16, lab)) + b.append('3.12.0' % (L, L + 10)) + b.append('3.13.0' % (L + 80, L + 90)) + for i, (op, old, new) in enumerate(CLIFFS): + y = top + i * pitch + 8 + b.append('%s' % (L - 10, y + 4, esc(op))) + b.append('' % (X(new), y, X(old), y)) + b.append('%s, 3.12.0: %g ms' % (X(old), y, esc(op), old)) + b.append('%s, 3.13.0: %g ms' % (X(new), y, esc(op), new)) + b.append('%s' % ((X(old) + X(new)) / 2, y - 7, fx(old / new))) + desc.append("%s: %g ms to %g ms" % (op, old, new)) + write("perf_blog_cliffs.svg", W, axis_y + 28, "nj-charts--blog", "nj-blog-cliffs", + "Operations that grew quadratically or worse: time with 3.12.0 and 3.13.0, log scale", + "Dot plot on a logarithmic time axis. " + "; ".join(desc) + ".", b) + + +# ---------- json_view: parse throughput ---------- +# engines and GB/s per file, copied from the table in "Build a different data structure" +VIEW_FILES = ["twitter.json", "citm_catalog.json", "canada.json", "jeopardy.json"] +VIEW_BYTES = [631514, 1727204, 2251051, 55554625] +VIEW_US = [ # (engine, css class, [µs per file]) + ("json_document", "nj-charts-c1", [181.7, 359.9, 959.5, 22687]), + ("yyjson", "nj-charts-c2", [192.0, 478.1, 1489, 30528]), + ("simdjson DOM", "nj-charts-c3", [216.4, 490.2, 2098, 26875]), + ("Boost.JSON", "nj-charts-c4", [866.0, 1480, 2501, 88900]), + ("json::parse", "nj-charts-c5", [1368, 2409, 5890, 167664]), +] +VIEW = [(e, c, [n / (t * 1000) for n, t in zip(VIEW_BYTES, us)]) for e, c, us in VIEW_US] # GB/s + + +def view(): + W, L, R = 580, 142, 545 + hi = max(max(v) for _, _, v in VIEW) + hi = math.ceil(hi * 2) / 2 + X = lambda v: L + v / hi * (R - L) + bh, gap = 11, 2 + top = 44 + group_h = len(VIEW) * (bh + gap) + 14 + b, desc = [], [] + x = 8 + for j, (eng, cls, _) in enumerate(VIEW): + b.append('' % (cls, x)) + b.append('%s' % (x + 18, esc(eng))) + x += 18 + 7.4 * len(eng) + 16 + axis_y = top + group_h * len(VIEW_FILES) + t = 0.5 + while t <= hi + 1e-9: + b.append('' % (X(t), top - 4, X(t), axis_y)) + b.append('%g' % (X(t), axis_y + 16, t)) + t += 0.5 + b.append('GB/s, longer is faster' % ((L + R) / 2, axis_y + 34)) + for i, f in enumerate(VIEW_FILES): + y0 = top + i * group_h + b.append('%s' % (L - 8, y0 + group_h / 2, esc(f))) + for j, (eng, cls, vals) in enumerate(VIEW): + y = y0 + j * (bh + gap) + b.append('%s, %s: %.2f GB/s' % (cls, hbar(X(0), X(vals[i]), y, bh), esc(eng), esc(f), vals[i])) + b.append('%.2f' % (X(vals[i]) + 4, y + bh - 1.5, vals[i])) + desc.append("%s %s %.2f GB/s" % (f, eng, vals[i])) + b.append('' % (X(0), top - 4, X(0), axis_y)) + write("perf_blog_view.svg", W, axis_y + 44, "nj-charts--blog", "nj-blog-view", + "Parse throughput of json_document, yyjson, simdjson's DOM, Boost.JSON and json::parse, Apple M1 Max", + "Grouped horizontal bars in GB/s. " + "; ".join(desc) + ".", b) + + +if __name__ == "__main__": + swar() + if OVERVIEW: + overview() + if TIMELINE: + timeline() + if CLIFFS: + cliffs() + if VIEW: + view() diff --git a/tests/benchmarks/README.md b/tests/benchmarks/README.md index 132bbb74a..ff85e20d3 100644 --- a/tests/benchmarks/README.md +++ b/tests/benchmarks/README.md @@ -138,6 +138,9 @@ the `View*` benchmarks (in `src/benchmarks_view.cpp`) are only built if the dire Only benchmarks present in both result files are compared, so for older releases, either filter the benchmarks or build that release's own `tests/benchmarks` against its own header. +To find out *which commit* changed a number, [`history/`](history/README.md) times one fixed program against the +header of every commit that changed it. + ## Getting stable numbers - Build and run both versions on the same machine with the same compiler and Google Benchmark, one right after the diff --git a/tests/benchmarks/history/README.md b/tests/benchmarks/history/README.md new file mode 100644 index 000000000..748576033 --- /dev/null +++ b/tests/benchmarks/history/README.md @@ -0,0 +1,43 @@ +# Performance history + +The benchmarks in [`tests/benchmarks`](../README.md) compare two versions. The program here answers a different +question: *which commit* made something faster or slower. `history.cpp` is one fixed benchmark that uses only API +3.12.0 already has. `history.py` compiles it against the `single_include` of every first-parent commit that changed +`json.hpp` or `json_view.hpp`, runs all of them interleaved, and writes one CSV row per commit with its pull request. +The timeline in the post [How 3.13.0 got faster](https://json.nlohmann.me/home/blog/how_3_13_0_got_faster/) comes from +it. It is not built by CMake or run by CI. + +## Workloads + +| name | what it does | +|---|---| +| `parse_twitter`, `parse_citm`, `parse_canada` | `json::parse()` of a `std::string` (`citm_catalog.json` as distributed is indented by four spaces) | +| `parse_citm_compact` | the same with a minified copy of `citm_catalog.json` | +| `parse_twitter_ifstream` | `json::parse()` of an `std::ifstream`, opening the file included | +| `accept_twitter` | `json::accept()` | +| `view_parse_*` | `json_document::parse()`; only where the header has `json_view.hpp` | +| `dump_twitter`, `dump_citm`, `dump_canada`, `dump4_citm` | `dump()` and `dump(4)` | +| `from_cbor_twitter`, `to_cbor_twitter` | read and write CBOR | +| `copy_destroy_citm` | copy a parsed document and destroy the copy | +| `equal_citm` | copy a parsed document and compare it with `==` | + +Each workload runs in batches of at least 20 ms; the program prints the median of 9 batches in microseconds per call. + +## Running it + +```sh +mkdir -p ~/history/data +cp /nativejson-benchmark/{twitter,citm_catalog,canada}.json ~/history/data/ +python3 history.py build --repo ../../.. --work ~/history # about 300 builds, a few minutes in parallel +caffeinate -i python3 history.py run --work ~/history --rounds 4 # about 1 h 45 min on an M1 Max (about 4 s per binary and round) +python3 history.py summarize --work ~/history > history.csv +``` + +`--extra DIR` adds one more header directory, such as an amalgamation of `develop` with open pull requests merged +in. `run` waits whenever the 1-minute load average is 4 or more. + +## Reading the results + +A single commit can move a workload by a few percent through code layout alone: the functions it did not touch +move to other addresses. Read a change as a step only if it is larger than that and stays. To check one, time the +two commits against each other with more rounds, or compare instruction counts. diff --git a/tests/benchmarks/history/history.cpp b/tests/benchmarks/history/history.cpp new file mode 100644 index 000000000..cd4794a93 --- /dev/null +++ b/tests/benchmarks/history/history.cpp @@ -0,0 +1,88 @@ +// __ _____ _____ _____ +// __| | __| | | | JSON for Modern C++ (supporting code) +// | | |__ | | | | | | version 3.12.0 +// |_____|_____|_____|_|___| https://github.com/nlohmann/json +// +// SPDX-FileCopyrightText: 2013-2026 Niels Lohmann +// SPDX-License-Identifier: MIT + +// One fixed benchmark, compiled against the json.hpp of every commit that changed it (see README.md). +// It uses only API that 3.12.0 has; the json_document rows exist only where the header has json_view.hpp. +// Usage: history +// Prints one line per workload: the name and the median of 9 batches in microseconds per call. +#include +#if __has_include() && !defined(HISTORY_NO_VIEW) +#include +#define HISTORY_VIEW 1 +#endif +#include +#include +#include +#include +#include +#include +#include +#include + +using nlohmann::json; +static std::string slurp(const std::string& p) +{ + std::ifstream f(p, std::ios::binary); + std::stringstream s; + s << f.rdbuf(); + return s.str(); +} +static volatile std::size_t sink; + +static void bench(const char* name, const std::function& f) +{ + using clk = std::chrono::steady_clock; + sink = f(); // warm up + std::size_t n = 1; + for (;;) + { + auto t0 = clk::now(); + for (std::size_t i = 0; i < n; ++i) sink = f(); + double dt = std::chrono::duration(clk::now() - t0).count(); + if (dt > 0.02) break; + n *= 2; + } + std::vector v; + for (int b = 0; b < 9; ++b) + { + auto t0 = clk::now(); + for (std::size_t i = 0; i < n; ++i) sink = f(); + v.push_back(std::chrono::duration(clk::now() - t0).count() / n); + } + std::sort(v.begin(), v.end()); + std::printf("%s %.3f\n", name, v[4]); +} + +int main(int, char** argv) +{ + std::string d = argv[1]; + const std::string tw = slurp(d + "/twitter.json"), ci = slurp(d + "/citm_catalog.json"), + ca = slurp(d + "/canada.json"), cii = slurp(d + "/citm_compact.json"); + const json jtw = json::parse(tw), jci = json::parse(ci), jca = json::parse(ca); + const std::vector cbtw = json::to_cbor(jtw); + + bench("parse_twitter", [&] { return json::parse(tw).size(); }); + bench("parse_citm", [&] { return json::parse(ci).size(); }); + bench("parse_canada", [&] { return json::parse(ca).size(); }); + bench("parse_citm_compact", [&] { return json::parse(cii).size(); }); + bench("parse_twitter_ifstream", [&] { std::ifstream f(d + "/twitter.json"); return json::parse(f).size(); }); + bench("accept_twitter", [&] { return static_cast(json::accept(tw)); }); +#ifdef HISTORY_VIEW + bench("view_parse_twitter", [&] { const auto doc = nlohmann::json_document::parse(tw); return doc.root().size(); }); + bench("view_parse_citm", [&] { const auto doc = nlohmann::json_document::parse(ci); return doc.root().size(); }); + bench("view_parse_canada", [&] { const auto doc = nlohmann::json_document::parse(ca); return doc.root().size(); }); +#endif + bench("dump_twitter", [&] { return jtw.dump().size(); }); + bench("dump_citm", [&] { return jci.dump().size(); }); + bench("dump_canada", [&] { return jca.dump().size(); }); + bench("dump4_citm", [&] { return jci.dump(4).size(); }); + bench("from_cbor_twitter", [&] { return json::from_cbor(cbtw).size(); }); + bench("to_cbor_twitter", [&] { return json::to_cbor(jtw).size(); }); + bench("copy_destroy_citm", [&] { json c = jci; return c.size(); }); + bench("equal_citm", [&] { json c = jci; return static_cast(c == jci); }); +} diff --git a/tests/benchmarks/history/history.py b/tests/benchmarks/history/history.py new file mode 100755 index 000000000..3bbfe8c32 --- /dev/null +++ b/tests/benchmarks/history/history.py @@ -0,0 +1,151 @@ +#!/usr/bin/env python3 +# __ _____ _____ _____ +# __| | __| | | | JSON for Modern C++ (supporting code) +# | | |__ | | | | | | version 3.12.0 +# |_____|_____|_____|_|___| https://github.com/nlohmann/json +# +# SPDX-FileCopyrightText: 2013-2026 Niels Lohmann +# SPDX-License-Identifier: MIT + +"""Time one fixed benchmark against the json.hpp of every commit that changed it. + +Usage: + history.py build --repo REPO --work DIR [--from v3.12.0] [--to origin/develop] [--extra INCLUDE_DIR] + history.py run --work DIR [--rounds 4] + history.py summarize --work DIR [--repo REPO] > history.csv + +build: lists the first-parent commits between --from and --to that changed single_include/nlohmann/json.hpp or +json_view.hpp, extracts single_include/ of each (and of --from itself), and compiles history.cpp against it into +DIR/bin/NNN, NNN being the line of the commit in DIR/commits.txt. --extra adds one more header directory at the end, +for example an amalgamation of develop with open pull requests merged in. It also writes the input files to DIR/data +(the nativejson-benchmark files from the test data, which must be in DIR/data already, plus a minified copy of +citm_catalog.json). + +run: runs every binary once per round, in a new random order each round, and only while the 1-minute load average is +below 4. Output: DIR/res/NNN-R.txt. + +summarize: the median over the rounds per binary and workload, as CSV with the commit, its date and its pull request. + +Only the Python 3 standard library is used; a C++17 compiler (c++) and git are needed. +""" +import argparse +import concurrent.futures +import csv +import json +import os +import random +import re +import statistics +import subprocess +import sys +import time +from pathlib import Path + +HERE = Path(__file__).resolve().parent +FLAGS = ["-std=c++17", "-O3", "-DNDEBUG", "-w"] + + +def git(repo, *args): + return subprocess.run(["git", "-C", str(repo), *args], check=True, capture_output=True, text=True).stdout + + +def build_one(work, idx, rev, repo): + hdr = work / "hdr" / idx + hdr.mkdir(parents=True, exist_ok=True) + if rev.startswith("dir:"): + subprocess.run(["cp", "-R", rev[4:] + "/nlohmann", str(hdr)], check=True) + else: + archive = subprocess.run(["git", "-C", str(repo), "archive", rev, "single_include/nlohmann"], + check=True, capture_output=True).stdout + subprocess.run(["tar", "-x", "-C", str(hdr), "--strip-components=1"], input=archive, check=True) + out = work / "bin" / idx + cmd = ["c++", *FLAGS, "-I", str(hdr), str(HERE / "history.cpp"), "-o", str(out)] + # json_view.hpp of the first commits that had it may not compile with history.cpp: time json.hpp alone then + if subprocess.run(cmd, capture_output=True).returncode != 0: + if subprocess.run(cmd[:-4] + ["-DHISTORY_NO_VIEW"] + cmd[-4:], capture_output=True).returncode != 0: + return "FAIL %s %s" % (idx, rev) + return None + + +def build(a): + work, repo = Path(a.work), Path(a.repo) + (work / "bin").mkdir(parents=True, exist_ok=True) + revs = [a.__dict__["from"]] + git(repo, "log", "--first-parent", "--reverse", "--format=%H", + "%s..%s" % (a.__dict__["from"], a.to), "--", + "single_include/nlohmann/json.hpp", "single_include/nlohmann/json_view.hpp").split() + if a.extra: + revs.append("dir:" + str(Path(a.extra).resolve())) + (work / "commits.txt").write_text("\n".join(revs) + "\n") + data = work / "data" + citm = json.loads((data / "citm_catalog.json").read_text(encoding="utf-8")) + (data / "citm_compact.json").write_text(json.dumps(citm, separators=(",", ":"), ensure_ascii=False), encoding="utf-8") + with concurrent.futures.ThreadPoolExecutor(os.cpu_count()) as ex: + for r in ex.map(lambda p: build_one(work, "%03d" % p[0], p[1], repo), enumerate(revs)): + if r: + print(r, file=sys.stderr) + + +def load(): + out = subprocess.run(["sysctl", "-n", "vm.loadavg"], capture_output=True, text=True).stdout + return float(out.split()[1]) if out else os.getloadavg()[0] + + +def run(a): + work = Path(a.work) + (work / "res").mkdir(exist_ok=True) + bins = sorted(p.name for p in (work / "bin").iterdir()) + for r in range(1, a.rounds + 1): + random.shuffle(bins) + for b in bins: + while load() >= 4: + time.sleep(15) + out = subprocess.run([str(work / "bin" / b), str(work / "data")], check=True, capture_output=True, text=True) + (work / "res" / ("%s-%d.txt" % (b, r))).write_text(out.stdout) + print("round %d done" % r, file=sys.stderr) + + +def summarize(a): + work = Path(a.work) + revs = (work / "commits.txt").read_text().split() + times, names = {}, [] + for f in sorted((work / "res").glob("*.txt")): + idx = int(f.name.split("-")[0]) + for line in f.read_text().splitlines(): + name, us = line.split() + if name not in names: + names.append(name) + times.setdefault((idx, name), []).append(float(us)) + w = csv.writer(sys.stdout, lineterminator="\n") + w.writerow(["index", "commit", "date", "pr", "subject"] + names) + for idx, rev in enumerate(revs): + if rev.startswith("dir:"): + sha, date, pr, subject = "candidate", "", "", "develop + open pull requests" + else: + sha, date, subject = git(a.repo, "log", "-1", "--format=%h%x09%cs%x09%s", rev).strip().split("\t") + m = re.search(r"\(#(\d+)\)$", subject) + pr = m.group(1) if m else "" + w.writerow([idx, sha, date, pr, subject] + + ["%.3f" % statistics.median(times[(idx, n)]) if (idx, n) in times else "" for n in names]) + + +def main(): + p = argparse.ArgumentParser(description=__doc__.splitlines()[0]) + sub = p.add_subparsers(dest="cmd", required=True) + b = sub.add_parser("build") + b.add_argument("--repo", required=True) + b.add_argument("--work", required=True) + b.add_argument("--from", default="v3.12.0") + b.add_argument("--to", default="origin/develop") + b.add_argument("--extra") + r = sub.add_parser("run") + r.add_argument("--work", required=True) + r.add_argument("--rounds", type=int, default=4) + s = sub.add_parser("summarize") + s.add_argument("--work", required=True) + s.add_argument("--repo", default=str(HERE.parents[2])) + a = p.parse_args() + {"build": build, "run": run, "summarize": summarize}[a.cmd](a) + + +if __name__ == "__main__": + main()