Pure-Go Stream VByte integer (uint32) compression with a SIMD encoder and
decoder on all six of Go's 64-bit SIMD targets — amd64, arm64,
ppc64le, s390x, riscv64, loong64 — and a portable scalar fallback
everywhere else. No cgo, no GOEXPERIMENT, plain go build.
Stream VByte is the byte-oriented integer codec of Lemire, Kurz & Rupp
(arXiv:1709.08990). A []uint32 is stored as
a control stream of 2-bit lengths (one byte per four integers) followed by a
data stream of each integer's significant little-endian bytes (1–4). Each
control byte indexes a 256-entry shuffle LUT that, in a single PSHUFB / VTBL /
VPERM / vrgather / vshuf.b, decodes four packed integers' bytes into
four zero-extended uint32 lanes — or, with the mirror-image LUT, encodes
four uint32 lanes by compacting their significant bytes back into the data
stream. (Per-group length classification stays scalar; the byte movement is the
vectorised part.)
The wire format is byte-for-byte identical to the reference C library (github.com/lemire/streamvbyte, standard 1234 variant), so output interoperates in both directions.
go get github.com/go-simd/streamvbyteimport "github.com/go-simd/streamvbyte"
src := []uint32{1, 280, 70000, 0xFFFFFFFF, 42}
// Encode.
buf := make([]byte, streamvbyte.EncodedMaxLen(len(src)))
n := streamvbyte.Encode(buf, src)
buf = buf[:n] // the compressed bytes
// Decode (the integer count is carried out-of-band, like the C library).
out := make([]uint32, len(src))
streamvbyte.Decode(out, buf, len(src))
// out == srcAPI:
| Function | Description |
|---|---|
EncodedMaxLen(n int) int |
upper bound on the encoded size of n integers |
Encode(dst []byte, src []uint32) int |
encode; returns bytes written |
Decode(dst []uint32, src []byte, n int) int |
decode n integers; returns bytes read |
Decode(Encode(x)) round-trips exactly. The count n is not stored in the
stream (store it yourself), matching the reference format.
Decode. Per group of four integers the kernel loads 16 data bytes, looks up
the control byte's 16-byte shuffle mask, performs one vector permute that drops
each integer's bytes into a zero-extended lane, and stores 16 result bytes. The Go
wrapper runs the kernel only over groups that have a full 16-byte data lookahead
and finishes the < 4 remainder (and any short-input tail) with the shared
scalar decoder, so the wide load never over-reads.
Encode is the inverse. The Go wrapper classifies each group's four integers
into 2-bit length codes (the control byte) and walks the resulting per-group data
lengths; the kernel then, per group, loads the 16 source bytes, looks up the
control byte's compaction mask (the mirror of the decode LUT), permutes the
significant low bytes of each lane contiguously into the data stream, and stores
16 bytes at the running data cursor (only 1..16 of them significant; the rest
are overwritten by the next group). The kernel runs only over groups with a full
16-byte store lookahead; the < 4 remainder uses the shared scalar encoder. Both
directions share the 256-entry LUT family in tables.go, so the wire bytes are
identical on every path. Length classification stays scalar, so encode's SIMD
speedup is smaller than decode's.
- amd64 —
PSHUFB(SSSE3; runtime-detected viagolang.org/x/sys/cpu, scalar fallback otherwise). - arm64 —
VTBL(NEON, baseline). - riscv64 —
vrgather.vv(RVV,VLEN >= 128). - loong64 —
vshuf.b(LSX) with a zero companion register. - ppc64le —
VPERMon POWER8+ (LXVB16X/STXVB16Xbyte-order-stable loads keep the index == memory-offset identity; zero companion register). - s390x —
VPERMon z13+. Big-endian: auint32is stored most-significant-byte-first while the data stream stays LSB-first, so the LUT reverses each lane's bytes (decodepermTableBE, encodeencodeShuffleTablePermBE; seetables.go). The byte order is pinned by a position-dependent test.
Each arch has both a decode and an encode kernel using the same primitive. The
assembly is generated by go-asmgen;
regenerate with go run decode_<arch>_gen.go / go run encode_<arch>_gen.go (the
.s files are committed).
go test -bench . on a 4096-element mixed-width slice (16 KiB of uint32):
| Target | Decode (SIMD) | Decode (scalar) | Speedup |
|---|---|---|---|
| arm64 (Apple M-series, native) | ~18.6 GB/s | ~1.9 GB/s | ~10× |
| amd64 (emulated VM*) | ~0.73 GB/s | ~0.32 GB/s | ~2.3× |
| ppc64le (POWER9, VSX, native) | ~3695 MB/s | ~311 MB/s | ~11.6× |
| riscv64 (SpacemiT X60, RVV 1.0, native) | ~829 MB/s | ~184 MB/s | ~4.5× |
| s390x (IBM z15, VXE2, native) | ~20× scalar (real silicon, 2026-07-03) | — | ~20× |
| loong64 (Loongson 3A5000, LSX, native) | ~11.8× scalar (real silicon, 2026-06-26) | — | ~11.8× |
| Target | Encode (SIMD) | Encode (scalar) | Speedup |
|---|---|---|---|
| arm64 (Apple M-series, native) | ~2.23 GB/s | ~2.06 GB/s | ~1.08× |
| amd64 (emulated VM*) | ~0.24 GB/s | ~0.20 GB/s | ~1.2× |
| ppc64le (POWER9, VSX, native) | full SIMD encode+decode | — | — |
| riscv64 (SpacemiT X60, RVV 1.0, native) | full SIMD encode+decode | — | — |
| s390x (IBM z15, VXE2, native) | ~1.4× scalar (real silicon, 2026-07-03) | — | ~1.4× |
| loong64 (Loongson 3A5000, LSX, native) | full SIMD encode+decode | — | — |
* The amd64 figures were measured inside an emulated x86-64 VM (no hardware
virtualization on the development host), so they understate native silicon by a
large margin; treat them as correctness-grade lower bounds, not hardware numbers.
Throughput is len(src)*4 bytes per slice. Decode is the algorithm's strong suit
(the whole hot loop vectorises); encode's gain is modest because only the byte
compaction is vectorised — the per-group length classification stays scalar.
Measured on real POWER9 (ppc64le VSX, GCC Compile Farm,
Go 1.26.4, 2026-06-26): SIMD decode ~11.6× the scalar baseline (3695 vs 311 MB/s),
plus full SIMD encode+decode. Also measured on real riscv64 (SpacemiT X60, RVV 1.0,
GCC Compile Farm, Go 1.26.4, 2026-06-26): SIMD decode ~4.5× the scalar baseline
(829 vs 184 MB/s). The X60 is a low-power in-order RVV 1.0 core — currently the
only widely-available RVV silicon — so this vrgather-bound decode would likely
do better on an out-of-order RVV part; treat it as a real but conservative win.
Also measured on real loong64 (Loongson 3A5000, LSX, GCC Compile Farm cfarm401,
Go 1.26.4, 2026-06-26): SIMD decode ~11.8× the scalar baseline, plus full SIMD
encode+decode (correctness-validated on real silicon).
Also measured on real IBM z15 (s390x vector facility, VXE2, native execution,
2026-07-03, -count=6): SIMD decode ~20× the scalar baseline — the largest
decode win in this table — while encode is a modest ~1.4× (only the byte
compaction vectorises; the per-group length classification stays scalar).
Existing Go implementations (thempatel/streamvbyte-simdgo, bmkessler/streamvbyte, mhr3/streamvbyte, nelz9999/stream-vbyte-go) ship SIMD only for amd64 (and scalar elsewhere). This package is, to our knowledge, the first to provide SIMD encode and decode on all six of Go's 64-bit SIMD architectures from one code base.
Round-trip table tests plus FuzzRoundTrip (Decode(Encode(x)) == x) run with
100% statement coverage on every architecture: amd64 and arm64 natively,
and ppc64le, s390x, riscv64, loong64 under QEMU (the same matrix the CI
runs). ppc64le and riscv64 are additionally exercised natively on real
POWER9 / SpacemiT X60 (RVV 1.0) silicon (GCC Compile Farm). The portable scalar
fallback is also build+test validated on
ppc64 (big-endian) on real POWER9 silicon — proving it bit-exact on a
big-endian target distinct from s390x's vector kernel. So: six SIMD targets,
validated on seven architectures. Format interop with the reference C library
was verified bidirectionally and byte-for-byte (Go→C decode, C→Go decode,
identical wire bytes).
BSD-3-Clause. See LICENSE.
