Skip to content

base32

base32

CI coverage

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
opamd64ppc64les390xarm64loong64 / riscv64
encodeAVX2 + SSE2VSXvector facilityNEONscalar (stdlib)
decodeAVX2 + SSE2VSXvector facilityNEONscalar (stdlib)

ppc64le, s390x and 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) only reached the Go assembler in Go 1.27; the module’s floor is now 1.27.1, so every arm64 build gets the NEON kernels. Decode on arm64 (NEON, 2026-10-07) reaches ~10.2 GB/s at 1 MiB, ~25× the stdlib on an Apple M4 Max. 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). The arm64 NEON kernels run on native arm64 (encode ~2.1× the stdlib encoder, decode up to ~25×); 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):

  1. PSHUFB spreads the input into eight 16-bit lanes, each holding the big-endian window that contains one output char’s 5 bits.
  2. 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 0x1f isolates it (0..31).
  3. A second PSHUFB packs the eight values into the low 8 bytes, then a two-range ASCII map turns value v into 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):

implementationkindMB/svs stdlib
encoding/base32 (stdlib)scalar~10551.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: LXVB16X byte-order-correct load → VPERM spread → the per-char 5-bit field is isolated with POWER’s per-lane variable right shift VSRH (field >> p — exactly what amd64’s multiply-high-by-2^(16-p) computes) and VAND 0x1f, then VPERM pack and a two-range ASCII map (VCMPGTUB/VADDUBM/VSUBUBM), STXVB16X store. 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 amd64 PMULHUW step almost instruction-for-instruction: VL → VPERM spread → VMLHH by 2^(16-p) → VPERM pack → VN mask → VCHLB/VAB/VSB ASCII map → VST. The VPERM control 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 + VMLHH are exactly the two primitives arm64 lacked before Go 1.27. 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-form VUSHL). POWER and IBM Z always provided them; with the Go floor at 1.27.1, arm64 now runs full NEON encode (~2.1× stdlib) and decode (up to ~25×) kernels too. loong64 / riscv64 still fall back to encoding/base32.
  • decode is SIMD on amd64, ppc64le, s390x and arm64 (scalar on loong64 / riscv64), with RFC 4648 error semantics — including CorruptInputError offsets — 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.