Skip to content

perf: split keccak benchmark into narrow and wide variants - #2195

Open
gbotrel wants to merge 1 commit into
mainfrom
perf/keccak-wide-narrow
Open

perf: split keccak benchmark into narrow and wide variants#2195
gbotrel wants to merge 1 commit into
mainfrom
perf/keccak-wide-narrow

Conversation

@gbotrel

@gbotrel gbotrel commented Aug 28, 2026

Copy link
Copy Markdown
Contributor

Replaces the keccak benchmark with two implementations of the same optimised algorithm, kept side by side until the prover cost of wide call lookups is settled. Then we delete the loser.

Shared by both: fully fused round (θ ρ π χ ι in one straight-line pass), constant rotations as :: split/rejoin instead of runtime shifts, zero integer divisions (the absorb loop guard replaces /136; a u5::u3 split replaces /%8).

keccak_narrow — 25 state lanes live in keccak_state memory between rounds. Narrow call signatures, 50 memory accesses per round.

keccak_wide — lanes threaded through keccak_f/keccak_round as parameters/returns. The permutation touches no memory; keccak_state shrinks to a 17-lane scratch for the final padded block. Cost: two 50-value call signatures.

KOALABEAR_16, 4,236-byte message (32 permutations); fast mode on 1 MB:

cells complexity Σn·d² lookups fast mode
old 35,924,894 195,360 3,633 2,639 ms
narrow 7,811,096 (−78%) 128,676 (−34%) 3,507 801 ms
wide 6,143,716 (−83%) 255,040 (+31%) 4,103 564 ms

wide wins cells and execution; narrow wins the complexity metric. The entire gap is the two 50-lane call-lookup edges — whichever way the lookup argument prices those, that variant stays.

ParallelTracing is dropped from the keccak tests: sharded tracing of read-write memories violates active_monotony (see the util.Config.ParallelTracing docstring). Pre-existing — reproducible on unmodified main with zkc trace --check --sharding keccak_f:10 on the 4,236-byte input; the old test was flaky whenever sampling picked that line.

Verified: 8/8 sponge-boundary hashes (lengths 0, 1, 7, 8, 135, 136, 137, 4236) + trace --check clean, both variants; Test_ZkcBench_Keccak{Narrow,Wide} green ×3; make zkc-lint clean.

Two implementations of the same optimised keccak, benchmarked side by
side until the prover cost of wide call lookups is settled; the loser
gets deleted later.

Both variants: fully fused round (theta/rho/pi/chi/iota in one
straight-line pass), constant rotations as ::split/rejoin instead of
runtime shifts, and no integer division (the absorb loop guard replaces
/136, a u5::u3 split replaces /%8).

- keccak_narrow: 25 state lanes in keccak_state memory between rounds;
  narrow call signatures, 50 memory accesses per round.
- keccak_wide: lanes threaded through keccak_f/keccak_round as
  parameters/returns; the permutation touches no memory, keccak_state
  shrinks to a 17-lane scratch for the final padded block, at the cost
  of two 50-value call signatures.

KOALABEAR_16, 4,236-byte message (32 permutations), fast mode on 1 MB:

            cells        complexity   fast-mode
  old       35,924,894   195,360      2,639 ms
  narrow     7,811,096   128,676        801 ms
  wide       6,143,716   255,040        564 ms

ParallelTracing is dropped from the keccak tests: sharded tracing of
read-write memories violates active_monotony (see the
util.Config.ParallelTracing docstring) and already failed on the
unmodified benchmark whenever sampling picked the 4,236-byte input.

Signed-off-by: Gautam Botrel <gautam.botrel@gmail.com>
Copilot AI lite review requested due to automatic review settings August 28, 2026 14:05

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR replaces the single Keccak benchmark implementation with two side-by-side variants (narrow vs wide call signatures) to compare prover/runtime tradeoffs while keeping the underlying optimized round logic equivalent.

Changes:

  • Split the Keccak benchmark into keccak_narrow (state stored in keccak_state memory between rounds) and keccak_wide (state threaded through calls; permutation is memory-free).
  • Simplify the Keccak scratch-memory layout to match the new benchmark variants (remove now-unused buffers/offsets).
  • Update ZkC bench tests to run both variants and drop ParallelTracing due to the known read-write-memory sharding issue.

Reviewed changes

Copilot reviewed 9 out of 10 changed files in this pull request and generated 2 comments.

Show a summary per file
File Description
testdata/zkc/bench/keccak/memory.zkc Shrinks/repurposes keccak_state layout to just rows + PIB scratch used by the new variants.
testdata/zkc/bench/keccak/impl.zkc Removes the previous single Keccak implementation (superseded by narrow/wide variants).
testdata/zkc/bench/keccak/impl_narrow.zkc Adds the memory-backed (“narrow-call”) Keccak implementation.
testdata/zkc/bench/keccak/impl_wide.zkc Adds the register-threaded (“wide-call”) Keccak implementation with pure permutation calls.
testdata/zkc/bench/keccak/constants.zkc Removes rotation/mod helpers that are no longer used by the fused-round implementations.
testdata/zkc/bench/keccak_narrow.zkc New benchmark entrypoint wiring inputs/outputs for the narrow variant.
testdata/zkc/bench/keccak_narrow.accepts Adds acceptance vectors for the narrow benchmark.
testdata/zkc/bench/keccak_wide.zkc Switches benchmark entrypoint to include impl_wide.zkc.
testdata/zkc/bench/keccak_wide.accepts Adds acceptance vectors for the wide benchmark.
pkg/test/zkc_bench_test.go Splits the benchmark test into Narrow/Wide cases and removes ParallelTracing for stability.
Suppressed comments (2)

testdata/zkc/bench/keccak/impl_wide.zkc:177

  • print_big_endian prints b0..b7 (least-significant byte first), so it is not big-endian. Updating the comment avoids confusion when comparing against expected digest byte order.
// print_big_endian prints a u64 in BIG-ENDIAN convention

testdata/zkc/bench/keccak/impl_narrow.zkc:62

  • print_big_endian prints b0..b7 (least-significant byte first), so it is not big-endian. Updating the comment avoids confusion about endianness.
// print_big_endian prints a u64 in BIG-ENDIAN convention

💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

print_hash(s00, s10, s20, s30)
}

// print_hash prints the hash in BIG-ENDIAN convention
print_hash()
}

// print_hash prints the hash in BIG-ENDIAN convention
@DavePearce

DavePearce commented Sep 1, 2026

Copy link
Copy Markdown
Contributor

Additional stats:

Original

------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
               |          | PC_max |            Registers (pre splitting)            |  Post |      Vanishing constraints (by degree)      | lookups |    complexity |           cells |
 Module        | type     |        | u1 | u2-u16 | u17-u32 | u33-u64 | u65+ | native | Split | d1 |   d2 |   d3 |  d4 | d5 | d6 | d7 | d8+ |         | (= sum n.d^2) | (static tables) |
------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
 Total: 46     | function |      7 | 59 |    309 |     475 |     275 |      |        |  3645 |  8 | 4290 | 2396 | 432 | 93 |  4 |    |     |    3617 |        195288 |                 |

Narrow

------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
              |          | PC_max |            Registers (pre splitting)            |  Post |      Vanishing constraints (by degree)      | lookups |    complexity |           cells |
 Module       | type     |        | u1 | u2-u16 | u17-u32 | u33-u64 | u65+ | native | Split | d1 |   d2 |   d3 |  d4 | d5 | d6 | d7 | d8+ |         | (= sum n.d^2) | (static tables) |
------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
 Total: 33    | function |      7 | 60 |    199 |     326 |     368 |      |        |  3412 |  5 | 3124 | 1285 | 302 | 97 |  4 |    |     |    3507 |        128676 |                 |

Wide

-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
                 |          | PC_max |            Registers (pre splitting)            |  Post |      Vanishing constraints (by degree)       | lookups |    complexity |           cells |
 Module          | type     |        | u1 | u2-u16 | u17-u32 | u33-u64 | u65+ | native | Split | d1 |   d2 |   d3 |  d4 |  d5 | d6 | d7 | d8+ |         | (= sum n.d^2) | (static tables) |
-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
 Total: 29       | function |      7 | 58 |    101 |     226 |     572 |      |        |  3798 |  4 | 7318 | 1867 | 867 | 155 |  2 |    |     |    4103 |        255040 |                 |

@DavePearce

Copy link
Copy Markdown
Contributor

The cell reduction for both wide and narrow implementations looks great, but they are not attacking the problem of columns. Overall, I am a little surprised the narrow implementation does not do better. Looking at some specifics, the implementation for narrow looks odd:

var r3h:u3, r3l:u61
r3h::r3l = a02 ^ d0
var b21:u64 = r3l::r3h

This is introducing a lot of temporary columns, instead of using a separate rotl function.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants