Skip to content

hex

hex

CI coverage

A drop-in fast path for hexadecimal (base16) encoding and decoding, byte- and error-identical to encoding/hex. Both directions run a SIMD kernel generated by go-asmgen; the short tail reuses an encoding/hex-equivalent scalar loop. Pure Go, CGO_ENABLED=0, stable Go. Repository →

API

s := hex.EncodeToString(data)   // same bytes as encoding/hex.EncodeToString
b, err := hex.DecodeString(s)   // same bytes + same errors as encoding/hex

Mirrors encoding/hex: Encode, EncodeToString, Decode, DecodeString, EncodedLen, DecodedLen.

opamd64ppc64les390xarm64 / loong64 / riscv64
encodeSSE2/SSSE3 + AVX2 (runtime dispatch)VSX (VPERM table lookup)vector facility (VPERM, big-endian)scalar (stdlib) — NEON/LSX/RVV planned
decodeSSE2/SSSE3 + AVX2 (runtime dispatch)VSX (VCHLB + VPERM)vector facility (VCHLB + VPERM, big-endian)scalar (stdlib) — NEON/LSX/RVV planned

Both directions have SIMD on amd64, ppc64le (VSX) and s390x (vector facility); the rest fall back to the encoding/hex scalar loop. ppc64le uses LXVB16X/STXVB16X (natural memory byte order on little-endian, sidestepping the LXVD2X doubleword swap), minding the VSX↔VMX Vn == VS(32+n) aliasing. s390x is big-endian: VL puts the first memory byte in the high-order lane. The ppc64le and s390x kernels are qemu-validated for correctness (the InvalidByteError offset and ErrLength semantics are byte- and error-identical there too). s390x is measured on real IBM z15 (VXE2), 2026-07-03, -count=6: encode ~18×, decode ~3.1× the encoding/hex scalar path. ppc64le native perf is pending a real POWER system.

Algorithm

Encode — per 16 input bytes (32 for AVX2): split each byte into high/low nibble (PSRLW $4+PAND 0x0f, PAND 0x0f), map each nibble via a PSHUFB lookup of "0123456789abcdef", interleave the two nibble streams with PUNPCKLBW/PUNPCKHBW, store. AVX2 doubles the width and recombines lanes with VPERM2I128.

Decode — per 32 input chars (64 for AVX2): map each ASCII hex char to its 0..15 value while detecting invalid chars in parallel. Validity is three range checks (['0','9'], ['A','F'], ['a','f']) via PCMPGTB pairs OR’d together. PMADDUBSW with a {16,1,…} multiplier fuses each (hi,lo) nibble pair into a byte; PACKUSWB packs the output. If a block contains any invalid char the kernel stops and a scalar encoding/hex-equivalent loop resumes — producing the exact InvalidByteError offset and ErrLength. So Decode/DecodeString are byte- and error-identical to the standard library.

Performance

1 MiB random buffer, native amd64 (AMD EPYC, AVX2, GOAMD64=v1, median of 6):

implementationkindencode MB/svs stdlibdecode MB/svs stdlib
encoding/hex (stdlib)scalar9801.00×20951.00×
this packageSIMD encode and decode2002320.4×130786.24×
tmthrgd/go-hexSIMD, archived Sep 20251878519.2×95664.57×

On decode this package is 1.37× faster than tmthrgd/go-hex and 6.24× over stdlib. Forced SSE-only paths are ~16800 MB/s encode and ~6329 MB/s decode.

Go’s standard encoding/hex is scalar; see golang/go#68188, the open proposal to add SIMD to the standard library.

Coverage

100% of the Go code (native amd64 + native arm64, plus QEMU-emulated ppc64le and s390x jobs). The .s kernels are validated by differential tests against encoding/hex — including invalid bytes at every offset — plus fuzzing (on real AVX2 for amd64; under qemu-user for the VSX and big-endian vector-facility kernels). BSD-3-Clause.