Skip to content

base64

CI coverage

A drop-in fast path for standard base64 (StdEncoding, padded), byte- and error-identical to encoding/base64. Both encode and decode run a SIMD kernel generated by go-asmgen; the short tail + padding (and, on decode, any block with an invalid byte) reuse the standard library, so output and every CorruptInputError offset match exactly. Repository →

API

s := base64.EncodeToString(data)   // same bytes as encoding/base64.StdEncoding
b, err := base64.DecodeString(s)   // same bytes + same error/offset as stdlib
op amd64 arm64 ppc64le s390x loong64 / riscv64
encode AVX2 + SSE2 (Lemire) NEON (VLD3/VST4 deinterleaving I/O) VSX (shift-based) vector facility (shift-based) scalar (stdlib)
decode AVX2 + SSE (Muła) NEON (shift-based) VSX (shift-based) vector facility (shift-based) scalar (stdlib)

Algorithm

The encoder is Lemire's vectorised base64: a shuffle spreads the input across 24-bit lanes, two multiplies pull out the 6-bit indices, and a PSHUFB / TBL offset-LUT maps each to its ASCII byte (constants via go-asmgen's emit.File.Data). The amd64 path uses the multiply trick; arm64, ppc64le and s390x use a shift-based variant (VSRW/VESRLF index extraction), since those ISAs lack the integer vector multiply amd64 relies on. All six 64-bit targets are covered: AVX2/SSE2 on amd64, NEON on arm64, VSX on ppc64le, the vector facility on s390x (big-endian), and a scalar fallback on loong64/riscv64. On s390x the VPERM control vectors use big-endian lane numbering (lane 0 = lowest address) and were verified to produce byte-identical output. arm64 uses the aklomp/emmansun deinterleaving-I/O design (VLD3.P / VST4.P), which needs no integer vector multiply and so builds on released Go. Decode is now SIMD too (amd64 Muła AVX2/SSE; NEON/VSX/vector-facility on arm64/ppc64le/s390x), with the CorruptInputError offset of any invalid block matching encoding/base64 exactly.

Performance

Encode throughput, 1 MiB buffer, native amd64 (GitHub Actions, AMD EPYC 7763, GOAMD64=v1, -count=6, median MB/s — CI-measured because the dev box is arm64):

implementation kind MB/s vs stdlib
encoding/base64 (stdlib) scalar ~1180 1.0×
cristalhq/base64 pure-Go scalar ~2650 2.2×
emmansun/base64 pure-Go SIMD (AVX2) ~19300 ~16×
this package pure-Go SIMD (AVX2) ~20500 ~17×

This package leads emmansun/base64 by ~5–6% (confirmed across CI reruns) — notable, since emmansun is the mature reference. The edge came from a cycle-model-guided optimization: llvm-mca showed the kernel was shuffle-port (Zn3FP1) bound; disassembling emmansun revealed a -4-offset 32-byte load that spreads with a single VPSHUFB (no cross-lane VINSERTI128). Combining that load with this package's 2× unroll was predicted at 2.15 cyc/block (vs emmansun's 2.20) and the benchmark confirmed it. (Block 0 keeps VINSERTI128 — a -4 load there would read before src.)

  • arm64: NEON VLD3/VST4 deinterleaving-I/O encode kernel on released Go — ~8× stdlib, ties emmansun/base64 (~22 GB/s); decode is a NEON shift-based kernel.
  • ppc64le: VSX SIMD encode and decode, natively measured on real POWER9 (GCC Compile Farm, Go 1.26.4, 2026-06-26): encode ~2.1× stdlib (1613 vs 782 MB/s), decode ~2.0× (2090 vs 1057 MB/s; decode also beats emmansun).
  • s390x: vector-facility SIMD encode+decode kernels, natively measured on real IBM z15 (VXE2) (2026-07-03, -count=6): ~3.7–4.2× stdlib on buffers ≥1 KiB; the scalar fallback wins at ≤16-byte small inputs (dispatch
  • non-vectorised tail dominate there).
  • decode is now SIMD on amd64/arm64/ppc64le/s390x — beats emmansun decode on amd64 (~1.3×); scalar on loong64/riscv64.
  • cgo wrappers of aklomp/base64 are faster still but need a C toolchain — excluded from this pure-Go comparison.

Coverage

100% of the Go code on every arch job (native amd64 + native arm64, plus QEMU jobs for ppc64le, s390x and riscv64). The generated .s kernels are validated by differential tests against encoding/base64 plus FuzzEncode — on a real AVX2 box for amd64/arm64, and under qemu-user for the VSX and big-endian vector-facility kernels. BSD-3-Clause.