Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
32 commits
Select commit Hold shift + click to select a range
2813baa
common/hexutil/simdhex: AVX2 lowercase hex encoder behind GOEXPERIMEN…
AskAlexSharov Sep 30, 2026
fc954d0
common/hexutil/simdhex: add table and SWAR scalar encoders to the bench
AskAlexSharov Sep 30, 2026
b42c834
common/hexutil: encode hex with an AVX2 kernel and a SWAR fallback
AskAlexSharov Sep 30, 2026
bb5b5fe
common/hexutil: drop the SWAR tail table, grow once in Bytes.AppendText
AskAlexSharov Sep 30, 2026
77bb958
common/hexutil: cover Bytes.AppendText in the encoder test
AskAlexSharov Sep 30, 2026
ea8efa1
common/hexutil: drop the SWAR fallback, build without simd uses hex.E…
AskAlexSharov Sep 30, 2026
efd5b36
common/hexutil: keep hex.Encode for U256, too short for the vector path
AskAlexSharov Sep 30, 2026
8c959d2
common/hexutil: AppendQuoted on top of AppendText
AskAlexSharov Sep 30, 2026
ad1aa43
common/hexutil: reslice in the AVX2 loop so it has no bounds checks
AskAlexSharov Sep 30, 2026
cc6feb4
ci: test the SIMD kernels with Go 1.27 and GOEXPERIMENT=simd
AskAlexSharov Sep 30, 2026
873fe90
common/hexutil: test the non-AVX2 fallback of the simd build
AskAlexSharov Sep 30, 2026
29f725f
Merge branch 'main' into alex/simdhex_37
AskAlexSharov Oct 2, 2026
49af067
common/hexutil: drop the no-AVX2 test and the stdlib bench arm
AskAlexSharov Oct 7, 2026
4468929
ci: run the SIMD hex test as a lint job step
AskAlexSharov Oct 7, 2026
3dba162
common/hexutil: hex decoding by simd
AskAlexSharov Oct 7, 2026
8fcedb7
common/hexutil: no AVX-512, close the control-byte hole, add NEON
AskAlexSharov Oct 7, 2026
d32cde6
common/hexutil: hex_amd64.go, hex_arm64.go, hex_generic.go
AskAlexSharov Oct 7, 2026
f7579da
common/hexutil: drop a no-op mask and a subsumed test loop
AskAlexSharov Oct 7, 2026
bb38cff
common/hexutil: name the AVX-512 instructions the ISA test rejects
AskAlexSharov Oct 7, 2026
6abaa99
ci: cover every architecture the SIMD hex kernels build for
AskAlexSharov Oct 7, 2026
9c3f46e
common/hexutil: reject EVEX encoding, not instruction names
AskAlexSharov Oct 7, 2026
f1f4f74
ci: run the arm64 hex test on a GitHub runner
AskAlexSharov Oct 7, 2026
02ceae2
common/hexutil: build the ISA test only with the kernel it audits
AskAlexSharov Oct 7, 2026
db8e41b
ci: scope actions: write to the lint job
AskAlexSharov Oct 7, 2026
a57bd6e
common/hexutil: range over int in the decode fuzz loops
AskAlexSharov Oct 7, 2026
e70175d
common/hexutil: test every digit pair, odd lengths and writes past n
AskAlexSharov Oct 8, 2026
a00d691
common/hexutil: arm64 hex encode by table lookup, decode 32 chars per…
AskAlexSharov Oct 8, 2026
731ff44
common/hexutil: amd64 hex encode two blocks per iteration, decode by …
AskAlexSharov Oct 8, 2026
5a32b3a
common/hexutil: hex kernels in assembly, fed at most 64 KiB per call
AskAlexSharov Oct 8, 2026
fe96ffd
common/hexutil: keep short MarshalText and Encode buffers on the stack
AskAlexSharov Oct 8, 2026
300a1f4
common/hexutil: pack decoded bytes with VPACKUSWB and VPERMQ
AskAlexSharov Oct 8, 2026
900b6bb
common/hexutil: arm64 decode with instructions Go 1.26 assembles
AskAlexSharov Oct 8, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 10 additions & 6 deletions common/hexutil/bytes.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,9 +17,9 @@
package hexutil

import (
"encoding/hex"
"encoding/json"
"reflect"
"slices"
)

var bytesT = reflect.TypeFor[Bytes]()
Expand All @@ -35,23 +35,27 @@ const HexPrefix = `0x`
func (b Bytes) MarshalText() ([]byte, error) {
result := make([]byte, len(b)*2+2)
copy(result, HexPrefix)
hex.Encode(result[2:], b)
encodeHex(result[2:], b)
return result, nil
}

// AppendText implements encoding.TextAppender: the alloc-free, byte-identical
// counterpart to MarshalText. Only encoding/json/v2 consults it today.
func (b Bytes) AppendText(dst []byte) ([]byte, error) {
dst = append(dst, HexPrefix...)
return hex.AppendEncode(dst, b), nil
n, size := len(dst), len(HexPrefix)+2*len(b)
dst = slices.Grow(dst, size)[:n+size]
dst[n], dst[n+1] = '0', 'x'
encodeHex(dst[n+2:], b)
return dst, nil
}

// QuotedLen is the length of n bytes encoded by AppendQuoted.
func QuotedLen(n int) int { return len(`"0x"`) + 2*n }

// AppendQuoted appends b as a 0x-prefixed hex JSON string.
func AppendQuoted(dst, b []byte) []byte {
return append(hex.AppendEncode(append(dst, `"`+HexPrefix...), b), '"')
dst, _ = Bytes(b).AppendText(append(dst, '"'))
return append(dst, '"')
}

// UnmarshalJSON implements json.Unmarshaler.
Expand All @@ -69,7 +73,7 @@ func (b *Bytes) UnmarshalText(input []byte) error {
return err
}
dec := make([]byte, len(raw)/2)
if _, err = hex.Decode(dec, raw); err != nil {
if _, err = decodeHex(dec, raw); err != nil {
err = mapError(err)
} else {
*b = dec
Expand Down
103 changes: 103 additions & 0 deletions common/hexutil/hex_amd64.s
Original file line number Diff line number Diff line change
@@ -0,0 +1,103 @@
// Copyright 2026 The Erigon Authors
// This file is part of Erigon.
//
// Erigon is free software: you can redistribute it and/or modify
// it under the terms of the GNU Lesser General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Erigon is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU Lesser General Public License for more details.
//
// You should have received a copy of the GNU Lesser General Public License
// along with Erigon. If not, see <http://www.gnu.org/licenses/>.

#include "textflag.h"

DATA hexDigits<>+0(SB)/8, $"01234567"
DATA hexDigits<>+8(SB)/8, $"89abcdef"
DATA hexDigits<>+16(SB)/8, $"01234567"
DATA hexDigits<>+24(SB)/8, $"89abcdef"
GLOBL hexDigits<>(SB), RODATA|NOPTR, $32

// 0xc6, 6, 0xf0, 0xdf, 'A', 10, 112, 0x0f
DATA decodeBytes<>+0(SB)/8, $0x0f700a41dff006c6
GLOBL decodeBytes<>(SB), RODATA|NOPTR, $8

DATA pairWeights<>+0(SB)/8, $0x0110011001100110
DATA pairWeights<>+8(SB)/8, $0x0110011001100110
DATA pairWeights<>+16(SB)/8, $0x0110011001100110
DATA pairWeights<>+24(SB)/8, $0x0110011001100110
GLOBL pairWeights<>(SB), RODATA|NOPTR, $32

// The 128-byte frames are unused. With a smaller frame the assembler drops the stack check of a
// leaf function, and that check is where a pending preemption stops the goroutine between the
// chunks of hex_asm.go.

// func encodeBlocks(dst, src *byte, blocks int)
TEXT ·encodeBlocks(SB), 0, $128-24
MOVQ dst+0(FP), DI
MOVQ src+8(FP), SI
MOVQ blocks+16(FP), CX
VMOVDQU hexDigits<>(SB), Y4
VPBROADCASTB decodeBytes<>+7(SB), Y5
VPMOVZXBW X5, Y5

loop:
VPMOVZXBW (SI), Y0
VPSRLW $4, Y0, Y1
VPAND Y5, Y0, Y0
VPSLLW $8, Y0, Y0
VPOR Y1, Y0, Y0
VPSHUFB Y0, Y4, Y0
VMOVDQU Y0, (DI)
ADDQ $16, SI
ADDQ $32, DI
DECQ CX
JNZ loop
VZEROUPPER
RET

// func decodeBlocks(dst, src *byte, blocks int) (chars int)
TEXT ·decodeBlocks(SB), 0, $128-32
MOVQ dst+0(FP), DI
MOVQ src+8(FP), SI
MOVQ blocks+16(FP), CX
XORQ AX, AX
VPBROADCASTB decodeBytes<>+0(SB), Y8
VPBROADCASTB decodeBytes<>+1(SB), Y9
VPBROADCASTB decodeBytes<>+2(SB), Y10
VPBROADCASTB decodeBytes<>+3(SB), Y11
VPBROADCASTB decodeBytes<>+4(SB), Y12
VPBROADCASTB decodeBytes<>+5(SB), Y13
VPBROADCASTB decodeBytes<>+6(SB), Y14
VMOVDQU pairWeights<>(SB), Y15

loop:
VMOVDQU (SI)(AX*1), Y0
VPADDB Y8, Y0, Y1
VPSUBUSB Y9, Y1, Y1
VPSUBB Y10, Y1, Y1
VPAND Y11, Y0, Y2
VPSUBB Y12, Y2, Y2
VPADDUSB Y13, Y2, Y2
VPMINUB Y2, Y1, Y1
VPADDUSB Y14, Y1, Y2
VPMOVMSKB Y2, BX
TESTL BX, BX
JNZ done
VPMADDUBSW Y15, Y1, Y1
VPACKUSWB Y1, Y1, Y1
VPERMQ $0x08, Y1, Y1
VMOVDQU X1, (DI)
ADDQ $16, DI
ADDQ $32, AX
DECQ CX
JNZ loop

done:
VZEROUPPER
MOVQ AX, chars+24(FP)
RET
100 changes: 100 additions & 0 deletions common/hexutil/hex_arm64.s
Original file line number Diff line number Diff line change
@@ -0,0 +1,100 @@
// Copyright 2026 The Erigon Authors
// This file is part of Erigon.
//
// Erigon is free software: you can redistribute it and/or modify
// it under the terms of the GNU Lesser General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Erigon is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU Lesser General Public License for more details.
//
// You should have received a copy of the GNU Lesser General Public License
// along with Erigon. If not, see <http://www.gnu.org/licenses/>.

#include "textflag.h"

DATA hexDigits<>+0(SB)/8, $"01234567"
DATA hexDigits<>+8(SB)/8, $"89abcdef"
GLOBL hexDigits<>(SB), RODATA|NOPTR, $16

// The 128-byte frames keep the stack check; see hex_amd64.s.

// func encodeBlocks(dst, src *byte, blocks int)
TEXT ·encodeBlocks(SB), 0, $128-24
MOVD dst+0(FP), R0
MOVD src+8(FP), R1
MOVD blocks+16(FP), R2
MOVD $hexDigits<>(SB), R3
VLD1 (R3), [V4.B16]
VMOVI $15, V5.B16

loop:
VLD1.P 16(R1), [V0.B16]
VUSHR $4, V0.B16, V1.B16
VAND V5.B16, V0.B16, V0.B16
VTBL V1.B16, [V4.B16], V1.B16
VTBL V0.B16, [V4.B16], V0.B16
VZIP1 V0.B16, V1.B16, V2.B16
VZIP2 V0.B16, V1.B16, V3.B16
VST1.P [V2.B16, V3.B16], 32(R0)
SUBS $1, R2, R2
BNE loop
RET

// Go 1.26 assembles neither UQSUB, UQADD nor UMAXV: saturation is UMAX/UMIN around SUB/ADD, and
// a block is valid when no nibble has a bit above the low four.
//
// func decodeBlocks(dst, src *byte, blocks int) (chars int)
TEXT ·decodeBlocks(SB), 0, $128-32
MOVD dst+0(FP), R0
MOVD src+8(FP), R1
MOVD blocks+16(FP), R2
MOVD $0, R5
VMOVI $0xc6, V8.B16
VMOVI $6, V9.B16
VMOVI $0xf0, V10.B16
VMOVI $0xdf, V11.B16
VMOVI $0x41, V12.B16
VMOVI $10, V13.B16
VMOVI $245, V14.B16

loop:
VLD1.P 32(R1), [V0.B16, V1.B16]
VADD V8.B16, V0.B16, V2.B16
VUMAX V9.B16, V2.B16, V2.B16
VSUB V9.B16, V2.B16, V2.B16
VSUB V10.B16, V2.B16, V2.B16
VAND V11.B16, V0.B16, V3.B16
VSUB V12.B16, V3.B16, V3.B16
VUMIN V14.B16, V3.B16, V3.B16
VADD V13.B16, V3.B16, V3.B16
VUMIN V3.B16, V2.B16, V2.B16
VADD V8.B16, V1.B16, V6.B16
VUMAX V9.B16, V6.B16, V6.B16
VSUB V9.B16, V6.B16, V6.B16
VSUB V10.B16, V6.B16, V6.B16
VAND V11.B16, V1.B16, V7.B16
VSUB V12.B16, V7.B16, V7.B16
VUMIN V14.B16, V7.B16, V7.B16
VADD V13.B16, V7.B16, V7.B16
VUMIN V7.B16, V6.B16, V6.B16
VUMAX V6.B16, V2.B16, V3.B16
VUSHR $4, V3.B16, V3.B16
VUADDLV V3.B16, V3
VMOV V3.H[0], R4
CBNZ R4, done
VUZP1 V6.B16, V2.B16, V3.B16
VUZP2 V6.B16, V2.B16, V7.B16
VSHL $4, V3.B16, V3.B16
VORR V7.B16, V3.B16, V3.B16
VST1.P [V3.B16], 16(R0)
ADD $32, R5
SUBS $1, R2, R2
BNE loop

done:
MOVD R5, chars+24(FP)
RET
76 changes: 76 additions & 0 deletions common/hexutil/hex_asm.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
// Copyright 2026 The Erigon Authors
// This file is part of Erigon.
//
// Erigon is free software: you can redistribute it and/or modify
// it under the terms of the GNU Lesser General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Erigon is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU Lesser General Public License for more details.
//
// You should have received a copy of the GNU Lesser General Public License
// along with Erigon. If not, see <http://www.gnu.org/licenses/>.

//go:build amd64 || arm64

package hexutil

import (
"encoding/hex"
"runtime"

"golang.org/x/sys/cpu"
)

// maxAsmBytes bounds the input of one assembly call. Assembly is never async-preempted, so a
// collection waits for the call to return.
const maxAsmBytes = 64 << 10

// NEON is in the arm64 baseline.
var useAsm = runtime.GOARCH == "arm64" || cpu.X86.HasAVX2

// encodeBlocks hex-encodes blocks of 16 bytes.
//
//go:noescape
func encodeBlocks(dst, src *byte, blocks int)

// decodeBlocks decodes blocks of 32 characters, stopping before the first block that holds a
// non-hex character, and returns the number of characters decoded.
//
//go:noescape
func decodeBlocks(dst, src *byte, blocks int) (chars int)

// encodeHex is hex.Encode with whole 16-byte blocks done by AVX2 or NEON.
func encodeHex(dst, src []byte) {
if useAsm {
for len(src) >= 16 && len(dst) >= 32 {
n := min(len(src), len(dst)/2, maxAsmBytes) &^ 15
encodeBlocks(&dst[0], &src[0], n/16)
src, dst = src[n:], dst[2*n:]
}
}
hex.Encode(dst, src)
}

// decodeHex is hex.Decode with whole 32-character blocks done by AVX2 or NEON, by algorithm 3 of
// http://0x80.pl/notesen/2022-01-17-validating-hex-parse.html: a digit maps to 0-9 and a letter of
// either case to 10-15, anything else to more than 15 on both paths, so the smaller of the two is
// the nibble. A block holding a non-hex character is left to hex.Decode, which reports it.
func decodeHex(dst, src []byte) (int, error) {
n := 0
if useAsm {
for len(src) >= 32 && len(dst) >= 16 {
c := min(len(src), 2*len(dst), maxAsmBytes) &^ 31
done := decodeBlocks(&dst[0], &src[0], c/32)
src, dst, n = src[done:], dst[done/2:], n+done/2
if done < c {
break
}
}
}
m, err := hex.Decode(dst, src)
return n + m, err
}
24 changes: 24 additions & 0 deletions common/hexutil/hex_generic.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
// Copyright 2026 The Erigon Authors
// This file is part of Erigon.
//
// Erigon is free software: you can redistribute it and/or modify
// it under the terms of the GNU Lesser General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Erigon is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU Lesser General Public License for more details.
//
// You should have received a copy of the GNU Lesser General Public License
// along with Erigon. If not, see <http://www.gnu.org/licenses/>.

//go:build !amd64 && !arm64

package hexutil

import "encoding/hex"

func encodeHex(dst, src []byte) { hex.Encode(dst, src) }
func decodeHex(dst, src []byte) (int, error) { return hex.Decode(dst, src) }
4 changes: 1 addition & 3 deletions common/hexutil/hexutil.go
Original file line number Diff line number Diff line change
Expand Up @@ -289,9 +289,7 @@ func EncodeTs(number uint64) []byte {

// Encode encodes b as a hex string with 0x prefix.
func Encode(b []byte) string {
enc := make([]byte, len(b)*2+2)
copy(enc, "0x")
hex.Encode(enc[2:], b)
enc, _ := Bytes(b).MarshalText()
return string(enc)
}

Expand Down
Loading
Loading