perf(compiler): strength-reduce EXP with a constant power-of-two base or trivial exponent#550
Open
abmcar wants to merge 1 commit into
Open
perf(compiler): strength-reduce EXP with a constant power-of-two base or trivial exponent#550abmcar wants to merge 1 commit into
abmcar wants to merge 1 commit into
Conversation
Contributor
There was a problem hiding this comment.
Pull request overview
This PR adds compiler-side EXP strength-reduction fast paths in the EVM MIR builder to avoid emitting the square-and-multiply loop for common constant-base cases, while preserving EIP-160 dynamic gas behavior. It also introduces a focused differential test to validate both output and gas behavior across interpreter vs multipass JIT.
Changes:
- Add
EXP(base, 0) -> 1andEXP(base, 1) -> basefast paths with explicit EIP-160 dynamic gas charging. - Add
EXP(2^k, x) -> (x >= ceil(256/k) ? 0 : 1 << (k*x))fast path for constant power-of-two bases with an overflow guard. - Add new multipass-only differential tests and wire them into the spec-test CMake targets, plus a light change proposal doc.
Reviewed changes
Copilot reviewed 4 out of 4 changed files in this pull request and generated 3 comments.
| File | Description |
|---|---|
src/compiler/evm_frontend/evm_mir_compiler.cpp |
Implements EXP strength-reduction fast paths and preserves dynamic gas charging semantics. |
src/tests/evm_exp_strength_tests.cpp |
Adds differential tests covering correctness and EIP-160 gas deltas for the new fast paths. |
src/tests/CMakeLists.txt |
Adds evmExpStrengthTests target and registers it under multipass spec tests. |
docs/changes/2026-06-15-evm-exp-strength-reduction/README.md |
Documents the optimization, motivation, and test plan as a light change proposal. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
Comment on lines
+133
to
+137
| EXPECT_TRUE(Multi.JITCompiled) << "multipass did not JIT-compile: " << Label; | ||
| EXPECT_EQ(Interp.Status, EVMC_SUCCESS) | ||
| << "interpreter did not succeed: " << Label; | ||
| EXPECT_EQ(Multi.Status, Interp.Status) << "status diverged: " << Label; | ||
| EXPECT_EQ(Multi.OutputHex, Interp.OutputHex) << "output diverged: " << Label; |
| @@ -0,0 +1,59 @@ | |||
| # Change: Strength-reduce EXP for constant power-of-two bases | |||
|
|
|||
| - **Status**: Proposed | |||
Comment on lines
+34
to
+36
| - EIP-160 dynamic gas unchanged: it depends only on the exponent and is charged | ||
| by the unchanged general path, so gas is byte-for-byte identical. | ||
| - No effect on other EXP forms or other opcodes. |
⚡ Performance Regression Check Results✅ Performance Check Passed (interpreter)Performance Benchmark Results (threshold: 25%)
Summary: 194 benchmarks, 0 regressions ✅ Performance Check Passed (multipass)Performance Benchmark Results (threshold: 25%)
Summary: 194 benchmarks, 0 regressions |
… or trivial exponent handleExp emitted the full inline square-and-multiply loop for every EXP whose operands were not both constant. Add three semantics- and EIP-160-gas-faithful fast paths: - EXP(base, 0) -> 1 (dynamic base, constant exponent 0; dynamic gas 0) - EXP(base, 1) -> base (dynamic base, constant exponent 1; dynamic gas GasPerByte) - EXP(2^k, x) -> (k*x >= 256) ? 0 : 1 << (k*x) for any constant power-of-two base with a dynamic exponent. For k == 1 this is 1 << x, where handleShift<BO_SHL> already wraps shift >= 256 to 0 and k*x cannot overflow. For k >= 2 the product k*x wraps modulo 2^256 for large x, so the result is guarded by an explicit x >= ceil(256/k) -> 0 test rather than handleShift's own >= 256 check (which would see the wrapped product); below that threshold k*x is exact and < 256. This covers the 256**x (k=8) storage-packing idiom. The EIP-160 dynamic gas depends only on the exponent and is charged by the unchanged general-path machinery, so gas is byte-for-byte identical. Correctness was checked during development with a differential harness (multipass output vs the interpreter and an independent 1<<x table, across power-of-two bases, threshold-straddling exponents, the overflow-guard case, and EIP-160 gas deltas); regression coverage relies on the EEST state-test corpus and the multipass unit suite. EXP is ~0.12% of opcodes on a mainnet sample and most is already double-constant- folded, so the aggregate runtime effect stays below benchmark noise; the per-hit win (one shift+select vs a square-and-multiply loop) is large on the 256**x sites this now covers. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
562875a to
8f11147
Compare
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.
What
Add three compile-time fast paths to
handleExpfor a constant base:EXP(base, 0) -> 1,EXP(base, 1) -> base(dynamic base, constant exponent)EXP(2^k, x) -> (k*x >= 256) ? 0 : 1 << (k*x)for any constant power-of-two base with a dynamic exponentPreviously every EXP whose operands were not both constant emitted the inline square-and-multiply loop. This covers the
2 ** xand256 ** xforms (the latter is the Solidity storage-packing idiom).Soundness
(2^k)^x = 2^(k*x) mod 2^256. Fork >= 2,k*xwraps modulo2^256on largex, so the result is guarded by an explicitx >= ceil(256/k) -> 0test rather than the shift primitive's own>= 256check (which would see the wrapped product); below that thresholdk*xis exact and< 256. EIP-160 dynamic gas depends only on the exponent and is charged by the unchanged general path, so gas is byte-for-byte identical.Tests
New differential test
evm_exp_strength_tests.cpp: the variable operand is fed viaCALLDATALOADso the analyzer cannot fold it and the fast path is taken. Output is checked against the interpreter and against an independent bit-placement1 << xtable (the interpreter'sintx::expfolds power-of-two bases to the same shift, so the table is an independent reference). Coverage: bases2through2^255, exponents straddling eachk*x = 256threshold, an overflow-guard case (base256,x = 2^253wraps8*xto 0), and offset-free EIP-160 gas deltas across Cancun and pre-Spurious-Dragon.Full suite: 223 multipass unit tests, 2723 state tests (
-k fork_Cancun), 6 EXP tests,tools/format.sh checkclean, 0 new compiler warnings.Performance
EXP is not present in the standard evmone benchmark suite. On a targeted synthetic benchmark (50x
EXP(2^k, x)on a dynamic exponent), the new path is constant-time (~7 ns/op) versus a square-and-multiply loop that scales with exponent byte width: per-execution speedup 3.3x–149x depending on exponent magnitude. Aggregate effect on real workloads is small (EXP is ~0.12% of opcodes on a mainnet sample, and most is already double-constant-folded).🤖 Generated with Claude Code