Skip to content

ascii

CI coverage

SIMD ASCII case foldingToUpper, ToLower, and a case-insensitive EqualFoldbyte- 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
function identical to
ToUpper(b []byte) []byte bytes.ToUpper
ToLower(b []byte) []byte bytes.ToLower
ToUpperString(s string) string strings.ToUpper
ToLowerString(s string) string strings.ToLower
EqualFold(a, b []byte) bool bytes.EqualFold (ASCII)

Always identical to the standard library

The SIMD kernels handle the ASCII range (0x000x7F), 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 AZ; ToUpper subtracts 0x20 from each byte in az. 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

arch ISA range mask apply
amd64 SSE2 + AVX2 PCMPGTB ×2 + PAND PADDB/PSUBB; AVX2 (32-byte) auto-selected at runtime
arm64 NEON sign-bit (VSUB,VAND,VEOR,VUSHR) VADD/VSUB
loong64 LSX sign-bit (VSUBB,VANDV,VXORV,VSRLB) VADDB/VSUBB
riscv64 RVV VMSGTUVX + VMSLEUVX + VMANDMM masked VADDVX/VSUBVX
ppc64le VSX sign-bit (VSUBUBM,VAND,VXOR,VSRB) VADDUBM/VSUBUBM
s390x vector 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):

arch host op speedup
arm64 Apple Silicon (native) ToUpper ~4.9×
arm64 Apple Silicon (native) ToLower ~4.8×
arm64 Apple Silicon (native) EqualFold ~2.0×
amd64 x86-64 QEMU VM (ratio valid) ToUpper ~4.0×
amd64 x86-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=6ToUpper/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.