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_asm_test.go b/keccak_asm_test.go index 44a8f64..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 @@ -34,3 +36,54 @@ func TestXorAndPermute(t *testing.T) { } } } + +// 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) + } + + 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 + // 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[:]) + } + // 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) + } + }) + } +}