Peephole Optimization: Simplify Function Selector Guard (PUSH1 4 / CALLDATASIZE / LT / ISZERO) → (PUSH1 3 / CALLDATASIZE / GT)
- Dominant language
- C++
- Stars
- 25.7k
- Forks
- 6.2k
- Avg merge
- 2d 19h
- Merged PRs (30d)
- 29
Description
Summary
This issue proposes adding a small but provably-safe peephole optimization to the Solidity EVM backend. The optimization rewrites the common function selector guard (checking for CALLDATASIZE ≥ 4) into a shorter and cheaper equivalent sequence.
This optimization:
-
Saves 1 byte of runtime bytecode (≈ 200 gas deploy-time).
-
Saves 2 gas per external call (measured using evmone).
-
Is proven semantically equivalent to the original code for all possible calldata sizes.
-
Applies only in the function dispatcher immediately following a
Tag/JUMPDEST, so it does not interact with ABI decoding or other uses ofCALLDATASIZE.
A PR implementing this change is available here:
👉[PR #16315]
Motivation
Every Solidity-generated dispatcher performs a check that CALLDATASIZE ≥ 4 before reading the 4-byte function selector.
The canonical pattern emitted by the compiler is:
This can be equivalently rewritten as:
because for all natural numbers cds:
The new sequence is shorter (4 bytes instead of 5) and avoids an ISZERO.
Bytecode and Gas Improvement
Metric | Before | After | Improvement
-- | -- | -- | --
Guard byte length | 5 bytes | 4 bytes | −1 byte
Deployment gas | — | — | ~200 gas saved per guard
Execution gas | 11 gas | 8 gas | 3 gas theoretical, 2 gas measured
Measured execution was performed using evmone on a minimal contract with a single external entry point.
Formal Equivalence
A formal equivalence argument is included in the associated PR.
Summary:
Let cds = CALLDATASIZE ≥ 0.
Before:
After:
Prove:
This has also been expressed in SMT form, where a Z3 model search for a counterexample is unsatisfiable:
Thus the transformation is semantics-preserving.
Safety Conditions
To avoid rewriting unrelated code, the peephole optimization is only applied when:
-
The sequence appears immediately after a
Tag/JUMPDEST, -
The constant is exactly PUSH1 4,
-
The
<tag>operand is a PushTag, -
The branch is a JUMPI,
-
No intermediate instructions modify the stack in a way incompatible with the canonical selector guard.
This prevents optimizations of ABI decoding paths such as string/bytes length checks, which often use similar patterns but require different semantics.
Request
I propose integrating this optimization into the Solidity backend.
The PR linked below contains:
-
Implementation in
PeepholeOptimiser.cpp -
A formal explanation of correctness
-
Unit tests
-
Demonstrations of gas and size savings
👉 [PR #16315]
Feedback from the maintainers on design, safety constraints, and test coverage would be very welcome.
Contributor guide
Research direction
Start by reviewing PeepholeOptimiser.cpp and the associated PR #16315, then inspect its unit tests and the existing dispatcher peephole patterns. Done means integrating the guarded selector transformation, preserving the stated safety conditions, and providing unit-test coverage plus the described bytecode and gas measurements.
Written by the indexing model from the issue text.
Assessment
- Tech stack
- cpp, solidity
- Domain
- compilers
- Issue type
- Feature
- Difficulty
- 4/5
- Estimated time
- 3-5 days
- Activity status
- Stale
- Clarity
- Clearly specified
- Newbie friendliness
- 25/100