Skip to content

ascii85

CI coverage

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

v / 85  ==  (v * 0xC0C0C0C1) >> 32 >> 6        remainder = v - (v/85)*85

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): LXVB16X byte-order-correct load needs no byte reversal; mulhi via VMULEUW/VMULOUW merged with VMRGEW; VPERM scatter (gaps via an index into an all-zero operand — ppc VPERM has no PSHUFB-style 0x80 zeroing).
  • s390x (vector facility, big-endian): group value is natively big-endian (no reversal); single-op VMLHF mulhi; VPERM scatter.
  • riscv64 (RVV): VMULHUVV mulhi; VRGATHERVV for both the per-lane byte reverse and the scatter (out-of-range index → 0 gives free gap zeroing).
  • loong64 (LSX): VMUHWU mulhi; VSHUFB for 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.
  • ppc64lemeasured 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).
  • riscv64measured 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.
  • loong64measured 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.
  • s390xmeasured 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.