Repository navigation
Conversation
The vector UTF-8 check of json_view needed SSSE3 at compile time (JSON_VIEW_USE_SSSE3 with -mssse3), so default x86-64 builds validated non-ASCII text one sequence at a time. The check is now compiled for SSSE3 with a function attribute (GCC 4.9 and later, Clang; MSVC compiles the intrinsics anyway) and used where CPUID reports SSSE3. The answer is kept in an atomic that is initialized at compile time, so neither a guard nor a global constructor is needed. The definitions do not depend on compiler flags, so there is no ODR issue. JSON_VIEW_USE_SSSE3 now only skips the CPU check. On x86-64 Linux (Haswell), twitter.json parses 23% faster with Clang 18 and 25-34% faster with GCC 13, now ahead of yyjson. Also always inline read_eight_bytes() and parse_eight_digits(): GCC called both in the number loops of the lexer and of json_view (52 call sites), which cost about 10% on citm_catalog.json at -O2. Document that reusing a document with read() avoids the page faults of a fresh node index (about 40% of a 55 MB parse on x86-64 Linux). Signed-off-by: Niels Lohmann <mail@nlohmann.me>
- compare.py: make --data, --corpus, and --build-dir absolute, since the benchmarks run in the build directory; download into a .part file and remove an archive whose SHA-256 does not match, so that an interrupted download is not kept - bench_view/bench_corpus/bench_edit: report files that cannot be opened instead of aborting; run each engine once untimed before its timed call, so that no engine pays for the allocator cleaning up after the previous one (with glibc, json_view after json::parse looked 1.7x slower on citm_catalog traverse); add "simdjson DOM (fresh)" and time "json_view (reused)" for traverse and select too - README: explain fresh vs. reused documents and page faults on Linux Signed-off-by: Niels Lohmann <mail@nlohmann.me>
nlohmann
added this pull request to stack #5739
October 5, 2026 18:26
- zmij::to_shortest() keeps the last digit apart from the 15 or 16 digits before it, and dtoa_impl::write_shortest() converts those at once: with SSE2 on x86-64 and NEON on 64-bit Arm (no CPU check needed), else eight digits at a time. The decimal point is inserted in the register; reading the digits back right after storing them stalled store forwarding. - json::dump() writes floats and integers straight into its write buffer instead of copying them from number_buffer, and integers below 10^16 eight digits at a time. - json_view's dump() writes doubles from their bits and tokens of up to 15 digits through the same code; its own NEON writer is removed. to_chars() on canada.json: 35 -> 29 ns per double (x86-64), 18.8 -> 14.2 ns (Apple M1); json::dump() of canada.json 16-29% faster, of citm_catalog.json 14-19%. Signed-off-by: Niels Lohmann <mail@nlohmann.me>
- With SSE2, string runs are checked 16 bytes at a time from their first byte: one compare finds the end of most keys and short values, faster on x86-64 than a branch per byte. AArch64 keeps the byte steps (there, a NEON mask costs more and the branches predict well; vector-first was 20% slower on Apple M1). - open() stores the parent's frame field by field. Built on the stack and copied, it was read back by loads wider than its stores, which waited for them (store forwarding fails): 18% of the time on citm_catalog.json. Parse on x86-64 (GCC 13 / Clang 18, us): twitter 352 -> 306 / 316 -> 288, citm_catalog 1000 -> 770 / 843 -> 692, canada 1941 -> 1687 / 1978 -> 1722. Unchanged on Apple M1. Signed-off-by: Niels Lohmann <mail@nlohmann.me>
- The output string reserves the size estimate (the source extent) and grows in steps of 64 KiB within it, instead of being resized to the estimate at once: resize() zero-fills, and for a pretty-printed source the estimate is far larger than the compact output. citm_catalog dump: 342 -> 283 us on x86-64, 144 -> 134 us on Apple M1. - bench_corpus compares the dump with source numbers with yyjson writing numbers read as raw text (YYJSON_READ_NUMBER_AS_RAW); both write the same bytes. Signed-off-by: Niels Lohmann <mail@nlohmann.me>
nlohmann
removed this pull request from stack #5739
October 6, 2026 09:26
Owner
Author
|
The zero-copy view stack (#5295) was rebased onto develop and regrouped from 20 PRs into 10, one squashed commit each. The changes of this PR are now in #5634 (core digit formatting and inlining), #5628 (SSSE3 dispatch, string scan, builder stall), #5633 (view dump), and #5627 (benchmark fixes); its review comments stay here for reference. Written by Claude Code. |
This was referenced Oct 6, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Part of the stack for the zero-copy view (#5295). Speeds up
json_viewon x86-64 Linux, closes most of the gap between GCC and Clang, formats the digits of numbers faster forjson::dump()andjson_viewon all platforms, and fixes the comparison benchmark of #5627, which madejson_viewlook slower on Linux than it is.Summary
JSON_VIEW_USE_SSSE3with-mssse3), so a default x86-64 build validated non-ASCII text one sequence at a time.std::atomic<int>that is initialized at compile time: no guard of a local static, no global constructor (-Wglobal-constructors).JSON_VIEW_USE_SSSE3now only skips the CPU check;JSON_VIEW_NO_SIMDstill selects the portable code.detail::read_eight_bytes()anddetail::parse_eight_digits(). GCC called both as functions in the number loops of the lexer and of the view's parser (52 call sites). This cost about 10% on citm_catalog at-O2.open()stores the parent's frame field by field. Built on the stack and copied, it was read back by loads wider than its stores, which waited until the stores were done (store forwarding fails); this was 18% of the parse time of citm_catalog on x86-64. Apple M1 forwards such stores and does not change.json::dump()andjson_view'sdump()):zmij::to_shortest()keeps the last digit apart from the 15 or 16 digits before it, as upstream Zmij does, so they are converted without first multiplying by 10 and counting digits.dtoa_impl::write_shortest()converts the 16 digits at once in one vector register: with SSE2 on x86-64 and NEON on 64-bit Arm, both baseline, so there is no CPU check. Other CPUs convert eight digits at a time.json::dump()writes floats and integers straight into its write buffer instead of copying them fromnumber_buffer(the same stall), and integers below 10^16 eight digits at a time.json_view'sdump()writes doubles from their bits, without the checks ofto_chars(), and tokens of up to 15 digits through the same code (their digit count is known from the token). Its own NEON writer is removed.write_decimal().json_view'sdump()reserves its size estimate (the source extent) and grows the output string in steps of 64 KiB within it, instead of resizing it to the estimate at once.resize()zero-fills, and for a pretty-printed source the estimate is far larger than the compact output: citm_catalog dump 342 -> 283 µs on x86-64, 144 -> 134 µs on Apple M1.tests/benchmarks/json_view, see Compare json_view with yyjson, simdjson, and Boost.JSON #5627):json::parseran last in each round and freed about 40,000 nodes; the next engine, alwaysjson_view, paid for glibc cleaning them up. This madejson_viewlook 1.7 times slower than yyjson on citm_catalog traverse.bench_corpuscompares the dump with source numbers (number_format::source) with yyjson writing numbers read as raw text (YYJSON_READ_NUMBER_AS_RAW); both write the same bytes.compare.pymakes--data,--corpus, and--build-dirabsolute. Before, the benchmarks, which run in the build directory, aborted with a relative--data.compare.pydownloads into a.partfile and removes an archive whose SHA-256 does not match, so an interrupted download is no longer kept forever.JSON_VIEW_USE_SSSE3describes the run-time check.read()notes that reusing a document avoids the page faults of a fresh node index, about 40% of the parse time of a 55 MB document on x86-64 Linux.Results
x86-64 Linux (KVM, Haswell with AVX2, Ubuntu 24.04),
compare.py --download --rounds 30, fresh documents. Times in µs; the yyjson and simdjson DOM columns are from the same run as "after" (Clang / GCC).With twitter and citm_catalog,
json_viewis now ahead of yyjson with both compilers, and 10–20% behind simdjson, which uses AVX2.Parse after the two parser changes (x86-64, pinned to one core, best of 7, µs):
simdjson's remaining lead on twitter and citm_catalog came from AVX2: with its SSE4.2 kernel forced (
SIMDJSON_FORCE_IMPLEMENTATION=westmere), simdjson is 8-18% slower thanjson_view. AVX2 does not helpjson_view: 32-byte versions of the string and indentation loops were slower than the 16-byte ones, as most runs are shorter than 32 bytes. On Apple M1,json_viewis ahead of yyjson and simdjson for parse, traverse, select, and dump of twitter, citm_catalog, and jeopardy.Digit formatting (best of several runs; the VPS had other load, so a few percent are noise):
to_chars(), canada doubles, x86-64 (GCC / Clang)to_chars(), canada doubles, Apple M1json::dump()canada, M1json::dump()canada, x86-64 (compare.py, GCC 13)json::dump()citm_catalog, x86-64json_viewdump canada, x86-64json_viewdump canada, M1to_chars()alone is now faster than yyjson's writer of one number (34 ns on x86-64). Thejson_viewdump of twitter, citm_catalog, and jeopardy is level with or ahead of yyjson and 2.4-2.9x faster than simdjson DOM.Dump with the numbers of the input (µs; same output bytes):
On canada on x86-64, yyjson's lead is mostly the zero-fill of
std::string::resize(), which yyjson'smalloced output does not have. Before C++23 a string cannot grow without it, and C++23'sresize_and_overwriterequires the whole dump inside its callback within a fixed size.What tuning cannot close:
Tried and dropped:
write_decimal(): slower. The store-forwarding stall it avoids is only about 14% of that function.json_viewtokens: 17% slower on M1 than taking the count from the token.-msse4.1(GCC's-O3code is worse without SSE4.1; not pursued).resize_and_overwrite(C++23) for the dump output: 9-20% faster, but growing the string and writing later violates its precondition (all bytes up to the returned size written by the callback).Verification
test-json_view*, GCC 13 and Clang 18, x86-64): assertion results identical to the base branch. The base already fails 5 assertions on Linux, 4 in the comment handling oftest-json_view_builderand 1 intest-json_view, as part of the current CI breakage.fuzzer-parse_json_viewwith ASan and UBSan, with the run-time dispatch active.json::accepton 200,000 random UTF-8 strings each, including ill-formed ones.-Weverything -Werror) are clean forx86_64andarm64, C++11/17/20. With the CI's own compilers (gcc:16, silkeh/clang:dev in Docker) and flags, the digit formatting adds no warning on amd64 or arm64 (GCC's-Winlineat-O2nameswrite_shortest()instead ofto_decimal()); clang-tidy with the repository's configuration is clean onunit-to_chars.cpp.to_chars()writes the same text as before, with SSE2 (x86-64), NEON (arm64), and GCC 4.9/5, Clang 3.5/5 in Docker; also into short buffers. Integer dumps equalstd::to_string()for 10M+ values and all boundaries.json_view's dump equalsordered_json::parse(text).dump()for 6.4M random number tokens. The full test suite with ASan and UBSan (Apple M1 with Clang, x86-64 with GCC 13) fails only the 5 assertions that already fail on the base branch; also after the parser changes, whose warnings with the CI's compilers equal the base branch's.Public API
No breaking changes:
JSON_VIEW_USE_SSSE3keeps its meaning for the result. Without it, x86-64 builds now use SSSE3 when the CPU has it; with it, the CPU is not checked. The same inputs are accepted or rejected either way.read_eight_bytes()andparse_eight_digits()are indetailand only change their inlining.detailand writes the same text.json::dump()no longer usesnumber_bufferfor floats and integers (a private member).Written by Claude Code.
🤖 Generated with Claude Code