hex

package module
v0.0.0-...-37129ad Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Jul 3, 2026 License: BSD-3-Clause Imports: 2 Imported by: 0

README

go-simd/hex

hex

CI coverage

A drop-in fast path for hexadecimal (base16) encoding and decoding, byte- and error-identical to the standard library's encoding/hex. Both directions run a SIMD kernel generated by go-asmgen; the short tail reuses an encoding/hex-equivalent scalar loop, so output and errors match exactly. Pure Go, CGO_ENABLED=0, stable Go, no GOEXPERIMENT.

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

API 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

Six architectures are wired into go-asmgen; three of them now have hand-tuned SIMD hex kernels (amd64, ppc64le VSX, s390x vector facility), the rest fall back to the encoding/hex scalar loop. The ppc64le and s390x kernels are QEMU-validated (full table + fuzz, byte- and error-identical to encoding/hex); s390x is additionally measured on real IBM z15 (VXE2), 2026-07-03, -count=6: encode ~18×, decode ~3.1× the encoding/hex scalar path. ppc64le native throughput is pending real POWER hardware.

How encode works

Per 16 input bytes (SSE; 32 for AVX2): split each byte into its high and low nibble (PSRLW $4 + PAND 0x0f for the high nibble, PAND 0x0f for the low), map each nibble 0..15 to its ASCII digit via a PSHUFB lookup of the table "0123456789abcdef", then interleave the two nibble streams with PUNPCKLBW/PUNPCKHBW so each input byte's (hi, lo) characters land adjacent, and store. The AVX2 path doubles the width and recombines the two 128-bit lanes with VPERM2I128. Constants are emitted via go-asmgen's emit.File.Data. Verified against encoding/hex (table + fuzz, native arm64 + amd64 under Rosetta).

How decode works

Per 32 input chars (SSE; 64 for AVX2) the decoder maps each ASCII hex char to its 0..15 nibble value and, in parallel, detects any invalid char:

  • Validity — a char is valid iff it lies in ['0','9'] OR ['A','F'] OR ['a','f']. Each range is two PCMPGTB (signed; all hex chars are < 0x80 so the sign bit is irrelevant) AND'd together; the three range masks are OR'd into a per-char "valid" mask.
  • Nibble — digit → c-'0', upper → c-('A'-10), lower → c-('a'-10); each subtraction is masked by its range mask and OR'd, so every valid char yields its 0..15 nibble.
  • FusePMADDUBSW with a {16,1,16,1,…} multiplier turns each adjacent (hi,lo) nibble pair into one byte hi*16+lo == (hi<<4)|lo (no overflow, each nibble < 16); PACKUSWB packs the two halves into the output bytes. The AVX2 path fixes the per-lane PACKUSWB/PMADDUBSW interleave with a single VPERMQ $0xD8.

The kernel never tries to pin-point an invalid byte in SIMD. The per-char "valid" masks are inverted and accumulated; if a 32-char (or 64-char) block contains any invalid char the kernel stops and returns the count of fully decoded blocks, and a scalar encoding/hex-equivalent loop resumes from there. That loop produces the exact InvalidByteError offset and ErrLength (odd input decodes floor(n/2) bytes before erroring) — so Decode / DecodeString are byte- and error-identical to the standard library. Verified against encoding/hex (table + fuzz: invalid bytes at every offset in both nibble positions, across block boundaries; native arm64 scalar + amd64 SSE under Rosetta; AVX2 force-tested on native CI hardware).

ppc64le (VSX) and s390x (vector facility)

Both ports map the PSHUFB/PMADDUBSW amd64 logic onto vector permute:

  • Encode — load 16 source bytes, split into nibbles (VAND 0x0f for the low nibble; VSRB/VESRLB $4 for the high), map each nibble to its ASCII digit with a single VPERM of the 16-entry table "0123456789abcdef", then interleave the hi/lo ASCII streams with two VPERM passes driven by control vectors so each input byte's (hi, lo) characters land adjacent, and store.
  • Decode — load two 16-char halves, range-check each char with unsigned compares (VCMPGTUB on ppc64le, VCHLB on s390x) for ['0','9']/['A','F']/ ['a','f'], derive the nibble by masked subtraction, accumulate a "bad" mask and bail to the scalar tail on any invalid char (so the InvalidByteError offset stays bit-exact), then VPERM-gather the even/odd nibbles, hi<<4 | lo (VSLB/VESLB $4 + VOR), and store.

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 into lane 0 (the leftmost/high-order lane), so the per-byte high-nibble- first ordering is fixed by the VPERM control vectors, not by endianness — a position-dependent test (0x12 0x34 → "1234") guards it. Both ports are validated under QEMU with the full differential table test and coverage-guided FuzzEncode/FuzzDecode (byte- and error-identical to encoding/hex); see the qemu CI job.

Performance

Encode/decode throughput on a 1 MiB random buffer, native amd64 (GitHub Actions ubuntu-latest), -count=6, median MB/s — see .github/workflows/bench.yml. The dev box is arm64, where this package's amd64 kernel only runs under Rosetta (unrepresentative), so these numbers come from CI on native hardware (GitHub Actions ubuntu-latest, AMD EPYC, AVX2-capable, 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 pure-Go SIMD (SSE2/SSSE3 + AVX2), encode and decode 20023 20.4× 13078 6.24×
tmthrgd/go-hex pure-Go SIMD (SSE/AVX), archived 18785 19.2× 9566 4.57×

On decode this package is 1.37× faster than tmthrgd/go-hex and 6.24× over stdlib. The forced SSE-only paths are ~16800 MB/s encode and ~6329 MB/s decode; the AVX2 dispatch above is the default on AVX2 hardware (~13063 MB/s decode).

Re-bench ratios (as of 2026-06-14, -count=6 medians, kernels unchanged). amd64 measured on the local x86_64 QEMU VM (absolute MB/s run low; ratios are the signal); arm64 native on Apple Silicon:

arch op ours vs stdlib ours vs tmthrgd
amd64 encode ~2.96× ~1.05× (ours edges)
amd64 decode ~2.29× ~1.23× (ours wins)
arm64 encode ~1.0× (no SIMD kernel — generic fallback) ~1.0×
arm64 decode ~1.0× (no SIMD kernel) ~9.7× (tmthrgd's arm64 decode is slow scalar)

Verdict holds: on amd64 this package still edges tmthrgd/go-hex on encode and clearly leads on decode. arm64 has no SIMD kernel yet (NEON planned), so encode/decode track stdlib; the ~9.7× over tmthrgd on arm64 decode is because tmthrgd has no arm64 SIMD path either and its scalar decode is much slower than encoding/hex. Native-CI absolute numbers above are unchanged (gh logs unavailable in this env).

s390x — measured on real IBM z15; ppc64le — llvm-mca cycle-model estimate

s390x — measured on real IBM z15 (VXE2), native execution, 2026-07-03, -count=6: the vector-facility hex kernel runs encode ~18× and decode ~3.1× the encoding/hex scalar path — hex encode is the SIMD sweet spot (a branchless VPERM table lookup). This supersedes the s390x cycle-model estimate below (which projected ~16×).

For ppc64le the row below is static analysis, NOT a hardware measurement; native POWER perf pending real silicon. qemu's TCG is not cycle-accurate, so the cycle model is the only defensible signal. Numbers from llvm-mca (LLVM 22) fed the straight-line encode inner loop (encodeBlocksVSX / encodeBlocksVX, 16 input bytes → 32 hex bytes/iter) translated to LLVM asm; no data-dependent branch, so the model is the whole story. Scalar baseline: the per-input-byte two-nibble-LUT loop modeled the same.

arch cpu model SIMD cyc/iter SIMD input-B/cyc scalar input-B/cyc est. speedup
ppc64le pwr9 5.0 (16 B in) ~3.2 ~0.5 (1 B / 2.0 cyc) ~6.4×
s390x z14 (est.) 3.0 (16 B in) ~5.33 ~0.33 (1 B / 3.0 cyc) ~16× (measured z15: encode ~18×, decode ~3.1×)

Hex encode is the SIMD sweet spot: a pure VPERM table-lookup with no branches, so the VSX kernel is estimated ~6× (ppc64le) on the cycle model, and the s390x kernel — modeled at ~16× — now measures ~18× encode on real z15 (its z14 model retires the four VPERMs + load + two stores in just 3 cycles). Caveats: llvm-mca idealizes the frontend (perfect dispatch, no branch misprediction, no cache/store-buffer stalls), so these are compute upper bounds; the scalar baseline's nibble-LUT loads are assumed L1-resident. The decode kernels (with their VCHLB range validation) would model lower but were not isolated here — encode is the cleaner straight-line loop. All instructions were accepted and modeled by llvm-mca (no fallbacks).

Notes:

  • tmthrgd/go-hex is the prior pure-Go SIMD hex codec (hand-written Plan9 asm, SSE/AVX). It was archived upstream in September 2025. It also ships a SIMD decoder; this package now has its own SIMD decoder (SSE2/SSSE3 + AVX2) too, so encode and decode are both vectorised and competitive.
  • Go's standard encoding/hex is scalar (byte-by-byte); see golang/go#68188, the open proposal to add SIMD to the standard library.

Regenerating the assembly

The per-arch .s kernels are committed. To regenerate (go-asmgen is a build-time tool, not a runtime dependency):

go get github.com/go-asmgen/asmgen@v0.5.0
go run encode_gen.go            # amd64
go run decode_gen.go            # amd64
go run encode_ppc64le_gen.go    # ppc64le VSX
go run decode_ppc64le_gen.go    # ppc64le VSX
go run encode_s390x_gen.go      # s390x vector facility
go run decode_s390x_gen.go      # s390x vector facility
go mod edit -droprequire github.com/go-asmgen/asmgen
go mod edit -go=1.20
go mod tidy

Coverage

The CI gate enforces 100% coverage of the Go code on every arch job: native amd64 + native arm64, plus the QEMU-emulated ppc64le and s390x jobs (the generic fallback compiles and is measured on arm64; the VSX and vector-facility dispatch + kernels are measured under emulation). Coverage is of the Go statements only: the generated .s SIMD kernels are not measured by go test -cover — they are validated by differential tests against the scalar encoding/hex reference (including invalid bytes at every offset) plus fuzzing.

License

BSD-3-Clause.

Documentation

Overview

Package hex is a drop-in fast path for hexadecimal (base16) encoding and decoding, byte- AND error-identical to the standard library's encoding/hex. Both directions run a SIMD kernel generated by go-asmgen (amd64 SSE2/SSSE3, with an AVX2 path; other architectures fall back to the standard library): encoding vectorises the nibble->ASCII mapping; decoding vectorises the ASCII->nibble mapping with parallel validity detection. The short tail (and, for decode, any block containing an invalid char) reuses an encoding/hex-equivalent scalar loop so the output and the exact InvalidByteError / ErrLength errors land at the same offsets.

Index

Constants

This section is empty.

Variables

This section is empty.

Functions

func Decode

func Decode(dst, src []byte) (int, error)

Decode decodes src into DecodedLen(len(src)) bytes, returning the number of bytes written. It is byte- AND error-identical to encoding/hex.Decode: a SIMD kernel decodes whole valid blocks; a scalar tail (identical to stdlib's loop) finishes the remainder and, on the first invalid char or odd length, returns the exact InvalidByteError offset or ErrLength.

func DecodeString

func DecodeString(s string) ([]byte, error)

DecodeString returns the bytes represented by the hexadecimal string s, identical to encoding/hex.DecodeString.

func DecodedLen

func DecodedLen(n int) int

DecodedLen returns the length of a decoding of n hexadecimal bytes: n/2.

func Encode

func Encode(dst, src []byte) int

Encode encodes src into EncodedLen(len(src)) bytes of dst, writing lowercase hexadecimal. It returns the number of bytes written, len(src)*2. Output is identical to encoding/hex.Encode.

func EncodeToString

func EncodeToString(src []byte) string

EncodeToString returns the lowercase hexadecimal encoding of src, identical to encoding/hex.EncodeToString.

func EncodedLen

func EncodedLen(n int) int

EncodedLen returns the length of an encoding of n source bytes: exactly 2n.

Types

This section is empty.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL