Skip to content

perf: add div/mod fast paths for the operand shapes real bytecode uses - #103

Open
Gabriel-Trintinalia wants to merge 1 commit into
Consensys-Incorporated:mainfrom
Gabriel-Trintinalia:perf/divmod-fast-paths
Open

perf: add div/mod fast paths for the operand shapes real bytecode uses#103
Gabriel-Trintinalia wants to merge 1 commit into
Consensys-Incorporated:mainfrom
Gabriel-Trintinalia:perf/divmod-fast-paths

Conversation

@Gabriel-Trintinalia

@Gabriel-Trintinalia Gabriel-Trintinalia commented Aug 28, 2026

Copy link
Copy Markdown
Collaborator

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.zig plus its tests.

Effect

−3.39% on the subset that exercises it.

⚠️ Do not judge this PR by the arithmetic suite. It shows +0.94% in arithmetic and −6.8% in memory. The arithmetic fixtures pick full-width operands on purpose, so every fast path fails after being paid for; the real win lands in MCOPY at copy_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, baseline 259467a. Full analysis in PERF_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 test plus 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 to 259467a.

🤖 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 signed SDIV/SMOD via absolute values) through new divU256 and modU256 helpers instead of always calling full-width limbDivMod and 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 (native div/mod), with Knuth limb math only on the slow path. addmod and mulmod gain 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 u512 references, 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.

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>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant