argotorg / argotorg/solidity

Peephole Optimization: Simplify Function Selector Guard (PUSH1 4 / CALLDATASIZE / LT / ISZERO) → (PUSH1 3 / CALLDATASIZE / GT)

Open
#16,316 0 comments 1 reaction 0 assignees View on GitHub
feature
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 of CALLDATASIZE.




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:


PUSH1 0x04
CALLDATASIZE
LT
ISZERO
PUSH <tag>
JUMPI

This can be equivalently rewritten as:


PUSH1 0x03
CALLDATASIZE
GT
PUSH <tag>
JUMPI

because for all natural numbers cds:


!(cds < 4) ≡ (cds ≥ 4) ≡ (cds > 3)

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:


b_before = !(cds < 4)

After:


b_after = (cds > 3)

Prove:


cds ℕ: !(cds < 4) = (cds > 3)

This has also been expressed in SMT form, where a Z3 model search for a counterexample is unsatisfiable:


(assert (>= cds 0))
(assert (not (= (ite (< cds 4) 0 1)
(ite (> cds 3) 1 0))))
(check-sat) ; returns "unsat"

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

Open the contributing 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

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.