Repository navigation
Conversation
There was a problem hiding this comment.
Pull request overview
Eliminates repeated fallback Hasher.Sum256 allocations by reusing digest storage.
Changes:
- Adds reusable digest buffers for fallback implementations.
- Adds zero-allocation tests and benchmarks for native, pure-Go, and runtime fallback paths.
Reviewed changes
Copilot reviewed 4 out of 4 changed files in this pull request and generated no comments.
| File | Description |
|---|---|
keccak_default.go |
Reuses a stored digest buffer. |
keccak_asm.go |
Reuses the inactive sponge buffer during fallback. |
keccak_alloc_test.go |
Adds allocation tests and benchmarks. |
keccak_alloc_asm_test.go |
Exercises forced runtime fallback allocation behavior. |
💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
|
[P2] Use the slice returned by In both Please use the returned slice: sum := h.xc.Sum(h.buf[:0])
return [32]byte(sum)and apply the equivalent change to I verified that this contract-safe form remains allocation-free in both forced-fallback and pure-Go benchmarks ( Reviewed at |
Holding it in Hasher made the interface call leak the receiver, so every stack-allocated Hasher went to the heap - 400 B on the assembly path to save 32 B on the fallback. Move it into the heap-allocated x/crypto state, drop the Sum256 doc's no-modification claim, and have the alloc test measure the whole hasher lifecycle instead of only the steady state.
Reusing a digest buffer on the receiver made Sum256 write through the Hasher, so two goroutines reading a digest from one shared *Hasher now race - proven under -race, and race-free on master. That hits every purego build and every non-BMI2/non-SHA3 CPU. The 32 B cannot be removed without giving up the read-only guarantee, so drop the production change and keep the tests as the guard against reintroducing it.
Found
The same test is clean on master. It is invisible on a BMI2 / FEAT_SHA3 box, because the sponge path stays read-only — it hits every
FixedThe production change is out. The 32 B is inherent: routing a local array through the interface call is what allocates it, and both ways to remove it are worse — a receiver-held buffer races, a buffer inside What stays is the test that catches either regression. It compares a stack-allocated With this, #22 and #24 now merge cleanly and pass |
Summary
Hasher.Sum256callsWhy
Passing a local
[32]bytethrough the storedhash.Hashinterface makes the compiler move it to the heap. Repeated fallbackSum256calls therefore allocate 32 bytes each.The runtime fallback reuses the otherwise inactive native sponge buffer. The pure-Go implementation keeps a 32-byte digest buffer in
Hasher.TDD
Both fallback variants initially failed their allocation test:
Benchmark
Apple M2 Max, Go 1.25.7, forced runtime fallback, representative results:
Verification
go test ./...go test -race ./...go test -tags purego ./...go vet ./...go mod tidy -diffgolangci-lint run(twice)golangci-lint run --build-tags purego