Abstract
In Solidity, we can delegatecall public functions of a library to execute external library code, a pattern originally introduced to help contracts stay below the byte-size limit at deployment time. Unfortunately, the current compiler implementation defeats this purpose: on every delegatecall the generated bytecode embeds a redundant PUSH20 <LibraryAddress>
instruction—often longer than the library code itself. As a result, in many scenarios the delegatecall pattern simultaneously increases gas consumption and bytecode size. This is a problem that the compiler could solve through optimization.
Motivation
Consider the following example:
pragma solidity >=0.7.0 <0.9.0;
library Ballot {
function B() public { }
function C() public { }
}
contract A {
function C() public {
Ballot.B();
}
function D() public {
Ballot.C();
}
}
The bytecode of contract A
is roughly when Optimization set to 10000000
.
60806040..73__$17280f6b09cfc0597fda7b4937e1c059ab$__63c89e43..
73__$17280f6b09cfc0597fda7b4937e1c059ab$__63fe073d1160..
Because A.C()
and A.D()
each make a library call, we see two separate PUSH20 <LibraryAddress>
sequences in the bytecode. A feature meant to compress code actually injects sizeable, duplicate address literals every time the library is invoked. Hoisting this address into a shared basic block would eliminate the redundancy.
Most libraries function are far smaller than 20 bytes, yet each external call still emits a full PUSH20
. One maybe reason we misses this case is that its current equivalence proofs do not model PUSH
instructions even though tools like HEVM can already validate the equivalence.
Other small PUSH
-level optimizations that are likewise ignored today include, for example:
PUSH32 0x0 -> PUSH0
PUSH20 0xFFFF -> PUSH0 NOT SHR
Other PUSH <constant | immutable value> - > JUMPDST PUSH xx JUMP
...
We are actively exploring more such patterns like above PUSH case and hope we can contribute to the solidity
Hope further discussion on this topic.
Proposal
For the library delegatecall scenario, users care primarily about shrinking deployment bytecode, not marginal gas savings. We therefore propose that the compiler, by default, consolidate all identical PUSH20 <address>
instructions for a given library into a single basic block and reuse it across every call site. This would remove redundant code fragments and restore the original byte-size advantage of the library mechanism. On the other hand, the increased bytecode will actually lead to greater GAS consumption when deployed, which may require more detailed quantitative discussion.
PUSH20 Address
...
PUSH20 Address
TO
JUMPDEST
PUSH20 Address
JUMP
...
PUSH <ORIGINPC>
PUSH <PUSHADDREPC>
JUMP
...
PUSH <ORIGINPC>
PUSH <PUSHADDRE>
JUMP