base32¶
A drop-in fast path for standard base32 (StdEncoding, RFC 4648, padded),
byte-identical to encoding/base32 (decode offsets included). Both encoding
and decoding run a SIMD kernel generated by
go-asmgen; the short tail + padding +
error reporting reuse the standard library.
Repository →
API¶
s := base32.EncodeToString(data) // same bytes as encoding/base32.StdEncoding
b, err := base32.DecodeString(s) // same bytes AND same error offsets
| op | amd64 | ppc64le | s390x | arm64 | loong64 / riscv64 |
|---|---|---|---|---|---|
| encode | AVX2 + SSE2 | VSX | vector facility | NEON on Go 1.27+, scalar on stable | scalar (stdlib) |
| decode | AVX2 + SSE2 | VSX | vector facility | scalar (stdlib) | scalar (stdlib) |
ppc64le, s390x and (on Go 1.27+) arm64 run the full spread-extract kernel —
the same algorithm amd64 uses — because POWER (VSX), IBM Z (vector facility) and
NEON each provide the per-lane variable shift / integer vector multiply the
kernel needs. On arm64 those ops (VUMULL, VUSHL, VTBL) were only added to
the Go assembler in Go 1.27, so on stable Go ≤ 1.26 arm64 encode falls
back to encoding/base32. ppc64le is now natively measured on real POWER9
(GCC Compile Farm, VSX, Go 1.26.4, 2026-06-26): SIMD decode ~5.5× the stdlib
scalar decoder (621 vs 113 MB/s) — a real VSX kernel (VSRH) on hardware where
arm64 stable can't run one. The arm64 NEON kernel is validated on native arm64
under gotip (~2.1× the stdlib encoder); s390x is now natively measured on real IBM z15 (VXE2) (2026-07-03,
-count=6): SIMD decode ~8.4× and encode ~3.4× the stdlib scalar baseline.
Algorithm¶
Per 5-byte group → 8 chars (base32 packs 5-bit groups: 5 bytes = 40 bits = 8 chars):
PSHUFBspreads the input into eight 16-bit lanes, each holding the big-endian window that contains one output char's 5 bits.- A per-lane unsigned high-multiply (
PMULHUW) by lane-specific powers of two shifts every char's 5-bit field down to bits[4:0];PAND 0x1fisolates it (0..31). - A second
PSHUFBpacks the eight values into the low 8 bytes, then a two-range ASCII map turns valuevinto its char —v + 65 - (PCMPGTB(v,25) & 41).
The AVX2 path processes two groups at once (one per 128-bit lane via
VINSERTI128 at src+0 / src+5), 2×-unrolled, with a VPERMQ gather and a
16-byte store per group-pair.
Performance¶
There is no pre-existing pure-Go SIMD base32 to compare against, so the
comparison is this package vs the stdlib scalar encoder. 1 MiB buffer, native
amd64 (AMD EPYC, GOAMD64=v1, median of 6):
| implementation | kind | MB/s | vs stdlib |
|---|---|---|---|
encoding/base32 (stdlib) |
scalar | ~1055 | 1.0× |
| this package (SSE2/SSSE3) | pure-Go SIMD | ~5430 | ~5.1× |
| this package (AVX2) | pure-Go SIMD | ~8285 | ~7.9× |
The speedup is below base64's because base32's 5-bit grouping forces an 8-byte store per 5-byte group (vs base64's 16-byte store per 12 bytes) and a longer serial extract chain — inherent to the format.
- ppc64le (VSX) runs the full kernel:
LXVB16Xbyte-order-correct load →VPERMspread → the per-char 5-bit field is isolated with POWER's per-lane variable right shiftVSRH(field >> p— exactly what amd64's multiply-high-by-2^(16-p)computes) andVAND 0x1f, thenVPERMpack and a two-range ASCII map (VCMPGTUB/VADDUBM/VSUBUBM),STXVB16Xstore. VSX is baseline on POWER8+, so there is no runtime dispatch. - s390x (vector facility, big-endian) is the one shipped non-amd64 arch with
a genuine vector integer multiply-high (
VMLHH), so it reproduces the amd64PMULHUWstep almost instruction-for-instruction:VL→VPERMspread →VMLHHby2^(16-p)→VPERMpack →VNmask →VCHLB/VAB/VSBASCII map →VST. TheVPERMcontrol vectors use big-endian lane order (lane 0 = lowest address), which matches amd64's big-endian 16-bit windows — so the output is byte-identical with no endianness fix-up. VSRH+VMLHHare exactly the two primitives arm64 lacked on stable Go. The per-char 5-bit fields need per-lane variable shifts and an integer vector multiply; the Go arm64 assembler exposed neither until Go 1.27 (VUMULL, register-formVUSHL). POWER and IBM Z always provided them, so ppc64le and s390x run real SIMD where arm64 stable cannot; on Go 1.27+ arm64 gets a full NEON encode kernel too (~2.1× stdlib on native arm64). loong64 / riscv64 still fall back toencoding/base32.- decode is now SIMD on amd64, ppc64le and s390x (scalar on arm64 /
loong64 / riscv64), with RFC 4648 error semantics — including
CorruptInputErroroffsets — kept identical to stdlib. ppc64le decode is the ~5.5× result measured on real POWER9 above.
Coverage¶
100% of the Go code (native amd64 + native arm64; the !amd64 fallback measured
on arm64; ppc64le and s390x under QEMU). The .s kernels are validated by
differential tests against encoding/base32 plus fuzzing — on real AVX2 for
amd64, and under qemu-user (power9 / s390x) for the VSX and big-endian
vector-facility kernels, where the output is proven byte-identical to stdlib.
BSD-3-Clause.