ascii85¶
A drop-in fast path for the Adobe/btoa ascii85 (base-85) encoding, byte-
and error-identical to the standard library's
encoding/ascii85. This is the first
SIMD ascii85. The bulk of both directions (the regular 4-byte ↔ 5-char path)
runs a SIMD kernel generated by go-asmgen;
encode's all-zero z shortcut and short trailing fragment, and decode's
whitespace, z, short/invalid groups, flush and CorruptInputError offsets,
reuse the standard library, so the output, errors and offsets match exactly.
Repository →
API¶
The API mirrors the standard library 1:1, so it is a literal drop-in:
import "github.com/go-simd/ascii85"
n := ascii85.Encode(dst, src) // identical bytes to encoding/ascii85.Encode
m := ascii85.MaxEncodedLen(len(src)) // sizing
nd, ns, err := ascii85.Decode(dst, src, true) // identical (ndst, nsrc, err), incl. CorruptInputError offsets
Encode, Decode, MaxEncodedLen, and a re-exported CorruptInputError type.
Algorithm¶
The hard part of base-85 is the repeated division by 85. The kernel avoids real division with a reciprocal multiply that is exact over the entire 32-bit range (verified exhaustively over all 2³² values):
Five such steps yield the five base-85 digits of each 4-byte group (most
significant first), '!' (33) is added, and a fixed cross-lane shuffle scatters
the per-lane digits into the stride-5 output layout (16 input bytes → 20 output
chars per 128-bit block). The wrapper hands the kernel only maximal runs of
groups that contain no all-zero group, so every kernel group expands to
exactly 5 chars; an all-zero group (which the spec shortens to z) and the
trailing fragment go to encoding/ascii85, keeping the output byte-identical.
Per-arch kernels¶
| op | amd64 | ppc64le | s390x | riscv64 | loong64 | arm64 (go1.27+) |
|---|---|---|---|---|---|---|
| encode | SSE (PMULULQ) | VSX (VMULEUW/OUW) | vector (VMLHF) | RVV (VMULHUVV) | LSX (VMUHWU) | NEON (VUMULL/VUMULL2) |
| decode | SSE (PMULLD) | VSX (VMULUWM) | vector (VMLF) | RVV (VMULVX) | LSX (VMULW) | NEON (VMUL) |
All six 64-bit targets ship a real SIMD kernel in both directions — but the
arm64 kernel needs Go 1.27+. The base-85 encode /85 reciprocal-divide needs
a 32-bit vector mulhi and the decode multiply-accumulate a 32-bit vector
multiply-low — both integer NEON multiplies (VUMULL/VUMULL2/VMUL) that
Go's arm64 assembler only gained upstream in Go 1.27 (the released toolchain
exposes only the polynomial VPMULL). The arm64 kernel is therefore guarded
//go:build arm64 && go1.27; on stable Go ≤ 1.26 arm64 falls back to a fused
scalar path — itself measurably faster than encoding/ascii85.
Encode's 32-bit mulhi (and decode's multiply-low MAC) is the only primitive that
differs across backends; the digit math, the +33, and the stride-4 ↔ stride-5
scatter/gather are otherwise identical. Notable encode backend specifics:
- ppc64le (VSX):
LXVB16Xbyte-order-correct load needs no byte reversal; mulhi viaVMULEUW/VMULOUWmerged withVMRGEW;VPERMscatter (gaps via an index into an all-zero operand — ppcVPERMhas noPSHUFB-style 0x80 zeroing). - s390x (vector facility, big-endian): group value is natively big-endian (no
reversal); single-op
VMLHFmulhi;VPERMscatter. - riscv64 (RVV):
VMULHUVVmulhi;VRGATHERVVfor both the per-lane byte reverse and the scatter (out-of-range index → 0 gives free gap zeroing). - loong64 (LSX):
VMUHWUmulhi;VSHUFBfor byte-reverse and scatter (index ≥ 16 selects the zero source → gap zeroing).
Every backend's operand orders and shuffle/lane semantics were confirmed
empirically under QEMU before the kernel was assembled, then verified against
encoding/ascii85 (table + FuzzEncode/FuzzDecode, byte- and error-identical).
Performance — honest¶
- arm64 (Apple Silicon, native, 1 MiB):
- Go 1.27+ NEON kernel (measured on gotip): ~1414 MB/s vs ~474 MB/s stdlib on encode (~3.0×) and ~1917 MB/s vs ~744 MB/s stdlib on decode (~2.6×).
- Stable Go fused-scalar fallback: still ~1.9× encode / ~1.8× decode over stdlib.
- amd64: SSE kernels correctness-validated (table + fuzz) on an x86_64 VM — but the only available host is QEMU-TCG, which makes SIMD disproportionately expensive, so its throughput is not representative. Native amd64 throughput is pending real silicon.
- ppc64le — measured on real POWER9 (GCC Compile Farm, VSX, Go 1.26.4,
2026-06-26): VSX encode ~508 vs ~177 MB/s stdlib — ~2.9× native (decode
~704 vs ~324 MB/s — ~2.2×). The VSX kernels use POWER9
LXVB16X/STXVB16X, runtime-gated on POWER9 with a byte-identical scalar fallback on POWER8 (verified no-SIGILL on real POWER8E, cfarm112). - riscv64 — measured on a real SpacemiT X60 (RVV 1.0) (GCC Compile Farm, Go 1.26.4, 2026-06-26, post RVV-gather-gap fix): RVV encode ~134 vs ~33 MB/s stdlib — ~4.0× native (decode ~121 vs ~69 MB/s — ~1.8×). The X60 is a low-power in-order RVV core, so an out-of-order part would likely do better.
- loong64 — measured on a real Loongson 3A5000 (LSX) (GCC Compile Farm cfarm401, Go 1.26.4, 2026-06-26): LSX encode ~4.3× scalar native; correctness PASSES byte-, error- and offset-identical on real silicon.
- s390x — measured on real IBM z15 (VXE2) (native execution, 2026-07-03,
-count=6): the vector-facility kernels run encode ~6.4× scalar and decode ~3.7× scalar native; correctness PASSES byte-, error- and offset-identical (incl. big-endian) on real IBM Z silicon.
Seventh architecture: ppc64 (big-endian)¶
Beyond the six SIMD targets, the fused-scalar fallback is build- and
test-validated on ppc64 (big-endian) on real POWER9 silicon — byte-, error-
and offset-identical to encoding/ascii85 on a big-endian target distinct from
s390x's vector kernel. Framing: six SIMD targets, validated on seven
architectures.
Coverage¶
100% of the Go code on every arch job: native amd64 + native arm64 (stable
Go = fused-scalar fallback), a separate gotip / Go 1.27 arm64 job covering the
NEON kernel, plus jobs for ppc64le / s390x / riscv64 / loong64 (ppc64le, riscv64
and loong64 additionally exercised natively on real POWER9 / SpacemiT X60 /
Loongson 3A5000 silicon; s390x under QEMU). The generated .s kernels are
validated by differential tests against the scalar encoding/ascii85 reference
plus FuzzEncode/FuzzDecode on every arch that ships a kernel. BSD-3-Clause.