diff --git a/docs/mkdocs/docs/css/fig-charts.css b/docs/mkdocs/docs/css/fig-charts.css index ffd679986..9a18bae68 100644 --- a/docs/mkdocs/docs/css/fig-charts.css +++ b/docs/mkdocs/docs/css/fig-charts.css @@ -51,6 +51,7 @@ .nj-charts .nj-charts-grid { stroke: var(--nj-charts-grid); stroke-width: 1; } .nj-charts .nj-charts-zero, .nj-charts .nj-charts-axis { stroke: var(--nj-charts-base); stroke-width: 1.5; } .nj-charts .nj-charts-ref { stroke: currentColor; stroke-width: 1.5; } +.nj-charts .nj-charts-head { fill: var(--nj-charts-base); } .nj-charts .nj-charts-c1 { fill: var(--nj-charts-c1); } .nj-charts .nj-charts-c2 { fill: var(--nj-charts-c2); } diff --git a/docs/mkdocs/docs/home/blog/how_3_13_0_got_faster.md b/docs/mkdocs/docs/home/blog/how_3_13_0_got_faster.md index f44f3daaf..7ac49dce5 100644 --- a/docs/mkdocs/docs/home/blog/how_3_13_0_got_faster.md +++ b/docs/mkdocs/docs/home/blog/how_3_13_0_got_faster.md @@ -26,6 +26,19 @@ machine, and say so. ## The short version +Most rows below use three files from Milo Yip's [nativejson-benchmark](https://github.com/miloyip/nativejson-benchmark) +that JSON libraries have been measured with for a decade, plus one large file. They stress different parts of a +parser: + +| file | size | what it is | what makes it hard | +| --- | --: | --- | --- | +| `twitter.json` | 632 KB | 100 tweets from Twitter's search API, with their users | strings: 58 % of the bytes are keys and strings, 15 % of the bytes are non-ASCII (almost all of it Japanese text), and 13,345 keys | +| `citm_catalog.json` | 1.7 MB | a concert hall's catalog of events, performances and seat categories, in French | structure: 21,388 small objects and arrays, 14,392 integers, keys that average 8 bytes, and 71 % whitespace from indentation | +| `canada.json` | 2.3 MB | the outline of Canada in GeoJSON | numbers: 111,080 floating-point coordinates, most with 16 or 17 significant digits, in 56,045 arrays, and almost no whitespace | +| `jeopardy.json` | 56 MB | 216,930 Jeopardy! questions as one array of objects | size: 1.5 million short strings in objects of 7 members; far larger than the CPU caches | + +

The test files. The counts are of the parsed values; the share of whitespace, string and non-ASCII bytes is of the file as distributed.

+ 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): @@ -50,10 +63,14 @@ headers. The parse rows do not include destroying the result (the suite pauses t | 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× | +

Time per call with 3.12.0 and 3.13.0, Google Benchmark suite, median of 8 runs; the speedup is the old time divided by the new one.

+
--8<-- "images/perf_blog_overview.svg"
+

The speedups of the table above as bars. More is better; the vertical line at 1× is 3.12.0.

+ 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 @@ -81,8 +98,18 @@ Eight themes did most of the work: 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 +(details in [How we measured](#how-we-measured)). Each panel is one workload, chosen so that each exercises a +different part of the library: + +- `parse()` of `citm_catalog.json`: the lexer's structure and whitespace handling, and building many small objects; +- `parse()` of `canada.json`: converting text to floating-point numbers; +- `accept()` of `twitter.json`: the lexer alone, since `accept()` only validates and builds nothing; +- `parse()` of `twitter.json` from an `std::ifstream`: reading from a stream instead of a string; +- `dump()` of `twitter.json` and of `canada.json`: writing strings and writing floating-point numbers; +- `from_cbor()` and `to_cbor()` of `twitter.json`: reading and writing a binary format. + +Each panel shows the speedup over 3.12.0 after each commit on a logarithmic scale, so that a step of 2× has the same +height everywhere; 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 %. @@ -114,6 +141,8 @@ with six code layouts: they were equal within 0.2 %. +

Speedup over 3.12.0 of eight workloads after each of the 315 commits that changed the headers, from 3.12.0 (left) to the 3.13.0 candidate (right). More is better; each point is the median of 4 interleaved runs of one build.

+ 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 @@ -250,6 +279,8 @@ also flag a byte *after* a real hit (a borrow), but never one before it, so the --8<-- "images/perf_blog_swar.svg" +

`swar_string_special()` on the last eight bytes of `"twitter",`: one word in, one flag out, and the count of trailing zero bits says how many bytes can be copied before the quote.

+ 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: @@ -261,6 +292,8 @@ now runs in many places: | `dump()` | runs that need no escaping, with and without `ensure_ascii` | [#5285][5285] | | `json_view` `dump()` | ASCII runs with `ensure_ascii` | [#5879][5879] | +

Where the eight-byte test is used in 3.13.0, and the pull requests that added it.

+ 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 @@ -495,10 +528,14 @@ fixed in this release cycle, measured on the same machine: | 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] | +

Operations that were quadratic or worse in 3.12.0, timed with both versions (C++17, `-O3`, median of 3 interleaved runs).

+
--8<-- "images/perf_blog_cliffs.svg"
+

The times of the table above on a logarithmic scale; each arrow leads from the time with 3.12.0 to the time with 3.13.0. Less is better.

+ 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 @@ -522,13 +559,24 @@ one million small objects 1.39 times (125 ms to 90 ms). 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, -``. +``. The two figures below show what each parser makes of this 54-byte text: + +```json +{"port": 6379, "price": 19.90, "tags": ["eu", "a\"b"]} +```
---8<-- "images/json_view_layout.svg" +--8<-- "images/json_view_layout_tree.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.

+

What `json::parse()` builds: a tree in which every object, array and string is a separate heap allocation (dashed). The float is converted to a `double` while parsing, so the original text `19.90` is gone.

+ +
+--8<-- "images/json_view_layout_index.svg" +
+ +

What `json_document::parse()` builds from the same text: one flat index of nine 16-byte nodes in document order, which point back into the source text, plus a buffer for the one string with an escape. A `json_view` is two pointers, here to the array.

+ 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 @@ -575,7 +623,7 @@ interleaved rounds): | engine | `twitter.json` (632 KB) | `citm_catalog.json` (1.7 MB) | `canada.json` (2.3 MB) | `jeopardy.json` (56 MB) | | --- | --: | --: | --: | --: | -| `json_document` | **183** | **364** | **943** | **22,681** | +| nlohmann/json `json_document` | **183** | **364** | **943** | **22,681** | | yyjson | 194 | 478 | 1,489 | 30,342 | | simdjson DOM | 202 | 486 | 2,097 | 26,904 | | sonic-rs (Rust) | 302 | 664 | 1,907 | 30,202 | @@ -585,14 +633,18 @@ interleaved rounds): | RapidJSON | 1,183 | 1,613 | 7,358 | 143,044 | | serde_json (Rust) | 1,202 | 2,337 | 6,836 | 139,829 | | Glaze `generic_u64` | 1,268 | 2,727 | 9,262 | 132,273 | -| `json::parse` | 1,352 | 2,386 | 5,915 | 163,750 | +| nlohmann/json `json::parse` | 1,352 | 2,386 | 5,915 | 163,750 | + +

Parse times in µs, less is better: a fresh document per call, built and freed, on an Apple M1 Max. The fastest engine of each file is in bold.

--8<-- "images/perf_blog_view.svg"
-The fastest *other* engine for each workload, as its time divided by `json_document`'s, over the four files above: -above 1, `json_document` is ahead; below 1, it is behind. +

The parse times of the table above as throughput (file size divided by time), more is better. RapidJSON, Glaze, serde_json and simd-json are left out to keep the chart readable; they are in the table.

+ +The fastest *other* engine for each workload, as its time divided by nlohmann/json's `json_document`'s, over the +four files above: above 1, `json_document` is ahead; below 1, it is behind. | workload | M1 Max, Apple clang 21 | x86-64, GCC 13 | x86-64, Clang 18 | | --- | --- | --- | --- | @@ -601,6 +653,8 @@ above 1, `json_document` is ahead; below 1, it is behind. | select | **0.34–0.84** (Glaze structs, simdjson On-Demand) | **0.26–0.65** | **0.33–0.86** | | parse+dump | 1.18–1.35 (yyjson) | 1.20–1.41 (yyjson, sonic-cpp) | 1.16–1.27 (yyjson, sonic-cpp) | +

Time of the fastest other engine divided by the time of nlohmann/json's `json_document`, as a range over the four files, with the engines that were fastest; below 1, nlohmann/json is behind.

+ So, is it the fastest JSON parser? Not in general. Among the parsers that build a document you can read in any order, it is the fastest at parsing and writing back in every configuration we ran, and the fastest at parsing on Apple silicon. On x86-64 it ties with simdjson's DOM on typical documents. Where it loses: @@ -642,6 +696,8 @@ The PRs record the ideas that measured worse, which is as useful as the ones tha | `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] | +

Ideas that were measured and dropped, from the pull requests that tried them.

+ ## 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 @@ -670,6 +726,8 @@ header of 11,310 lines that you only pay for if you include it). Apple clang 21 | 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 %) | +

Compile time of one translation unit, Apple clang 21, `-O2`, median of 9 compilations; less is better.

+ 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. @@ -693,6 +751,8 @@ removed because 3.12.0 has no BON8. | 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 | +

Setup of the Google Benchmark comparison of 3.12.0 and 3.13.0.

+ 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 @@ -710,7 +770,9 @@ 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: +the order they appear above. Where the library contains code adapted from another project, we name the license we use +it under; the [README](https://github.com/nlohmann/json#license) has the copyright notices. From the others we took +the idea, not their code. - [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 @@ -718,17 +780,19 @@ the order they appear above: - [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. + 2021, adapted code; MIT License, also available under the Apache License 2.0), 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. + "Fast number parsing without fallback", 2023; adapted code; MIT License, also available under the Apache License + 2.0 and the Boost Software License 1.0). 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. + sequence of Dougall Johnson (ported to C++11; MIT License). - [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. + and a 16-byte flat document to measure against (adapted code in `json_view.hpp`; MIT License). - [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 @@ -775,6 +839,7 @@ object keys ([#5328][5328]). [5741]: https://github.com/nlohmann/json/pull/5741 [5746]: https://github.com/nlohmann/json/pull/5746 [5762]: https://github.com/nlohmann/json/pull/5762 +[5782]: https://github.com/nlohmann/json/pull/5782 [5809]: https://github.com/nlohmann/json/pull/5809 [5810]: https://github.com/nlohmann/json/pull/5810 [5822]: https://github.com/nlohmann/json/pull/5822 diff --git a/docs/mkdocs/docs/images/json_view_layout.svg b/docs/mkdocs/docs/images/json_view_layout.svg index 72277c1f1..275220080 100644 --- a/docs/mkdocs/docs/images/json_view_layout.svg +++ b/docs/mkdocs/docs/images/json_view_layout.svg @@ -17,16 +17,16 @@ std::map - + "port" 6379 - + "price" 19.9 double: 19.90 is gone - + "tags" • @@ -38,8 +38,8 @@ json json - - + + std::string eu diff --git a/docs/mkdocs/docs/images/json_view_layout_index.svg b/docs/mkdocs/docs/images/json_view_layout_index.svg new file mode 100644 index 000000000..9f5249b8c --- /dev/null +++ b/docs/mkdocs/docs/images/json_view_layout_index.svg @@ -0,0 +1,148 @@ + +The same J​SON text parsed by json_document::parse() +The same text parsed by json_document::parse(): it keeps the 54 bytes of source text and builds a flat index of nine 16-byte nodes in document order: object, key port, unsigned integer 6379 (the converted value is in the node), key price, float, key tags, array, string eu, and an escaped string. Keys, the float, and the plain string point back into the source text; the escaped string points into the document's buffer of decoded strings. A json_view is two pointers: one to the document and one to a node, here the array. + + + + + + + + + + + +json_document::parse(): the source text stays where it is; one flat index of 16-byte nodes +source text, 54 bytes (borrowed, or owned by the document) + + + + + + + + +{ +" +p +o +r +t +" +: +6 +3 +7 +9 +, +" +p +r +i +c +e +" +: +1 +9 +. +9 +0 +, +" +t +a +g +s +" +: +[ +" +e +u +" +, +" +a +\ +" +b +" +] +} +0 +10 +20 +30 +40 +50 + +json_document: owns the node index and the buffer of decoded strings +node index: 9 nodes × 16 bytes = 144 bytes, in document order + +0 +object +len 3 +next 9 + +1 +key +off 2 +len 4 + + +2 +uint +6379 +(converted) + +3 +key +off 16 +len 5 + + +4 +float +off 24 +len 5 + + +5 +key +off 32 +len 4 + + +6 +array +len 2 +next 3 + +7 +string +off 41 +len 2 + + +8 +string +escaped +len 3 +decoded strings + +a +" +b + +json_view v = doc.root()["tags"] + + +doc* +node* + + +integers are converted while parsing and kept in their node; +floats and strings point back into the text + + diff --git a/docs/mkdocs/docs/images/json_view_layout_tree.svg b/docs/mkdocs/docs/images/json_view_layout_tree.svg new file mode 100644 index 000000000..47aaa04f9 --- /dev/null +++ b/docs/mkdocs/docs/images/json_view_layout_tree.svg @@ -0,0 +1,49 @@ + +A J​SON text parsed by basic_json::parse() +The text {"port": 6379, "price": 19.90, "tags": ["eu", "a\"b"]} parsed by basic_json::parse(): the root json value points to a std::map whose three nodes are separate heap allocations; the price is stored as the double 19.9; the tags array is a std::vector of two json values, each pointing to its own std::string. + + + + + + + + + + +basic_json::parse(): a tree, every array, object, and string in its own heap allocation + +json + + +std::map + + +"port" +6379 + + +"price" +19.9 +double: 19.90 is gone + + +"tags" +• + + +std::vector + + + +json +json + + + +std::string eu + +std::string a"b + +a separate heap allocation + diff --git a/docs/mkdocs/docs/images/perf_blog_cliffs.svg b/docs/mkdocs/docs/images/perf_blog_cliffs.svg index 4fb0ac1a5..9aa5969cf 100644 --- a/docs/mkdocs/docs/images/perf_blog_cliffs.svg +++ b/docs/mkdocs/docs/images/perf_blog_cliffs.svg @@ -1,6 +1,6 @@ - + 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. +Dot plot on a logarithmic time axis, less is better; an arrow leads from each 3.12.0 time to the 3.13.0 time. 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 @@ -17,50 +17,52 @@ 1 s 10 s +runtime (log scale), less is better + 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 index aaa35f4a5..929300a31 100644 --- a/docs/mkdocs/docs/images/perf_blog_overview.svg +++ b/docs/mkdocs/docs/images/perf_blog_overview.svg @@ -1,6 +1,6 @@ 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. +Horizontal bars of the speedup, more is better. 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 @@ -83,5 +83,5 @@ 1.04× -speedup over 3.12.0 (the line at 1× is 3.12.0) +speedup over 3.12.0, more is better (the line at 1× is 3.12.0) diff --git a/docs/mkdocs/docs/images/perf_blog_timeline_accept_twitter.svg b/docs/mkdocs/docs/images/perf_blog_timeline_accept_twitter.svg index a274c4377..eb3b63952 100644 --- a/docs/mkdocs/docs/images/perf_blog_timeline_accept_twitter.svg +++ b/docs/mkdocs/docs/images/perf_blog_timeline_accept_twitter.svg @@ -1,4 +1,4 @@ - + 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 @@ -34,4 +34,5 @@ #5853 +speedup over 3.12.0 (log scale), more is better diff --git a/docs/mkdocs/docs/images/perf_blog_timeline_dump_canada.svg b/docs/mkdocs/docs/images/perf_blog_timeline_dump_canada.svg index 7ebc45833..05694e9f0 100644 --- a/docs/mkdocs/docs/images/perf_blog_timeline_dump_canada.svg +++ b/docs/mkdocs/docs/images/perf_blog_timeline_dump_canada.svg @@ -1,4 +1,4 @@ - + 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 @@ -24,4 +24,5 @@ #5634 +speedup over 3.12.0 (log scale), more is better diff --git a/docs/mkdocs/docs/images/perf_blog_timeline_dump_twitter.svg b/docs/mkdocs/docs/images/perf_blog_timeline_dump_twitter.svg index b82f2cc81..7c2b77cdd 100644 --- a/docs/mkdocs/docs/images/perf_blog_timeline_dump_twitter.svg +++ b/docs/mkdocs/docs/images/perf_blog_timeline_dump_twitter.svg @@ -1,4 +1,4 @@ - + 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 @@ -32,4 +32,5 @@ #5883 +speedup over 3.12.0 (log scale), more is better 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 index dd75ce0a4..d3e09ce78 100644 --- a/docs/mkdocs/docs/images/perf_blog_timeline_from_cbor_twitter.svg +++ b/docs/mkdocs/docs/images/perf_blog_timeline_from_cbor_twitter.svg @@ -1,4 +1,4 @@ - + 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 @@ -28,4 +28,5 @@ #5746 +speedup over 3.12.0 (log scale), more is better diff --git a/docs/mkdocs/docs/images/perf_blog_timeline_parse_canada.svg b/docs/mkdocs/docs/images/perf_blog_timeline_parse_canada.svg index 5460432d7..4d9c79f3c 100644 --- a/docs/mkdocs/docs/images/perf_blog_timeline_parse_canada.svg +++ b/docs/mkdocs/docs/images/perf_blog_timeline_parse_canada.svg @@ -1,4 +1,4 @@ - + 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 @@ -34,4 +34,5 @@ #5882 +speedup over 3.12.0 (log scale), more is better diff --git a/docs/mkdocs/docs/images/perf_blog_timeline_parse_citm.svg b/docs/mkdocs/docs/images/perf_blog_timeline_parse_citm.svg index f81b44fa8..b8473fb09 100644 --- a/docs/mkdocs/docs/images/perf_blog_timeline_parse_citm.svg +++ b/docs/mkdocs/docs/images/perf_blog_timeline_parse_citm.svg @@ -1,4 +1,4 @@ - + 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 @@ -34,4 +34,5 @@ #5882 +speedup over 3.12.0 (log scale), more is better 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 index e3efdf150..fee96a029 100644 --- a/docs/mkdocs/docs/images/perf_blog_timeline_parse_twitter_ifstream.svg +++ b/docs/mkdocs/docs/images/perf_blog_timeline_parse_twitter_ifstream.svg @@ -1,4 +1,4 @@ - + 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 @@ -22,4 +22,5 @@ #5884 +speedup over 3.12.0 (log scale), more is better 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 index b091ac5ba..48dbdcac7 100644 --- a/docs/mkdocs/docs/images/perf_blog_timeline_to_cbor_twitter.svg +++ b/docs/mkdocs/docs/images/perf_blog_timeline_to_cbor_twitter.svg @@ -1,4 +1,4 @@ - + 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 @@ -27,4 +27,5 @@ #5885 +speedup over 3.12.0 (log scale), more is better diff --git a/docs/mkdocs/docs/images/perf_blog_view.svg b/docs/mkdocs/docs/images/perf_blog_view.svg index 916c5d522..160ca0d94 100644 --- a/docs/mkdocs/docs/images/perf_blog_view.svg +++ b/docs/mkdocs/docs/images/perf_blog_view.svg @@ -1,100 +1,100 @@ - -Parse throughput of json_document and six other document parsers, fresh documents, Apple M1 Max -Grouped horizontal bars in GB/s. twitter.json json_document 3.45 GB/s; twitter.json yyjson 3.26 GB/s; twitter.json simdjson D​OM 3.13 GB/s; twitter.json sonic-rs (Rust) 2.09 GB/s; twitter.json sonic-cpp 1.67 GB/s; twitter.json Boost.J​SON 0.73 GB/s; twitter.json json::parse 0.47 GB/s; citm_catalog.json json_document 4.75 GB/s; citm_catalog.json yyjson 3.61 GB/s; citm_catalog.json simdjson D​OM 3.55 GB/s; citm_catalog.json sonic-rs (Rust) 2.60 GB/s; citm_catalog.json sonic-cpp 1.75 GB/s; citm_catalog.json Boost.J​SON 1.16 GB/s; citm_catalog.json json::parse 0.72 GB/s; canada.json json_document 2.39 GB/s; canada.json yyjson 1.51 GB/s; canada.json simdjson D​OM 1.07 GB/s; canada.json sonic-rs (Rust) 1.18 GB/s; canada.json sonic-cpp 0.87 GB/s; canada.json Boost.J​SON 0.91 GB/s; canada.json json::parse 0.38 GB/s; jeopardy.json json_document 2.45 GB/s; jeopardy.json yyjson 1.83 GB/s; jeopardy.json simdjson D​OM 2.06 GB/s; jeopardy.json sonic-rs (Rust) 1.84 GB/s; jeopardy.json sonic-cpp 1.36 GB/s; jeopardy.json Boost.J​SON 0.62 GB/s; jeopardy.json json::parse 0.34 GB/s. + +Parse throughput of nlohmann/json's json_document and json::parse and five other document parsers, fresh documents, Apple M1 Max +Grouped horizontal bars in GB/s, more is better. twitter.json nlohmann/json json_document 3.45 GB/s; twitter.json yyjson 3.26 GB/s; twitter.json simdjson D​OM 3.13 GB/s; twitter.json sonic-rs (Rust) 2.09 GB/s; twitter.json sonic-cpp 1.67 GB/s; twitter.json Boost.J​SON 0.73 GB/s; twitter.json nlohmann/json json::parse 0.47 GB/s; citm_catalog.json nlohmann/json json_document 4.75 GB/s; citm_catalog.json yyjson 3.61 GB/s; citm_catalog.json simdjson D​OM 3.55 GB/s; citm_catalog.json sonic-rs (Rust) 2.60 GB/s; citm_catalog.json sonic-cpp 1.75 GB/s; citm_catalog.json Boost.J​SON 1.16 GB/s; citm_catalog.json nlohmann/json json::parse 0.72 GB/s; canada.json nlohmann/json json_document 2.39 GB/s; canada.json yyjson 1.51 GB/s; canada.json simdjson D​OM 1.07 GB/s; canada.json sonic-rs (Rust) 1.18 GB/s; canada.json sonic-cpp 0.87 GB/s; canada.json Boost.J​SON 0.91 GB/s; canada.json nlohmann/json json::parse 0.38 GB/s; jeopardy.json nlohmann/json json_document 2.45 GB/s; jeopardy.json yyjson 1.83 GB/s; jeopardy.json simdjson D​OM 2.06 GB/s; jeopardy.json sonic-rs (Rust) 1.84 GB/s; jeopardy.json sonic-cpp 1.36 GB/s; jeopardy.json Boost.J​SON 0.62 GB/s; jeopardy.json nlohmann/json json::parse 0.34 GB/s. -json_document - -yyjson - -simdjson D​OM - -sonic-rs (Rust) - -sonic-cpp - -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.45 GB/s -3.45 -yyjson, twitter.json: 3.26 GB/s -3.26 -simdjson D​OM, twitter.json: 3.13 GB/s -3.13 -sonic-rs (Rust), twitter.json: 2.09 GB/s -2.09 -sonic-cpp, twitter.json: 1.67 GB/s -1.67 -Boost.J​SON, twitter.json: 0.73 GB/s -0.73 -json::parse, twitter.json: 0.47 GB/s -0.47 -citm_catalog.json -json_document, citm_catalog.json: 4.75 GB/s -4.75 -yyjson, citm_catalog.json: 3.61 GB/s -3.61 -simdjson D​OM, citm_catalog.json: 3.55 GB/s -3.55 -sonic-rs (Rust), citm_catalog.json: 2.60 GB/s -2.60 -sonic-cpp, citm_catalog.json: 1.75 GB/s -1.75 -Boost.J​SON, citm_catalog.json: 1.16 GB/s -1.16 -json::parse, citm_catalog.json: 0.72 GB/s -0.72 -canada.json -json_document, canada.json: 2.39 GB/s -2.39 -yyjson, canada.json: 1.51 GB/s -1.51 -simdjson D​OM, canada.json: 1.07 GB/s -1.07 -sonic-rs (Rust), canada.json: 1.18 GB/s -1.18 -sonic-cpp, canada.json: 0.87 GB/s -0.87 -Boost.J​SON, canada.json: 0.91 GB/s -0.91 -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.83 GB/s -1.83 -simdjson D​OM, jeopardy.json: 2.06 GB/s -2.06 -sonic-rs (Rust), jeopardy.json: 1.84 GB/s -1.84 -sonic-cpp, jeopardy.json: 1.36 GB/s -1.36 -Boost.J​SON, jeopardy.json: 0.62 GB/s -0.62 -json::parse, jeopardy.json: 0.34 GB/s -0.34 - +nlohmann/json json_document + +yyjson + +simdjson D​OM + +sonic-rs (Rust) + +sonic-cpp + +Boost.J​SON + +nlohmann/json json::parse + +0.5 + +1 + +1.5 + +2 + +2.5 + +3 + +3.5 + +4 + +4.5 + +5 +GB/s, more is better +twitter.json +nlohmann/json json_document, twitter.json: 3.45 GB/s +3.45 +yyjson, twitter.json: 3.26 GB/s +3.26 +simdjson D​OM, twitter.json: 3.13 GB/s +3.13 +sonic-rs (Rust), twitter.json: 2.09 GB/s +2.09 +sonic-cpp, twitter.json: 1.67 GB/s +1.67 +Boost.J​SON, twitter.json: 0.73 GB/s +0.73 +nlohmann/json json::parse, twitter.json: 0.47 GB/s +0.47 +citm_catalog.json +nlohmann/json json_document, citm_catalog.json: 4.75 GB/s +4.75 +yyjson, citm_catalog.json: 3.61 GB/s +3.61 +simdjson D​OM, citm_catalog.json: 3.55 GB/s +3.55 +sonic-rs (Rust), citm_catalog.json: 2.60 GB/s +2.60 +sonic-cpp, citm_catalog.json: 1.75 GB/s +1.75 +Boost.J​SON, citm_catalog.json: 1.16 GB/s +1.16 +nlohmann/json json::parse, citm_catalog.json: 0.72 GB/s +0.72 +canada.json +nlohmann/json json_document, canada.json: 2.39 GB/s +2.39 +yyjson, canada.json: 1.51 GB/s +1.51 +simdjson D​OM, canada.json: 1.07 GB/s +1.07 +sonic-rs (Rust), canada.json: 1.18 GB/s +1.18 +sonic-cpp, canada.json: 0.87 GB/s +0.87 +Boost.J​SON, canada.json: 0.91 GB/s +0.91 +nlohmann/json json::parse, canada.json: 0.38 GB/s +0.38 +jeopardy.json +nlohmann/json json_document, jeopardy.json: 2.45 GB/s +2.45 +yyjson, jeopardy.json: 1.83 GB/s +1.83 +simdjson D​OM, jeopardy.json: 2.06 GB/s +2.06 +sonic-rs (Rust), jeopardy.json: 1.84 GB/s +1.84 +sonic-cpp, jeopardy.json: 1.36 GB/s +1.36 +Boost.J​SON, jeopardy.json: 0.62 GB/s +0.62 +nlohmann/json json::parse, jeopardy.json: 0.34 GB/s +0.34 + diff --git a/docs/mkdocs/scripts/figures/json_view_layout.py b/docs/mkdocs/scripts/figures/json_view_layout.py index 9d362ab9a..be36e875f 100644 --- a/docs/mkdocs/scripts/figures/json_view_layout.py +++ b/docs/mkdocs/scripts/figures/json_view_layout.py @@ -104,10 +104,8 @@ def ribbon(body, x0, y, cw, prefix, label=True): # ---------------------------------------------------------------- figure 1: parse() vs json_document -def layout(): - W, H, P = 840, 692, "jvl" - b = markers(P) - # --- panel A: basic_json::parse() +def layout_tree(b, P): + """panel A: basic_json::parse(), y from 0 to 220""" b.append(text(16, 26, "basic_json::parse(): a tree, every array, object, and string in its own heap allocation", "nj-jsonview-h")) b.append(rect(16, 98, 60, 34, "nj-jsonview-box")) b.append(text(46, 120, "json", "nj-jsonview-mono", anchor="middle")) @@ -117,7 +115,7 @@ def layout(): rows = [('"port"', "6379", None), ('"price"', "19.9", "double: 19.90 is gone"), ('"tags"', "\u2022", None)] for i, (k, v, note) in enumerate(rows): y = 50 + i * 48 - b.append(curve(192, 115, 226, y + 17, LINE, P, bend=0)) + b.append(line(192, 115, 226, y + 17, LINE, P)) b.append(rect(228, y, 180, 34, "nj-jsonview-heap")) b.append(text(240, y + 22, k, "nj-jsonview-mono", KEY)) b.append(text(396, y + 22, v, "nj-jsonview-mono", NUM if i < 2 else LINE, anchor="end")) @@ -132,8 +130,9 @@ def layout(): b.append('' % (yt - 17, yt + 17)) b.append(text(601, yt + 5, "json", "nj-jsonview-mono", anchor="middle")) b.append(text(647, yt + 5, "json", "nj-jsonview-mono", anchor="middle")) - b.append(curve(601, yt - 17, 706, 130, LINE, P, bend=-20)) - b.append(curve(647, yt + 17, 706, 196, LINE, P, bend=20)) + for x1, y1, y2 in ((601, yt - 17, 130), (647, yt + 17, 196)): + b.append('' + % (LINE, x1, y1, x1, y2, x1 + 40, y2, 706, y2, P, LINE)) b.append(rect(708, 113, 126, 34, "nj-jsonview-heap")) b.append(text(771, 135, "std::string eu", "nj-jsonview-mono nj-jsonview-sm", STR, anchor="middle")) b.append(rect(708, 179, 126, 34, "nj-jsonview-heap")) @@ -141,9 +140,10 @@ def layout(): # legend b.append(rect(16, 186, 18, 14, "nj-jsonview-heap", rx=3)) b.append(text(42, 198, "a separate heap allocation", "nj-jsonview-note")) - # --- panel B: json_document::parse() - y0 = 252 - b.append('' % (y0 - 18)) + + +def layout_index(b, P, y0): + """panel B: json_document::parse(), y from y0 - 16 to y0 + 440""" b.append(text(16, y0 + 10, "json_document::parse(): the source text stays where it is; one flat index of 16-byte nodes", "nj-jsonview-h")) b.append(text(16, y0 + 38, "source text, 54 bytes (borrowed, or owned by the document)", "nj-jsonview-note")) cw, rx0, ry = 14, 42, y0 + 48 @@ -185,6 +185,25 @@ def layout(): b.append(curve(vx + 105, vy, n6x, ny + nh, STRUCT, P)) b.append(text(30, ny + nh + 50, "integers are converted while parsing and kept in their node;", "nj-jsonview-note")) b.append(text(30, ny + nh + 68, "floats and strings point back into the text", "nj-jsonview-note")) + + +TREE_DESC = ("The text {\"port\": 6379, \"price\": 19.90, \"tags\": [\"eu\", \"a\\\"b\"]} parsed by basic_json::parse(): " + "the root json value points to a std::map whose three nodes are separate heap allocations; the price is stored " + "as the double 19.9; the tags array is a std::vector of two json values, each pointing to its own std::string.") +INDEX_DESC = ("The same text parsed by json_document::parse(): it keeps the 54 bytes of source text and builds a flat index of " + "nine 16-byte nodes in document order: object, key port, unsigned integer 6379 (the converted value is in the " + "node), key price, float, key tags, array, string eu, and an escaped string. Keys, the float, and the plain " + "string point back into the source text; the escaped string points into the document's buffer of decoded " + "strings. A json_view is two pointers: one to the document and one to a node, here the array.") + + +def layout(): + # both panels in one figure (features/json_view.md) + W, H, P = 840, 692, "jvl" + b = markers(P) + layout_tree(b, P) + b.append('') + layout_index(b, P, 252) write("json_view_layout.svg", W, H, "fig-jvl-title", "The same J\u200bSON text parsed by basic_json::parse() and by json_document::parse()", "The text {\"port\": 6379, \"price\": 19.90, \"tags\": [\"eu\", \"a\\\"b\"]}. Top: basic_json::parse() builds a tree: " @@ -195,6 +214,16 @@ def layout(): "key tags, array, string eu, and an escaped string. Keys, the float, and the plain string point back into the " "source text; the escaped string points into the document's buffer of decoded strings. A json_view is two " "pointers: one to the document and one to a node, here the array.", b) + # each panel on its own (home/blog/how_3_13_0_got_faster.md), with their own marker ids + b = markers("jvlt") + layout_tree(b, "jvlt") + write("json_view_layout_tree.svg", W, 222, "fig-jvlt-title", + "A J\u200bSON text parsed by basic_json::parse()", TREE_DESC, b) + b = markers("jvli") + [''] + layout_index(b, "jvli", 252) + b.append('') + write("json_view_layout_index.svg", W, H - 236, "fig-jvli-title", + "The same J\u200bSON text parsed by json_document::parse()", INDEX_DESC, b) # ---------------------------------------------------------------- figure 2: lifetimes diff --git a/docs/mkdocs/scripts/figures/perf_blog.py b/docs/mkdocs/scripts/figures/perf_blog.py index 42e4787fb..a8ff34c71 100644 --- a/docs/mkdocs/scripts/figures/perf_blog.py +++ b/docs/mkdocs/scripts/figures/perf_blog.py @@ -164,10 +164,10 @@ def overview(): 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)) + b.append('speedup over 3.12.0, more is better (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) + "Horizontal bars of the speedup, more is better. " + "; ".join(desc) + ".", b) # ---------- timeline: one panel per workload ---------- @@ -188,7 +188,7 @@ 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 + W, H = 340, 270 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] @@ -251,6 +251,7 @@ def timeline(): b.append('' % (x, ty + 4, x, y - 5)) b.append('%s' % (tx, ty, text)) b.append('' % (L, B, R, B)) + b.append('speedup over 3.12.0 (log scale), more is better' % (8, H - 8)) 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))) @@ -284,19 +285,24 @@ def cliffs(): 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('runtime (log scale), less is better' % ((L + R) / 2, axis_y + 34)) + b.append('') 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)) + # an arrow from the old time to the new one, ending just before the new dot + b.append('' + % (X(old) - 6, y, X(new) + 7, 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", + write("perf_blog_cliffs.svg", W, axis_y + 44, "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) + "Dot plot on a logarithmic time axis, less is better; an arrow leads from each 3.12.0 time to the 3.13.0 time. " + "; ".join(desc) + ".", b) # ---------- json_view: parse throughput ---------- @@ -304,13 +310,13 @@ def cliffs(): 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]), fresh documents, from the table in "How it compares" - ("json_document", "nj-charts-c1", [183, 364, 943, 22681]), + ("nlohmann/json json_document", "nj-charts-c1", [183, 364, 943, 22681]), ("yyjson", "nj-charts-c2", [194, 478, 1489, 30342]), ("simdjson DOM", "nj-charts-c3", [202, 486, 2097, 26904]), ("sonic-rs (Rust)", "nj-charts-c4", [302, 664, 1907, 30202]), ("sonic-cpp", "nj-charts-c5", [379, 986, 2580, 40804]), ("Boost.JSON", "nj-charts-c6", [868, 1483, 2481, 88925]), - ("json::parse", "nj-charts-c1 nj-charts-dim", [1352, 2386, 5915, 163750]), + ("nlohmann/json json::parse", "nj-charts-c1 nj-charts-dim", [1352, 2386, 5915, 163750]), ] VIEW = [(e, c, [n / (t * 1000) for n, t in zip(VIEW_BYTES, us)]) for e, c, us in VIEW_US] # GB/s @@ -321,7 +327,6 @@ def view(): hi = math.ceil(hi * 2) / 2 X = lambda v: L + v / hi * (R - L) bh, gap = 9, 2 - top = 64 group_h = len(VIEW) * (bh + gap) + 14 b, desc = [], [] x, ly = 8, 10 @@ -332,13 +337,14 @@ def view(): b.append('' % (cls, x, ly)) b.append('%s' % (x + 18, ly + 10.5, esc(eng))) x += w + top = ly + 34 # below the last row of the legend 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)) + b.append('GB/s, more is better' % ((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))) @@ -349,8 +355,8 @@ def view(): 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 and six other document parsers, fresh documents, Apple M1 Max", - "Grouped horizontal bars in GB/s. " + "; ".join(desc) + ".", b) + "Parse throughput of nlohmann/json's json_document and json::parse and five other document parsers, fresh documents, Apple M1 Max", + "Grouped horizontal bars in GB/s, more is better. " + "; ".join(desc) + ".", b) if __name__ == "__main__":