From ea6b7b284aaec800fb58370883e69f29e0490cf2 Mon Sep 17 00:00:00 2001 From: yperbasis Date: Fri, 14 Aug 2026 17:29:01 +0200 Subject: [PATCH 1/3] keccak: benchmark ARM64 backends directly --- keccak_arm64_bench_test.go | 49 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 49 insertions(+) create mode 100644 keccak_arm64_bench_test.go diff --git a/keccak_arm64_bench_test.go b/keccak_arm64_bench_test.go new file mode 100644 index 0000000..a583892 --- /dev/null +++ b/keccak_arm64_bench_test.go @@ -0,0 +1,49 @@ +//go:build arm64 && !purego + +package keccak + +import ( + "strconv" + "testing" +) + +var arm64BackendDigest [32]byte + +func BenchmarkARM64Backends(b *testing.B) { + nativeAvailable := useASM + defer func() { useASM = nativeAvailable }() + + if nativeAvailable { + b.Run("SHA3", func(b *testing.B) { + benchmarkARM64Backend(b, true) + }) + } + b.Run("XCrypto", func(b *testing.B) { + benchmarkARM64Backend(b, false) + }) +} + +func benchmarkARM64Backend(b *testing.B, native bool) { + for _, size := range []int{32, 128, 256, 1024, 4096, 500 * 1024} { + data := make([]byte, size) + for i := range data { + data[i] = byte(i) + } + + b.Run(strconv.Itoa(size), func(b *testing.B) { + useASM = native + var h Hasher + h.Reset() + var out [32]byte + b.SetBytes(int64(size)) + b.ReportAllocs() + b.ResetTimer() + for b.Loop() { + h.Reset() + h.Write(data) + h.Read(out[:]) + arm64BackendDigest = out + } + }) + } +} From 9179263394c9255308489cc7ac94013c0bd1ff4f Mon Sep 17 00:00:00 2001 From: Alexey Sharov Date: Wed, 2 Sep 2026 11:10:19 +0700 Subject: [PATCH 2/3] keccak: benchmark both backends, not just arm64 Move the A/B into keccak_asm_test.go so amd64 runs it too, reuse benchSizes/benchName so the names line up with the rest of the suite, and gate the assembly arm on a new immutable hasASM probe instead of the useASM flag this benchmark mutates. The recorded digest is now compared across the two backends rather than only written. --- keccak_amd64.go | 7 +++++- keccak_arm64.go | 7 +++++- keccak_arm64_bench_test.go | 49 -------------------------------------- keccak_asm_test.go | 49 ++++++++++++++++++++++++++++++++++++++ 4 files changed, 61 insertions(+), 51 deletions(-) delete mode 100644 keccak_arm64_bench_test.go diff --git a/keccak_amd64.go b/keccak_amd64.go index b4565dd..e463d5c 100644 --- a/keccak_amd64.go +++ b/keccak_amd64.go @@ -4,7 +4,12 @@ package keccak import "golang.org/x/sys/cpu" -func init() { useASM = cpu.X86.HasBMI1 && cpu.X86.HasBMI2 } +// hasASM is the CPU probe, recorded once. useASM is the dispatch flag and +// benchmarks flip it, so anything asking "can this CPU run the assembly?" +// must read hasASM. +var hasASM = cpu.X86.HasBMI1 && cpu.X86.HasBMI2 + +func init() { useASM = hasASM } // keccakF1600BMI2 permutes state. When buf != nil, it first XORs rate bytes // of buf into state, saving one full memory pass. diff --git a/keccak_arm64.go b/keccak_arm64.go index f651865..1d90f8c 100644 --- a/keccak_arm64.go +++ b/keccak_arm64.go @@ -18,8 +18,13 @@ import ( // likewise leaves iOS without CPU feature detection (its darwin sysctl // probing in internal/cpu is build-tagged `darwin && !ios`), so its SHA3 // assembly never runs on iOS either. +// hasASM is the CPU probe, recorded once. useASM is the dispatch flag and +// benchmarks flip it, so anything asking "can this CPU run the assembly?" +// must read hasASM. +var hasASM = runtime.GOOS == "darwin" || cpu.ARM64.HasSHA3 + func init() { - useASM = runtime.GOOS == "darwin" || cpu.ARM64.HasSHA3 + useASM = hasASM } // keccakF1600Sha3 permutes state. When buf != nil, it first XORs rate bytes diff --git a/keccak_arm64_bench_test.go b/keccak_arm64_bench_test.go deleted file mode 100644 index a583892..0000000 --- a/keccak_arm64_bench_test.go +++ /dev/null @@ -1,49 +0,0 @@ -//go:build arm64 && !purego - -package keccak - -import ( - "strconv" - "testing" -) - -var arm64BackendDigest [32]byte - -func BenchmarkARM64Backends(b *testing.B) { - nativeAvailable := useASM - defer func() { useASM = nativeAvailable }() - - if nativeAvailable { - b.Run("SHA3", func(b *testing.B) { - benchmarkARM64Backend(b, true) - }) - } - b.Run("XCrypto", func(b *testing.B) { - benchmarkARM64Backend(b, false) - }) -} - -func benchmarkARM64Backend(b *testing.B, native bool) { - for _, size := range []int{32, 128, 256, 1024, 4096, 500 * 1024} { - data := make([]byte, size) - for i := range data { - data[i] = byte(i) - } - - b.Run(strconv.Itoa(size), func(b *testing.B) { - useASM = native - var h Hasher - h.Reset() - var out [32]byte - b.SetBytes(int64(size)) - b.ReportAllocs() - b.ResetTimer() - for b.Loop() { - h.Reset() - h.Write(data) - h.Read(out[:]) - arm64BackendDigest = out - } - }) - } -} diff --git a/keccak_asm_test.go b/keccak_asm_test.go index 44a8f64..d7ff874 100644 --- a/keccak_asm_test.go +++ b/keccak_asm_test.go @@ -34,3 +34,52 @@ func TestXorAndPermute(t *testing.T) { } } } + +// backendDigests carries the digest each size produced on the first backend +// benchmarked, so the second one is checked against it rather than only +// timed. +var backendDigests = map[int][32]byte{} + +// BenchmarkBackends compares the assembly backend against the x/crypto +// fallback on the same CPU, to answer whether the assembly is actually +// faster on this core. It flips useASM, so it must not run in parallel with +// anything else in the package. +func BenchmarkBackends(b *testing.B) { + restore := useASM + defer func() { useASM = restore }() + + if hasASM { + b.Run("ASM", func(b *testing.B) { benchmarkBackend(b, true) }) + } + b.Run("XCrypto", func(b *testing.B) { benchmarkBackend(b, false) }) +} + +func benchmarkBackend(b *testing.B, native bool) { + for _, size := range benchSizes { + data := make([]byte, size) + for i := range data { + data[i] = byte(i) + } + + b.Run(benchName(size), func(b *testing.B) { + useASM = native + var h Hasher + // Warm up outside the timer: with useASM false the first Reset + // lazily builds the x/crypto hasher, and that one allocation + // would otherwise be reported as a per-op cost. + h.Reset() + var out [32]byte + b.SetBytes(int64(size)) + b.ReportAllocs() + for b.Loop() { + h.Reset() + h.Write(data) + h.Read(out[:]) + } + if want, ok := backendDigests[size]; ok && out != want { + b.Fatalf("backend digest for %d bytes = %x, other backend gave %x", size, out, want) + } + backendDigests[size] = out + }) + } +} From 72b4bfced22b8df5c1ba9922b20c832783b6b2ce Mon Sep 17 00:00:00 2001 From: Alexey Sharov Date: Wed, 2 Sep 2026 11:53:32 +0700 Subject: [PATCH 3/3] keccak: check each backend against x/crypto, not against the other arm `-bench BenchmarkBackends/ASM` ran one arm, so the cross-arm compare had nothing to compare and reported success on a corrupted digest. --- keccak_asm_test.go | 20 ++++++++++++-------- 1 file changed, 12 insertions(+), 8 deletions(-) diff --git a/keccak_asm_test.go b/keccak_asm_test.go index d7ff874..5ce3f2b 100644 --- a/keccak_asm_test.go +++ b/keccak_asm_test.go @@ -5,6 +5,8 @@ package keccak import ( "math/rand" "testing" + + "golang.org/x/crypto/sha3" ) // TestXorAndPermute verifies the fused XOR+permute assembly entry point @@ -35,11 +37,6 @@ func TestXorAndPermute(t *testing.T) { } } -// backendDigests carries the digest each size produced on the first backend -// benchmarked, so the second one is checked against it rather than only -// timed. -var backendDigests = map[int][32]byte{} - // BenchmarkBackends compares the assembly backend against the x/crypto // fallback on the same CPU, to answer whether the assembly is actually // faster on this core. It flips useASM, so it must not run in parallel with @@ -61,6 +58,11 @@ func benchmarkBackend(b *testing.B, native bool) { data[i] = byte(i) } + ref := sha3.NewLegacyKeccak256().(KeccakState) + ref.Write(data) + var want [32]byte + ref.Read(want[:]) + b.Run(benchName(size), func(b *testing.B) { useASM = native var h Hasher @@ -76,10 +78,12 @@ func benchmarkBackend(b *testing.B, native bool) { h.Write(data) h.Read(out[:]) } - if want, ok := backendDigests[size]; ok && out != want { - b.Fatalf("backend digest for %d bytes = %x, other backend gave %x", size, out, want) + // Against x/crypto, not against the other arm: a -bench filter + // that runs only one arm would leave a cross-arm check with + // nothing to compare, and report success either way. + if out != want { + b.Fatalf("backend digest for %d bytes = %x, want %x", size, out, want) } - backendDigests[size] = out }) } }