Skip to content
Open
Show file tree
Hide file tree
Changes from 9 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: 9 additions & 7 deletions common/hexutil/bytes.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ import (
"encoding/hex"
"encoding/json"
"reflect"
"slices"
)

var bytesT = reflect.TypeFor[Bytes]()
Expand All @@ -33,25 +34,26 @@ const HexPrefix = `0x`

// MarshalText implements encoding.TextMarshaler
func (b Bytes) MarshalText() ([]byte, error) {
result := make([]byte, len(b)*2+2)
copy(result, HexPrefix)
hex.Encode(result[2:], b)
return result, nil
return b.AppendText(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 Down
23 changes: 23 additions & 0 deletions common/hexutil/encode_generic.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
// 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 !(go1.27 && goexperiment.simd && amd64)

package hexutil

import "encoding/hex"

func encodeHex(dst, src []byte) { hex.Encode(dst, src) }
47 changes: 47 additions & 0 deletions common/hexutil/encode_simd.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
// 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 go1.27 && goexperiment.simd && amd64

package hexutil

import (
"encoding/hex"
"simd/archsimd"
)

var (
hexDigits32 = [32]uint8{'0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'a', 'b', 'c', 'd', 'e', 'f',
'0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'a', 'b', 'c', 'd', 'e', 'f'}
hasAVX2 = archsimd.X86.AVX2()
)

// encodeHex is hex.Encode with whole 16-byte blocks done by AVX2. Each byte is widened to a
// uint16 holding its high nibble in the low byte and its low nibble in the high byte, so one
// in-lane byte shuffle turns the nibbles into digits in output order.
func encodeHex(dst, src []byte) {
if hasAVX2 {
digits := archsimd.LoadUint8x32Array(&hexDigits32)
low := archsimd.BroadcastUint16x16(0x0f)
for len(src) >= 16 && len(dst) >= 32 {
w := archsimd.LoadUint8x16Array((*[16]uint8)(src)).ExtendToUint16()
w = w.ShiftAllRight(4).Or(w.And(low).ShiftAllLeft(8))
digits.PermuteOrZeroGrouped(w.AsUint8x32().AsInt8x32()).StoreArray((*[32]uint8)(dst))
src, dst = src[16:], dst[32:]
}
}
hex.Encode(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).AppendText(nil)
return string(enc)
}

Expand Down
24 changes: 24 additions & 0 deletions common/hexutil/hexutil_bench_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,8 @@
package hexutil

import (
"encoding/hex"
"fmt"
"math/big"
"testing"
)
Expand All @@ -33,3 +35,25 @@ func BenchmarkEncodeBig(b *testing.B) {
}
}
}

func BenchmarkAppendQuoted(b *testing.B) {
for _, n := range []int{20, 32, 64, 256, 1024} {
src := make([]byte, n)
for i := range src {
src[i] = byte(i * 7)
}
dst := make([]byte, 0, QuotedLen(n))
b.Run(fmt.Sprintf("stdlib/%d", n), func(b *testing.B) {
b.SetBytes(int64(n))
for b.Loop() {
dst = append(hex.AppendEncode(append(dst[:0], `"0x`...), src), '"')
}
})
b.Run(fmt.Sprintf("hexutil/%d", n), func(b *testing.B) {
b.SetBytes(int64(n))
for b.Loop() {
dst = AppendQuoted(dst[:0], src)
}
})
}
}
19 changes: 19 additions & 0 deletions common/hexutil/hexutil_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,8 +17,10 @@
package hexutil

import (
"encoding/hex"
"fmt"
"math/big"
"math/rand/v2"
"testing"

"github.com/stretchr/testify/require"
Expand Down Expand Up @@ -261,3 +263,20 @@ func TestIsValidQuantity(t *testing.T) {
})
}
}

func TestEncodeHexMatchesStdlib(t *testing.T) {
Comment thread
AskAlexSharov marked this conversation as resolved.
r := rand.New(rand.NewPCG(1, 2))
for n := 0; n <= 300; n++ {
src := make([]byte, n)
for i := range src {
src[i] = byte(r.Uint32())
}
want := hex.EncodeToString(src)
dst := make([]byte, 2*n)
encodeHex(dst, src)
require.Equal(t, want, string(dst), "len %d", n)
require.Equal(t, `x"0x`+want+`"`, string(AppendQuoted([]byte("x"), src)), "len %d", n)
text, _ := Bytes(src).AppendText([]byte("x"))
require.Equal(t, "x0x"+want, string(text), "len %d", n)
}
}
Loading