perf: add div/mod fast paths for the operand shapes real bytecode uses - #103
Open
Gabriel-Trintinalia wants to merge 1 commit into
Open
Conversation
opDiv/opMod ran the full toLimbs -> Knuth Algorithm D -> fromLimbs round trip whatever the operands looked like, and each computed the quotient/remainder pair via limbDivMod only to throw half away. Only a single-limb *divisor* was special-cased, never a small dividend, and there was no power-of-two path anywhere. Add three tests, cheapest first, in divU256/modU256: a < b, a power-of-two divisor (shift and mask), and both operands inside a machine word (one hardware divu). Split the two functions so DIV does not compute a remainder and MOD goes through limbMod, which computes only the remainder. addmod/mulmod get the power-of-two-modulus and single-word paths too; for a power of two the mask is exact rather than an approximation, because reducing mod 2^k is precisely "keep the low k bits" and the wrapping add/multiply already holds the true result's low 256 bits — which lets MULMOD skip the 512-bit multiply and the Knuth reduction both. Measured alone on top of main@259467a, over for_amsterdam_at_0010M (2,458 units, ReleaseFast ELF under ziskemu): total trace cells 22.9308e12 -> 22.5227e12, **-1.78%**; steps -1.97%. Two things about those numbers are worth recording, because the first reading of them was wrong and nearly cost this change. The win is *not* in the arithmetic suite. Those fixtures pick full-width operands by construction (mod_bits_127/191/255), so every fast path fails after being paid for: instruction/arithmetic measures **+0.94%**, worst units ADDMOD/MOD at +3.7-4.9%. Read per-suite that looks like a pure regression, and it was reverted on exactly that reading before a per-unit check showed otherwise. The win is in fixtures whose bytecode *computes* something. The memory and call-context variants that vary their offsets per iteration divide to derive them, so the paths fire: instruction/memory -6.49%, instruction/call_context -3.84%, instruction/log -2.64%, instruction/account_query -1.18%. CODECOPY and MCOPY at size 0 go 37.1G -> 19.0G (-48.7%), while their fixed-offset siblings are unchanged to five significant figures. That is also the shape of real bytecode, where / 32, / 1e18 and x / BIG are everywhere — the arithmetic suite is the unrepresentative one here. Net across the tier this is clearly positive, so the +0.94% on deliberately worst-case operands is accepted rather than tuned against. MULMOD at full width is untouched and remains the worst per-gas opcode in the corpus; that is a repricing question (8 gas for a genuine 512-bit modmul), not an implementation one. It also leaves the ZisK arith256_mod/div256 offload deferred on firmer ground: the cheap paths already take the realistic operand shapes, so the accelerator would be buying only the full-width case. The fuzz tests generate the boundary values and exact powers of two explicitly instead of hoping a PRNG hits them, and check against Zig's own operators and a u512 reference, which share none of the limb code. Gate: zig build test clean; blockchain-tests 97324 passed / 0 failed / 48 skipped; zkevm 23994/23994 — all identical to main. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Problem
DIV/MOD/SDIV/SMOD took the full-width path regardless of operand shape, even though real bytecode overwhelmingly uses small or otherwise-degenerate operands.
Change
Add fast paths for the operand shapes real bytecode actually uses. Touches
src/evm/interpreter/opcodes/arithmetic.zigplus its tests.Effect
−3.39% on the subset that exercises it.
arithmeticand −6.8% inmemory. The arithmetic fixtures pick full-width operands on purpose, so every fast path fails after being paid for; the real win lands in MCOPY atcopy_size_0(37.1G → 19.1G, −48.7%) and RETURNDATACOPY (−39.2%).This is the clearest instance of a general trap worth knowing: fixtures named after one opcode family exercise several, so attributing a suite to a change by its name is unreliable.
Why it matters beyond this number
The div/mod family is 20 units carrying 6.4% of total proving cost at a median 7,965 c/g — by far the worst cost-per-gas cluster, against an overall median near 1,400. Related and not addressed here: MULMOD remains an outlier at 114.3B cycles vs ~20.6B for EXP in the ZisK v1.1.0-alpha instruction benchmark, which is the case for wiring up
zkvm_mulmod256.Measurement context
Numbers are from
for_amsterdam_at_0010M(2,458 units), ReleaseFast ELF under ziskemu at--jobs 8, baseline259467a. Full analysis inPERF_FINDINGS_AMSTERDAM.md.This is one of 5 granular PRs split out of a stack that measured −10.92% cumulatively (22.93e12 → 20.43e12 trace cells). That headline figure belongs to the stack, not to this PR alone.
Gated on
zig build testplus the full blockchain-test and zkevm suites, which sat at 97324 passed / 0 failed / 48 skipped and 23994/23994 at every commit in the stack — identical to259467a.🤖 Generated with Claude Code
Note
Medium Risk
Changes core EVM arithmetic semantics paths; fast-path branches must match spec exactly, though behavior is guarded by new differential fuzz tests against independent references.
Overview
Speeds up EVM division and modular arithmetic by routing
DIV/MOD(and signedSDIV/SMODvia absolute values) through newdivU256andmodU256helpers instead of always calling full-widthlimbDivModand discarding half the result.Each helper short-circuits for operand shapes common in real bytecode: divide-by-zero,
a < b, power-of-two divisors (shift/mask), and 64-bit operands (nativediv/mod), with Knuth limb math only on the slow path.addmodandmulmodgain the same style of power-of-two modulus and all-operands-fit-in-u64 fast paths.Tests: two 20k-iteration differential fuzz suites compare the fast paths against Zig’s native operators / widened
u512references, with operands biased toward corners and powers of two.Reviewed by Cursor Bugbot for commit 4a22f09. Bugbot is set up for automated code reviews on this repo. Configure here.