Skip to content

ascii

ascii

CI coverage

SIMD ASCII case folding — ToUpper, ToLower, and a case-insensitive EqualFold — byte- and result-identical to the standard library on every input, with a vectorised inner loop on all six 64-bit Go targets (assembly generated by go-asmgen). Plain go build, no cgo, no GOEXPERIMENT. Repository →

API

import "github.com/go-simd/ascii"

ascii.ToUpper([]byte("Hello, World!"))      // []byte("HELLO, WORLD!")
ascii.ToLower([]byte("Hello, World!"))      // []byte("hello, world!")
ascii.ToUpperString("Hello")                // "HELLO"
ascii.ToLowerString("Hello")                // "hello"
ascii.EqualFold([]byte("Go"), []byte("GO")) // true
functionidentical to
ToUpper(b []byte) []bytebytes.ToUpper
ToLower(b []byte) []bytebytes.ToLower
ToUpperString(s string) stringstrings.ToUpper
ToLowerString(s string) stringstrings.ToLower
EqualFold(a, b []byte) boolbytes.EqualFold (ASCII)

Always identical to the standard library

The SIMD kernels handle the ASCII range (0x00–0x7F), where case mapping is self-contained. Any input containing a byte ≥ 0x80 is not ASCII — the standard library applies full Unicode case mapping there — so such inputs are handed in full to bytes/strings/unicode and the SIMD path is skipped. The result is therefore byte-identical to the standard library for every input: pure-ASCII goes the fast way, non-ASCII matches the Unicode reference exactly. This is checked by table tests over every ASCII byte and by FuzzToUpper, FuzzToLower, FuzzEqualFold differentially against the standard library on all input — including non-ASCII — on every architecture.

Algorithm (compare + conditional add, no multiply)

ToLower adds 0x20 to each byte in A–Z; ToUpper subtracts 0x20 from each byte in a–z. The per-byte delta is computed without a table and without a multiply, so it maps to a real SIMD kernel on every target — including arm64, where the case-mapping imul trick that some libraries use has no NEON equivalent. Where a target exposes unsigned vector compares the range mask is a compare + AND; where it does not (notably arm64), the same predicate is synthesised from a per-byte subtraction’s sign bit (every ASCII byte is < 0x80, so (b - 'A')’s top bit is exactly b < 'A'):

lo  = (b - 'A')     & 0x80      // 0x80 iff b <  'A'
hi  = (b - ('Z'+1)) & 0x80      // 0x80 iff b <= 'Z'
inr = hi & (lo ^ 0x80)          // 0x80 iff 'A' <= b <= 'Z'
out = b + (inr >> 2)            // 0x80 >> 2 == 0x20

EqualFold folds both inputs the same way, XORs the folded blocks, and reduces the differences — zero means equal.

Per-arch kernels

archISArange maskapply
amd64SSE2 + AVX2PCMPGTB ×2 + PANDPADDB/PSUBB; AVX2 (32-byte) auto-selected at runtime
arm64NEONsign-bit (VSUB,VAND,VEOR,VUSHR)VADD/VSUB
loong64LSXsign-bit (VSUBB,VANDV,VXORV,VSRLB)VADDB/VSUBB
riscv64RVVVMSGTUVX + VMSLEUVX + VMANDMMmasked VADDVX/VSUBVX
ppc64leVSXsign-bit (VSUBUBM,VAND,VXOR,VSRB)VADDUBM/VSUBUBM
s390xvector facility (big-endian)sign-bit (VSB,VN,VX,VESRLB)VAB/VSB

amd64 picks the AVX2 (32-byte) kernel at runtime when the CPU supports it, else SSE2 (16-byte). VSX is baseline on POWER8+ and the vector facility on z13+, so ppc64le, s390x, riscv64 and loong64 need no runtime feature dispatch. Every kernel is per-byte, so s390x’s big-endian lane order is transparent — a VL/transform/VST round-trips byte-for-byte; the position-sensitive fuzz and table tests are the gate.

Performance — honest

SIMD vs the standard library over a 4 KiB ASCII buffer (-count=3, medians):

archhostopspeedup
arm64Apple Silicon (native)ToUpper~4.9×
arm64Apple Silicon (native)ToLower~4.8×
arm64Apple Silicon (native)EqualFold~2.0×
amd64x86-64 QEMU VM (ratio valid)ToUpper~4.0×
amd64x86-64 QEMU VM (ratio valid)EqualFold~2.7×

The amd64 figures come from a QEMU/TCG VM, so the absolute throughput is artificially low (no native silicon was available); only the SIMD-vs-stdlib ratio is meaningful there. ppc64le: the kernel is QEMU-validated for correctness; native perf pending real POWER hardware. s390x: measured on real IBM z15 (VXE2), 2026-07-03, -count=6 — ToUpper/ToLower ~10× stdlib, EqualFold ~1.6×.

Coverage

100% statement coverage, gated in CI on every architecture (native amd64 + arm64, and riscv64 + loong64 + ppc64le + s390x under QEMU); the build fails below 100%. Both amd64 dispatch branches (AVX2 and the SSE2 fallback) are exercised on the native amd64 runner by toggling the feature flag. The generated .s kernels are validated by differential table tests and fuzzing against the bytes/strings/unicode reference. BSD-3-Clause.