Skip to content

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.

op amd64 ppc64le s390x arm64 / loong64 / riscv64
encode SSE2/SSSE3 + AVX2 (runtime dispatch) VSX (VPERM table lookup) vector facility (VPERM, big-endian) scalar (stdlib) — NEON/LSX/RVV planned
decode SSE2/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):

implementation kind encode MB/s vs stdlib decode MB/s vs stdlib
encoding/hex (stdlib) scalar 980 1.00× 2095 1.00×
this package SIMD encode and decode 20023 20.4× 13078 6.24×
tmthrgd/go-hex SIMD, archived Sep 2025 18785 19.2× 9566 4.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.