Skip to content

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
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):

  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):

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: 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: VLVPERM 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 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-form VUSHL). 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 to encoding/base32.
  • decode is now SIMD on amd64, ppc64le and s390x (scalar on arm64 / 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.