jsonvalidate¶
Valid(data []byte) bool — a drop-in replacement for encoding/json.Valid that
returns the byte-identical verdict on every input while accelerating the
hottest sub-scans of JSON validation with SIMD on all six 64-bit Go targets.
No cgo, no GOEXPERIMENT, plain go build. As far as we know, it is the only
SIMD JSON validator that covers all six of Go's vector architectures.
Repository →
import "github.com/go-simd/jsonvalidate"
ok := jsonvalidate.Valid(data) // == encoding/json.Valid(data), always
Correctness is the gate, not a goal: Valid(data) == encoding/json.Valid(data)
is asserted on the seed table, on 60 000 deterministic pseudo-random inputs on
every arch (including under qemu), and by a go test -fuzz differential fuzzer
on amd64 (AVX2 and SSE paths) and arm64. There is no input on which the answer can
differ — the SIMD kernels only ever return an index the scalar reference would
also have returned, and anything they don't accelerate runs through a faithful
scalar reimplementation of encoding/json's scanner.
Why this exists¶
minio/simdjson-go is a superb full SIMD JSON parser, but it is amd64-only. jsonvalidate is scoped to validation — the common "is this valid JSON?" gate — and trades full-parse SIMD for portable SIMD on every architecture Go can target with vectors: amd64, arm64, riscv64, loong64, ppc64le and s390x.
Scope: what is SIMD, what is scalar (honest split)¶
A full simdjson "stage 1" computes, for a whole block, which structural characters
fall inside strings. That needs a prefix-XOR over the quote bitmask, which on
amd64 is a carry-less multiply (PCLMULQDQ) with no portable equivalent on
the other five targets — so a single full-SIMD grammar across six arches is not
feasible. jsonvalidate therefore keeps the grammar — bracket balancing,
number/literal syntax, the exact 10 000-deep nesting limit, and every
encoding/json acceptance rule — in a correct scalar state machine, and uses
SIMD only for the two scans whose answer is a pure, position-independent "find the
first byte in (or not in) a small set":
scanString— from just after an opening quote, find the first",\, or control byte (< 0x20). (encoding/jsondoes not validate UTF-8 inside strings, so a byte-class scan is exactly right.)skipSpace— find the first byte that is not JSON whitespace (,\t,\n,\r).
Both kernels are shuffle/compare only — a per-byte nibble-LUT classification
(c is in the set iff loLUT[c&0xF] & hiLUT[c>>4] != 0) plus a first-match
extraction. No multiply, no cross-lane carry, so the same algorithm ports to all
six arches. They are pure accelerators bolted onto an always-correct scalar
validator.
Per-arch kernels¶
| arch | ISA | classify | first match |
|---|---|---|---|
| amd64 | SSSE3 + AVX2 | (V)PSHUFB ×2, (V)PAND |
(V)PMOVMSKB+BSF; AVX2 (32-byte) auto-selected via golang.org/x/sys/cpu |
| arm64 | NEON | VTBL ×2, VAND |
lane→GPR via VMOV V.D[i], RBIT+CLZ |
| riscv64 | RVV | VRGATHERVV ×2, VANDVV |
VMSNE/VMSEQ + vfirst.m |
| loong64 | LSX | VSHUFB ×2, VANDV |
lane→GPR via VMOVQ V.V[i], CTZV |
| ppc64le | VSX | VPERM ×2, VAND |
mask→GPR via MFVSRD, CNTTZD |
| s390x | vector facility (big-endian) | VPERM ×2, VN |
VFENEBS (find-element-not-equal w/ CC), index via VLGVB |
VSX is baseline on POWER8+ and the vector facility on z13+, so ppc64le and s390x
(like riscv64 and loong64) need no runtime feature dispatch — the SIMD path is the
arch's only path, build-tagged. s390x is the one big-endian target: VL puts
the lowest memory address in lane 0 and VFENEBS scans lane 0 upward, so the
returned byte index is already the memory offset of the first match — the
classification LUTs are verified in big-endian lane order by the
position-dependent random/fuzz tests. The assembly is generated by
go-asmgen (scan_*_gen.go; the .s files
are committed).
Validation status per arch¶
| arch | how validated | status |
|---|---|---|
| amd64 (AVX2 + SSE) | native x86_64 VM, table + 60k random + -fuzz (both paths) |
native |
| arm64 | native, table + 60k random + -fuzz |
native |
| riscv64 | real SpacemiT X60 (RVV 1.0, GCC Compile Farm), table + 60k random + native bench | native (measured) |
| loong64 | qemu la464, table + 60k random |
qemu-validated |
| ppc64le | real POWER9 (GCC Compile Farm, VSX), table + 60k random + native bench | native (measured) |
| ppc64 (BE) | real POWER9 (big-endian, scalar fallback), table + 60k random | native build+test |
| s390x | qemu (big-endian), table + 60k random | qemu-validated; native perf pending |
Six SIMD targets, validated on seven architectures.
Performance — honest¶
vs encoding/json.Valid (arm64, Apple silicon; representative, not a contest):
| input | jsonvalidate | encoding/json | ratio |
|---|---|---|---|
| string-heavy (long string values) | ~710 MB/s | ~515 MB/s | ~1.4× faster |
| realistic mixed records | ~465 MB/s | ~490 MB/s | ~on par |
| number-heavy array | ~140 MB/s | ~585 MB/s | slower |
| compact-whitespace (4-space indent) | ~215 MB/s | ~490 MB/s | slower |
On real silicon (GCC Compile Farm, Go 1.26.4, 2026-06-26) the string fast path wins big where stdlib is slowest: string-heavy JSON runs ~8.5× the stdlib on real POWER9 (~406 vs ~48 MB/s) and ~5.2× on a real SpacemiT X60 (RVV 1.0, ~191 vs ~36.5 MB/s); number-/structure-heavy input stays on the scalar path with no speedup, the same honest split. s390x throughput is still pending native hardware.
The SIMD scans win where they apply — long string bodies, which dominate most
real-world JSON (text, logs, API payloads). Where the document is mostly numbers,
structure, or short (< 16-byte) whitespace runs, the scalar grammar dominates and
encoding/json's long-tuned scanner is faster. jsonvalidate's value is
correctness parity across six arches with a string fast path, not a universal
speedup: if your payloads are number-dense, encoding/json.Valid is the better
choice today (the scalar number scan is the obvious next thing to optimise).
Valid allocates nothing on any path.
Coverage¶
100% statement coverage gated on every architecture (amd64 AVX2+SSE and arm64
natively, riscv64/loong64/ppc64le/s390x under QEMU). The seed table, 60k
deterministic random inputs, and the differential fuzzer all assert exact parity
with encoding/json.Valid. BSD-3-Clause.