Skip to content

utf8

CI coverage

A drop-in fast path for UTF-8 validation and rune counting, byte-identical to unicode/utf8. The bulk runs a SIMD kernel generated by go-asmgen (amd64 SSE/SSSE3 and AVX2); the short tail and the rune straddling the last SIMD block reuse the standard library, so results match exactly. Repository →

API

ok := utf8.Valid(p)              // same bool as unicode/utf8.Valid
ok := utf8.ValidString(s)        // same bool as unicode/utf8.ValidString
n  := utf8.RuneCount(p)          // same int  as unicode/utf8.RuneCount
n  := utf8.RuneCountInString(s)  // same int  as unicode/utf8.RuneCountInString
arch kernel
amd64 SSE2/SSSE3 + SSE4.1 (16 B/block) and AVX2 (32 B/block), runtime-dispatched
ppc64le VSX/AltiVec (16 B/block, POWER8 baseline) — qemu-validated; native perf pending
s390x vector facility (16 B/block, z13 baseline, big-endian) — qemu-validated; native perf pending
arm64 / loong64 / riscv64 scalar (unicode/utf8) — NEON/LSX/RVV planned

The ppc64le and s390x kernels are 1:1 ports of the amd64 SSE path (no runtime dispatch, since VSX and the vector facility are baseline on POWER8+ and z13+): the PSHUFB nibble lookups become VPERM, the range compares VCMPGTSB/VCHB, and the rune-count popcount becomes VPOPCNTD (ppc64le) / VPOPCT+VSUMB (s390x). On ppc64le the source is loaded with LXVB16X; on big-endian s390x, VL already places the lowest memory address in lane 0, so the kernel is byte-identical to stdlib with no endianness fix-up.

Algorithm

Valid

Lemire & Keiser's Validating UTF-8 In Less Than One Instruction Per Byte: each byte is classified by its high nibble via PSHUFB nibble-lookups, then over the (prev, curr) byte pair (PALIGNR / VPERM2I128+VPALIGNR) a handful of checks run — continuation-length carry, the ED/F4 first-continuation maxima, the overlong-encoding minima, and byte > 0xF4 — each ORing into a running error accumulator. Blocks are valid iff the accumulator is all-zero (PTEST). Ported 1:1 from Lemire's reference intrinsics. The caller backs the SIMD/scalar split up to a rune boundary so a sequence straddling the last block is re-validated by unicode/utf8.

RuneCount

The number of runes in valid UTF-8 equals the number of non-continuation bytes — every rune has exactly one leading byte with (b & 0xC0) != 0x80. The kernel marks continuation bytes per block (PAND 0xC0 then PCMPEQB 0x80), extracts a bitmask (PMOVMSKB) and POPCNTs it; the count is blockBytes − continuations. That identity holds only for valid UTF-8, so the kernel is trusted only over a prefix the Valid validator confirms; if the prefix is invalid the whole input falls back to the scalar decoder, making the result byte-identical to unicode/utf8.RuneCount on all input. RuneCountInString defers to the stdlib to avoid an allocating []byte copy.

Performance

Valid throughput, ~1 MiB mixed-UTF-8 buffer, native amd64 (AMD EPYC 7763, AVX2, GOAMD64=v1, median of 6):

implementation kind MB/s vs stdlib
unicode/utf8.Valid (stdlib) scalar 312 1.00×
this package (Valid) pure-Go SIMD (AVX2) 5940 19.07×
stuartcarnie/go-simd pure-Go SIMD (SSE4/AVX2) 5738 18.42×

This package edges the competitor by ~3.5% and beats stdlib ~19× on this AVX2 runner. Both implement the same Lemire reference; the margin comes from this package's tighter go-asmgen-emitted loop and rune-boundary tail split. On a local AVX2 VM RuneCount measured ~4.6× stdlib; the native-runner CI fills the headline.

charlievieth/simdutf is a cgo wrapper around the C++ simdutf library — excluded from this pure-Go comparison.

Coverage

100% of the Go code (native amd64 drives the AVX2/SSE/POPCNT branches; native arm64 covers the generic fallback; ppc64le and s390x run under QEMU where VSX / the vector facility are baseline). The .s kernels are validated by differential tests against unicode/utf8 plus fuzzing — on a real AVX2 box for amd64 (~75 M executions, zero mismatches), and under qemu-user for the VSX and big-endian vector-facility kernels. BSD-3-Clause; the kernel derives from Lemire's reference (Apache-2.0).