* ci: update to checkout v6
v4 and older causes warnings about deprecated node.js
* ci: update to actions cache v5
the older versions cause deprecation warings for node.js 20
* docs: clarify raw_json() consumption and rewind option
Document that raw_json() consumes objects/arrays and point readers to
reset() or document::rewind() when they need to parse again.
Related to #1943
* ci: retrigger workflow
The dominant cost in the reflection serializer was the strict-aliasing
reload pattern: every char* write through `string_builder::buffer.get()`
forced the compiler to assume `string_builder::position` and `::capacity`
may have been clobbered, so it reloaded both members from memory after
each byte. perf annotate showed `ldr [position]` and `ldr [capacity]`
taking 15-20% of CITM serialization time.
This change refactors the reflection atom path to use an internal
`writer` struct (buffer pointer + position + capacity + back-ref to
string_builder). The writer lives as a stack-local in the top-level
append() entry point, threaded through the inlined atom() chain via
`writer&`. After SROA, the three fields are register-resident across
the entire serialization. The `pos` is never round-tripped through
memory between writes — it's a local size_t.
This mirrors the pattern Glaze uses (passing `B&& b, auto&& ix` through
every helper). The simdjson-specific bit is keeping the public
string_builder API intact: only the internal atom() family is
refactored to take `writer&`. The top-level append(string_builder&, T)
constructs a writer on the stack, threads it through atom(), then
syncs the local position back at the end.
For string fields specifically, the escape path is also inlined
through the writer — the existing `write_string_escaped(input, out)`
helper already takes a destination pointer and returns bytes-written,
so it composes cleanly with `w.ptr + w.pos`. Without this, sync/
reload around each string write was a real cost on Twitter (-7%).
Performance
===========
TRUE A/B in single docker invocation, 7 alternating rounds, byte-
identical output, all static_reflection_comprehensive_tests pass.
Build: clang-p2996 21.0.0git, -O3 -DNDEBUG, aarch64 Linux.
CITM Catalog (496 682 bytes):
baseline (master): 4356 MB/s
patched: 6776 MB/s +55.5%
Glaze: 4860 MB/s simdjson now 39.4% AHEAD of Glaze
Twitter (81 927 bytes):
baseline (master): 6437 MB/s
patched: 6452 MB/s +0.2% (break-even)
Twitter is string-heavy; the writer's gain over the existing
member-based path is small here, but it no longer regresses.
Default initial capacity is unchanged at 1024.
* Add tests for dumping out NaN/Infinity to the json builder
* If SIMDJSON_ENABLE_NAN_INF is set, write out NaN and Infinity
* Add tests for dumping out NaN/Infinity when serializing DOM
* If SIMDJSON_ENABLE_NAN_INF, use 'NaN' and 'Infinity' for nan/inf
* Fix bug where 'Inf' as a root atom + spaces of padding parses incorrectly
* Update FracturedJson printers so tables containing NaN/Infinity are aligned
* add compile option 'SIMDJSON_ENABLE_NAN_INF' but disable by default
* extend parser to support NaN/Infinity when SIMDJSON_ENABLE_NAN_INF=1
* update tests to check parsing of NaN/Infinity, when enabled
* update minefield tests: mark nan/inf tests as passing when nan/inf is ON
* update CI/CD to run tests with extensions for NaN/Infinity enabled
* builder: force-inline atom templates and replace integer writer
Two complementary changes that together speed up reflection-driven JSON
serialization by ~28% on integer-heavy structs (CITM) and ~12% on
string-heavy structs (Twitter).
(1) Add simdjson_really_inline (always_inline) to the hot atom<T>
template overloads (arithmetic, struct, container, optional,
string-like). The constexpr-only declaration was only a hint; the
compiler routinely chose to leave atom<unsigned long> as a real
out-of-line function.
(2) Replace string_builder::append<UInt> body with a forward
cascade-on-magnitude integer writer. The old code computed
digit_count(v) upfront and wrote backward in a loop; the new code is
a straight-line if/else cascade that writes digits forward, no loop,
no helper call.
Either change alone gives only modest gains. Together they unlock the
compiler's cross-call optimization: with all atoms inlined and the
integer writer reduced to straight-line code, the compiler can hoist
b.position into a register across the whole struct serialization, fold
redundant capacity_check calls, and eliminate the strict-aliasing
penalty that otherwise forces b.position/b.capacity reloads after every
char* write.
Output is byte-identical to master on CITM (496682 bytes) and Twitter
(81927 bytes). All static_reflection_comprehensive_tests pass.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* builder: de-recurse write_uint_jeaiii so always_inline applies on g++/MSVC
g++ ('inlining failed in call to always_inline ...: function not
considered for inlining') and MSVC ('warning C4714: __forceinline not
inlined' under warnings-as-errors) both refuse to inline recursive
functions marked simdjson_really_inline. The original write_uint_jeaiii
called itself in the >=10^4 branches.
Refactor into a non-recursive DAG of helpers: write_lt100, write_lt10000,
write_4_digits, write_lt1e8, write_uint_jeaiii. Each calls strictly
smaller-domain helpers, no cycles. Same straight-line cascade behavior,
same byte output, but every node is now a candidate for always_inline on
all compilers.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* builder: port signed-int append to jeaiii writer and drop digit_count helpers
The signed-integer branch in string_builder::append was structurally
identical to the OLD unsigned branch — same digit_count() upfront +
backward 4-digit batched loop. Port it to use the same forward
write_uint_jeaiii() helper as the unsigned branch (write '-'
unconditionally and advance position only if negative — branchless).
This makes int_log2 / fast_digit_count_32 / fast_digit_count_64 /
digit_count fully unused (verified via grep across include/ and src/);
remove them, ~80 lines of dead code.
The signed write path now benefits from the same compiler-level
optimization (full inlining, capacity-check fusion, no opaque-loop
boundary) as the unsigned path. Signed-int microbench (200 × 100k
values, mixed magnitude and sign): 2272 → 2685 MB/s (+18.2%).
CITM and Twitter benchmarks unchanged in shape and remain byte-identical
to baseline output.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* fix tests
---------
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Co-authored-by: Daniel Lemire <daniel@lemire.me>
Each non-first field in a reflection-driven struct serialization was
emitting three separate string_builder::append calls (',', "\"key\"", ':'),
each going through capacity_check + a small write. Combine them into a
single compile-time string per field so each field does one capacity_check
and one memcpy. Same pattern fixed in atom(), append(), and extract_from().
Measured under clang-p2996 -O3 -DNDEBUG -freflection -std=c++26
(median of 5 contemporaneous runs, output byte-identical to baseline):
CITM serialization (496682 bytes):
simdjson_reuse_buffer: 3192 -> 3673 MB/s (+15.1%)
simdjson_static_reflection: 3014 -> 3443 MB/s (+14.2%)
simdjson_to: 2717 -> 3234 MB/s (+19.0%)
simdjson_to_reuse: 2836 -> 3226 MB/s (+13.7%)
Twitter serialization (81927 bytes):
simdjson_reuse_buffer: 6637 -> 7162 MB/s (+7.9%)
simdjson_static_reflection: 5527 -> 5915 MB/s (+7.0%)
simdjson_to: 5070 -> 5350 MB/s (+5.5%)
simdjson_to_reuse: 4976 -> 5303 MB/s (+6.6%)
extract_from (out-of-tree micro-bench, 100 records per call):
User 4-of-9 fields: 1833 -> 1888 MB/s (+3.0%)
Status 3-of-6 fields: 4286 -> 4334 MB/s (+1.1%)
Parsing benchmarks unchanged. tests/builder/static_reflection_comprehensive_tests
still passes (round-trip through extract_from / extract_into is verified).
* add std::ranges support for On-Demand API (#2382)
Add zero-cost range wrappers (array_range, object_range) that satisfy
std::ranges::input_range, enabling std::views::transform and other
C++20 range adaptors with the On-Demand parser.
Uses direct forwarding via simdjson_inline with no value buffering,
avoiding the per-element overhead (~20%) of the previous approach.
Guarded by SIMDJSON_SUPPORTS_RANGES.
* fix: replace non-ASCII em dash in test comment
The just_ascii CI check flags any non-ASCII characters in source files.
* let us see what we get with this...
* minor tweak
* minor update
* update doc
---------
Co-authored-by: Justin Li <justin53@bu.edu>
* adding memory-file mapping to Windows.
* making memory-file mapping optional under windows, as it is fragile
* removing non-ascii
* saving.
* bumping up
* take 2