ascii¶
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
| 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 (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¶
| 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=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.