Skip to content

sparkles:wired — runtime JSON benchmark baseline

The evidence base for replacing std.json inside sparkles:wired with a state-of-the-art JSON parser. Numbers from the harness at libs/wired/bench/runtime; the raw snapshot is results/2026-07-05-ryzen9-7940hx-x86-64-v4.json.

Environment

CPUAMD Ryzen 9 7940HX (Zen 4, AVX-512)
D toolchainLDC, front-end 2.111, -mcpu=native (bench build type)
Shim ISA presetx86-64-v4 (simdjson: runtime dispatch, icelake kernel)
Enginessimdjson 4.6.0, rapidjson 1.1.0, yyjson 0.12.0, serde_json 1.0.150, simd-json 0.17.0, sonic-rs 0.5.8, mir-ion 2.3.5, asdf 0.8.0, jsoniopipe 0.2.7
Corporatwitter.json 632 KB (strings), citm_catalog.json 1.7 MB (structure), canada.json 2.2 MB (floats), github_events.json 65 KB (small-doc)

Every engine reproduced the std.json structural fingerprint on every corpus, and the TwitterStats checksum on the decode op, before being timed. Throughputs are MB/s over the median iteration. Hardware counters come from a separate perf_event_open counting pass per op (kernel+user; the LLC pair was dropped because the NMI watchdog holds one of Zen 4's six PMCs and a multiplexed group only yields rotation-scaled estimates).

The headline: typed decode (twitter.json)

The op closest to wired's real workload — raw text → a partial Twitter struct:

EngineMB/s× wired today
wired (parseJSON + fromJSON)1591.0
std.json manual extraction1571.0
mir-ion1 74010.9
serde_json1 89611.9
asdf1 99412.5
sonic-rs2 06913.0
simd-json2 10113.2
yyjson (accessor walk)3 51822.1
simdjson On-Demand7 59047.7

Parse (full DOM/tape, immutable input)

Enginetwittercitm_catalogcanadagithub_events
std.json16314378174
jsoniopipe320297106387
serde_json425779496540
mir-ion498441185505
rapidjson (full precision)9091 588365883
simd-json1 1021 0214571 431
sonic-rs2 0441 9351 2832 421
asdf ¹2 7242 4281 0453 208
yyjson4 0223 9661 3604 257
simdjson DOM5 3205 5751 4495 815
simdjson On-Demand (full walk)4 2304 3861 1464 897

¹ asdf's tape keeps numbers textual (decoded on access), which flatters its parse column — most visible on float-heavy canada, where engines that materialize doubles pay for exact parsing.

Hardware counters (twitter.json)

The "why" behind the tables above — per input byte, over the counting pass:

EngineopIPCcyc/Bins/Bbr-miss%faults/iter
std.jsonparse2.4337.9792.420.67171.8
wireddecode2.3340.1793.700.79211.2
mir-iondecode3.102.898.930.200
serde_jsondecode3.293.6311.940.270
sonic-rsdecode4.002.359.390.090
asdfparse1.672.844.741.220
yyjsonparse3.681.264.640.130
simdjson DOMparse3.440.963.290.150
simdjson On-Demanddecode3.490.682.380.120

What the counters add to the findings:

  • The 48× decode gap is an instruction-budget gap, not an IPC gap. wired burns 93.7 instructions per byte where simdjson On-Demand spends 2.38 (≈ 39×), while IPC differs only 2.3 vs 3.5 (≈ 1.5×). The replacement parser must do less work per byte — fewer instructions — not merely schedule the same work better.
  • Branch discipline is visible and worth ~0.5–1 IPC. The fast engines (yyjson, sonic-rs, simdjson) all sit at ~0.1% branch misses; std.json and wired sit at 0.7–0.8%, serde_json's eager parse at 1.6%. yyjson's documented branch-layout work shows up exactly as advertised.
  • asdf's ceiling is its tape walk: the lowest IPC in the field (1.67) and the highest miss rate among the fast engines (1.2%) — a dependent-chained, branchy traversal — caps an otherwise tiny instruction budget (4.7 ins/B).
  • Page faults are the GC signature. GC-backed engines (std.json, wired, asdf's tape buffers) fault 100–300×/iteration in whichever counting pass catches the GC heap growing (the exact rows vary run to run); every native engine sits at 0 after warmup, always. An arena- or reuse-oriented document representation eliminates this class of cost outright.

Other ops in brief (twitter): validate — simdjson-OD structural skip 6 798, serde_json IgnoredAny 2 731, simd-json to_tape 2 571, sonic-rs 2 211, rapidjson SAX 1 051, jsoniopipe drain 678. serialize — yyjson 5 026, sonic-rs 2 445, simdjson-DOM 2 078, serde_json 1 630, std.json 179.

Findings

  1. wired is parser-bound, not mapping-bound. The fromJSON DbI layer costs nothing measurable (159 vs 157 MB/s for hand-written extraction); std.json.parseJSON is the whole bottleneck. Replacing the parser lifts wired directly.
  2. The state of the art is 10–48× away. Every serious engine decodes typed structs at 1.7–2.1 GB/s; going through a compact DOM first (yyjson, 3.5 GB/s) or lazy extraction (simdjson On-Demand, 7.6 GB/s) goes further. A wired parser at 1.5 GB/s twitter-decode (~10×) is a realistic v1 bar; the lazy design points at 3+ GB/s.
  3. SIMD is one road, not the only one. yyjson — deliberately scalar C — parses at 4 GB/s on structure/string corpora and 1.36 GB/s on floats, beating every SIMD engine except simdjson. Careful scalar D (branch layout, arena document, deferred number decode) gets most of the way; a vectorized structural scan (simdjson/sonic style) buys the rest.
  4. Laziness is the biggest single lever for typed decode. simdjson On-Demand extracts the twitter subset at 7.6 GB/s because untouched fields are skipped, not parsed — and its skip-only "validate" runs at 6.8–8.0 GB/s. wired's decode always knows the target struct, so an on-demand cursor (rather than a DOM) fits wired's shape exactly.
  5. Float parsing is its own battleground. canada.json compresses every ranking: exact double parsing (Eisel–Lemire in simdjson/serde/yyjson; rapidjson needs kParseFullPrecisionFlag to even qualify) costs ~3× the throughput of the string-heavy corpora. A replacement parser needs a first-class fast-float path from day one.
  6. The D ecosystem today doesn't reach the bar. mir-ion (0.2–0.5 GB/s parse; 1.7 GB/s decode) and asdf (fast tape, but lazy numbers and a dated codebase) are solid but 2–4× behind the C/C++/Rust frontier on comparable work; jsoniopipe's typed deserialize additionally leaves string escapes undecoded (caught by the checksum verification, excluded from the decode op).
  7. Allocation and copies dominate the tail. The immutable-input contract makes engines pay their real ingestion cost: simd-json's required &mut copy halves its parse column, and rapidjson's in-situ variant beats its copying parse by 33% on twitter. A wired parser should parse from const(char)[] without requiring caller copies, and keep its own scratch reusable.

Reproducing

The bench now runs on sparkles:test-runner (the bespoke executable harness this snapshot was recorded with is gone — the C/C++/Rust engines are not wired up on the port yet):

sh
cd libs/wired/bench/runtime
dub test -b bench -- --bench --perf --group-by=dataset,operation \
    --bench-min-time=2000 \
    --bench-json=results/$(date -I)-<host>-$WIRED_BENCH_ISA.json

--bench-min-time=2000 is required for baselines: the runner's default budget is 5 ms, and short budgets under-report allocation-heavy paths (yyjson's copying parse measured 1.6 GB/s at a 300 ms budget vs 4.0 GB/s at the old 2 s default — cold pages dominate the first few thousand iterations). Two consecutive 2 s-budget runs on the machine above agreed within ~5% on every spot-checked row.

Old → new JSON field mapping: enginename; dataset/oplabels.*; iterssamples; mbPerSecmetrics["B/s"] / 1e6; raw perf counter totals → per-iteration catalog cells (ipc, instr, …); meanNs has no successor (the runner reports median absolute deviation). The port also builds without -enable-cross-module-inlining (mir-ion breaks under it) — a known ~15% delta on wired's number-heavy hot loops. Numbers are machine- and preset-specific; compare only within one snapshot.