Skip to content

Rewrite constant optimizer#16788

Open
moh-eulith wants to merge 5 commits into
argotorg:developfrom
moh-eulith:moh-new-const-opt
Open

Rewrite constant optimizer#16788
moh-eulith wants to merge 5 commits into
argotorg:developfrom
moh-eulith:moh-new-const-opt

Conversation

@moh-eulith

Copy link
Copy Markdown

Description

Supersedes #15935 and #16729. Offers an alternative to #16738 (see comments below for details)

The libevmasm constant optimizer has been restructured to use several simple primitive rewrites recursively.
This provides:

  • cleaner design
  • easier extensibility
  • removal of expensive/ineffective operations (mul,exp,add)

The yul constant optimizer has been significantly simplified (see the next comment for some history).

The constant optimizer has a higher effect with lower runs, which is rare in the codebase's tests. It tends to be more effective in real world contracts than tiny little test contracts. A high runs value disables it (as before).

I'll add a changelog if we get consensus on this approach.

Checklist

AI Disclosure

  • No AI tools were used

@moh-eulith

moh-eulith commented Jun 9, 2026

Copy link
Copy Markdown
Author

I tried removing the yul constant optimizer, but it caused a big regression in some real world contracts. I dug into some of the effects, but I can't provide a succinct description of why this optimization step makes a difference. It seems to revolve around the vagaries of stack scheduling/manipulation.

What I did notice from starring at differences is that the most cases are actually the trivial ones involving NOT and shifts. So I compromised: a much simpler yul optimizer that is essentially trivial to reason about and maintain.

Some notes that might help:
Yul code generates large constants in few instances. These include:

  • bit manipulation: to replace the last 8 bits of A with the bits of B, the code does: (A & ~0xff) | (B & 0xff).
  • sub rewrite: some optimization step seems to rewrite subs of constants with adds: sub(a, 1) -> add(a, ~0)

These types of things cause the stack to potentially be organized differently (e.g. push 0xff dup, instead of push 0xff push 0xffffff..ff00).

@moh-eulith

moh-eulith commented Jun 9, 2026

Copy link
Copy Markdown
Author

I'd appreciate a bit of help with the test failures:

t_ubu_force_release_soltest_all

This test (SolidityOptimizer.cpp:425) expects this constant: 0x77abc0000000000000000000000000000000000000000000000000000000001
to appear directly in the output, but the new algorithm successfully makes a smaller one.
How should the test be modified?

ci/circleci: t_native_test_ext_uniswap
Not sure what this is about

@moh-eulith

Copy link
Copy Markdown
Author

t_ubu_force_release_soltest_all

This test (SolidityOptimizer.cpp:425) expects this constant: 0x77abc0000000000000000000000000000000000000000000000000000000001
to appear directly in the output, but the new algorithm successfully makes a smaller one.
How should the test be modified?

I removed the check in the test and added that constant to the asm test instead.

@moh-eulith

Copy link
Copy Markdown
Author

ci/circleci: t_native_test_ext_uniswap
Not sure what this is about

passed on second run

@moh-eulith

moh-eulith commented Jun 10, 2026

Copy link
Copy Markdown
Author

Benchmark results (updated as of 2026-06-26):

ir-no-optimize

project bytecode_size deployment_gas method_gas
brink -0.05% ✅
colony 0%
elementfi -0.15% ✅
ens -0.13% ✅
euler
gnosis
gp2 -0.11% ✅
pool-together -0.16% ✅
uniswap -0.38% ✅ -0.35% ✅ -0.28% ✅
yield_liquidator -0.32% ✅ -0.33% ✅ -0.04% ✅
zeppelin

ir-optimize-evm+yul

project bytecode_size deployment_gas method_gas
brink -2.08% ✅
colony -2.45% ✅
elementfi -1.74% ✅
ens -2.37% ✅ -2.82% ✅ -0.07% ✅
euler -3.46% ✅ -3.52% ✅ -0.49% ✅
gnosis
gp2 -2.06% ✅
pool-together -2.63% ✅
uniswap -3.16% ✅ -3.04% ✅ -2.87% ✅
yield_liquidator -3.7% ✅ -3.81% ✅ -0.08% ✅
zeppelin -2.62% ✅ -2.42% ✅ -1% ✅

ir-optimize-evm-only

project bytecode_size deployment_gas method_gas
brink -1.68% ✅
colony -2.11% ✅
elementfi -0.99% ✅
ens -1.45% ✅ -1.02% ✅ -0.19% ✅
euler
gnosis
gp2 -1.1% ✅
pool-together -1.4% ✅
uniswap -1.72% ✅ -1.69% ✅ -1.51% ✅
yield_liquidator -2.02% ✅ -2.01% ✅ -0.27% ✅
zeppelin -1.58% ✅

legacy-no-optimize

project bytecode_size deployment_gas method_gas
brink 0%
colony 0%
elementfi 0%
ens 0%
euler 0% +0% +0%
gnosis 0%
gp2 0%
pool-together 0%
uniswap 0% -0% +0%
yield_liquidator 0% +0% 0%
zeppelin

legacy-optimize-evm+yul

project bytecode_size deployment_gas method_gas
brink -2.86% ✅
colony -2.45% ✅
elementfi -1.78% ✅
ens -2.12% ✅ -2.43% ✅ -0.08% ✅
euler -2.71% ✅ -2.77% ✅ -0.68% ✅
gnosis -2.59% ✅
gp2 -2.01% ✅
pool-together -2.66% ✅
uniswap -3.08% ✅ -2.89% ✅ -2.77% ✅
yield_liquidator -3.7% ✅ -3.67% ✅ -0.15% ✅
zeppelin -2.57% ✅ -2.2% ✅ -1.37% ✅

legacy-optimize-evm-only

project bytecode_size deployment_gas method_gas
brink -2.84% ✅
colony -2.11% ✅
elementfi -1.51% ✅
ens -1.86% ✅ -2.1% ✅ -0.08% ✅
euler -2.32% ✅ -2.38% ✅ -0.64% ✅
gnosis -2.11% ✅
gp2 -1.61% ✅
pool-together -2.27% ✅
uniswap -2.76% ✅ -2.54% ✅ -2.53% ✅
yield_liquidator -3.16% ✅ -3.15% ✅ -0.15% ✅
zeppelin

@blishko blishko left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hi @moh-eulith, thanks for pushing this topic. We will have onsite next week where we plan to discuss how to deal with constant optimization. We have several proposals already and have to decide on one. I will make aa thorough review after that!

Comment on lines -61 to -66
std::optional<BuiltinHandle> add;
std::optional<BuiltinHandle> exp;
std::optional<BuiltinHandle> mul;
std::optional<BuiltinHandle> not_;
std::optional<BuiltinHandle> shl;
std::optional<BuiltinHandle> sub;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There is no need to remove these, no?
It's interesting that they were not used outside of constant optimiser, but I suggest to keep these around.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Just cleaning up. Happy to revert that bit.

@moh-eulith

Copy link
Copy Markdown
Author

I will make aa thorough review after that!

Sorry, the diff is not easy to look at because it's a rewrite. I tried to make it clean, so I hope looking at the code by itself makes sense.

@DanielVF

Copy link
Copy Markdown
Contributor

I'll look at this tomorrow.

@DanielVF

Copy link
Copy Markdown
Contributor

I like the modularity of the different solutions.

However, from a cost benefit point of view, there looks to be very little real world extra benefit to be worth adding in so many different methods of constant generation as well as recursion.

@moh-eulith

Copy link
Copy Markdown
Author

there looks to be very little real world extra benefit to be worth adding in so many different methods of constant generation as well as recursion.

The old code was recursive, so that's not new. The new code is a series of simpler functions that reproduce the same results, so it's just cleaner/simpler. The idea is that we get complex behavior from simple combinations.

  • Compare one block of code counting zeros and ones and then trying to find a mask via many conditionals that look like magic with the new two simpler functions that find more patterns (e.g. a shift left of a constant).
  • Compare the large/recursive/max compute bound loop before with the simpler functions that create the same constants with a bound on recursion.

@moh-eulith

Copy link
Copy Markdown
Author

from a cost benefit point of view

Maybe I misunderstood what you meant by cost. If you mean code size, that hasn't changed much. I'd argue it's much simpler now to reason about, so it has lower (mental) "cost".

If you mean runtime performance, I can't seem to measure any difference on my setup (work laptop running a bunch of stuff all the time). I'd welcome a more precise measurement.

@DanielVF

Copy link
Copy Markdown
Contributor

At least in my measuring, this PR makes a fairly dramatic compilation time slowdown.

contract (no via-ir) (via-ir)
WETH9 +88.92% ❌ +62.15% ❌
ENSRegistryWithFallback +184.38% ❌ +49.03% ❌
TetherToken +97.38% ❌ +24.06% ❌
OETH +174.71% ❌ +33.87% ❌
BoredApeYachtClub +105.36% ❌ +48.32% ❌
RocketDepositPool +115.62% ❌ +31.09% ❌
Poap +83.97% ❌ +18.28% ❌
FiatTokenV2_2 +205.98% ❌ +51.78% ❌
Morpho +84.31% ❌ +17.68% ❌
AccessControlledOCR2Aggregator +130.23% ❌ +27.80% ❌
PoolManager +46.14% ❌ +10.45% ❌
EulerSwap +31.06% ❌ +7.94% ❌
EtherfiLiquidityPool +86.38% ❌ +21.28% ❌
AaveV4HubInstance +61.43% ❌ +12.38% ❌
UniswapV3Pool +30.00% ❌ +3.45% ❌
Total +106.17% +24.08%

Looking at the FiatTokenV2_2 contract with --via-ir and --optimize in a profiler, the region in blue is the non-yul constant optimizer.

image

If we compile with just --optimize:

image

@moh-eulith

Copy link
Copy Markdown
Author

At least in my measuring, this PR makes a fairly dramatic compilation time slowdown.

Thanks for doing this. Obviously, the results are not good. I'll have a look. Could you point me to the sources for FiatTokenV2_2 or any other one that's easy to compile?

@DanielVF

Copy link
Copy Markdown
Contributor

Sure! This is the USDC implementation contract ported to compile on the current version of solidity. Contract here

@moh-eulith

Copy link
Copy Markdown
Author

@DanielVF I added a quick filter to optimize the search. It's still hard for me to benchmark this (thermal throttling), so I'd appreciate another run if you have the time. I believe it is now within 5-10% for the worst cases.

@moh-eulith
moh-eulith force-pushed the moh-new-const-opt branch 2 times, most recently from 6f63253 to c5d5e53 Compare June 16, 2026 18:25
@DanielVF

DanielVF commented Jun 22, 2026

Copy link
Copy Markdown
Contributor
contract (no via-ir) (via-ir)
WETH9 -0.08% ✅ +1.36% ❌
ENSRegistryWithFallback +8.38% ❌ +1.47% ❌
TetherToken +0.11% ❌ +1.40% ❌
OETH +19.22% ❌ +4.27% ❌
BoredApeYachtClub +7.14% ❌ +1.33% ❌
RocketDepositPool +11.27% ❌ +1.75% ❌
Poap +3.02% ❌ +0.36% ❌
FiatTokenV2_2 +2.74% ❌ +1.19% ❌
Morpho +5.87% ❌ +1.15% ❌
AccessControlledOCR2Aggregator +14.49% ❌ +3.39% ❌
PoolManager +0.83% ❌ -0.66% ✅
EulerSwap +1.12% ❌ +0.15% ❌
EtherfiLiquidityPool +0.95% ❌ +1.87% ❌
AaveV4HubInstance +2.02% ❌ +0.93% ❌
UniswapV3Pool +2.67% ❌ +0.95% ❌
Total +4.84% ❌ +1.50% ❌

@moh-eulith

Copy link
Copy Markdown
Author

@DanielVF thanks for the run.
I just looked at OETH and RocketDepositPool, averaging around 0.25 and 0.2 s per compile each; too tiny to bother?

AccessControlledOCR2Aggregator is bigger; I'll take a closer look there.

@DanielVF

DanielVF commented Jun 22, 2026

Copy link
Copy Markdown
Contributor

Speaking only for my own taste, I would be sad about overall compiles take 2%-5% longer from this change - the constant optimizer shouldn't require a lot of computation compared with other things the overall optimization pipeline needs to do. My guess is that it should be possible to result in the same optimized constants for the most common cases with less of a CPU hit.

(I'm not a part of the solidity team, I just want to see the compiler get faster and make better bytecode)

@moh-eulith

Copy link
Copy Markdown
Author

I would be sad about overall compiles take 2%-5% longer from this change

I pushed a change that seems to be on par now. If you have a chance, please test.

@blishko

blishko commented Jun 23, 2026

Copy link
Copy Markdown
Contributor

Hi @moh-eulith , @DanielVF! Thanks for all the effort you are putting into improving solc!
We have discussed this topic at our onsite last week and deciding on the following course of action.
We need to step back a little and try to converge on the desired design first. PRs are great for demonstrating PoCs and evaluating the impact, but this should be only the second step, once the design is clear.
I have created a new issue to consolidate the discussion: #16814
My plan is to document how the current constant optimizer works there. Then, the proposed change should describe how is it changing that design and why is the proposed change beneficial.
Given such a design note, it would be much easier for us to evaluate whether or not such a change is desired and if so, to review the proposed implementation.

@DanielVF

Copy link
Copy Markdown
Contributor

It's clearly faster than it was.

contract solc-moh3 (no via-ir) solc-moh3 (via-ir)
WETH9 +1.79% ❌ -0.82% ✅
ENSRegistryWithFallback +3.69% ❌ -0.24% ✅
TetherToken +3.07% ❌ +0.75% ❌
OETH +9.17% ❌ +1.23% ❌
BoredApeYachtClub +3.41% ❌ +0.32% ❌
RocketDepositPool +3.34% ❌ -1.20% ✅
Poap +0.51% ❌ +0.41% ❌
FiatTokenV2_2 -1.05% ✅ -2.30% ✅
Morpho +3.13% ❌ -0.28% ✅
AccessControlledOCR2Aggregator +5.27% ❌ +2.52% ❌
PoolManager -0.30% ✅ -0.80% ✅
EulerSwap -1.39% ✅ +0.66% ❌
EtherfiLiquidityPool +0.73% ❌ -0.12% ✅
AaveV4HubInstance +2.06% ❌ -1.11% ✅
UniswapV3Pool +1.27% ❌ +0.43% ❌
Total +1.62% -0.02%

@DanielVF

Copy link
Copy Markdown
Contributor

The simple change, PR #16729 is current outperforming this PR on bytecode. Grabbed examples from WETH9:

  PUSH0 NOT PUSH1 0x60 SHR NOT AND
vs
  PUSH0 NOT PUSH1 0xa0 SHL AND
  PUSH4 0xffffffff PUSH1 0xe0 SHL
vs
  PUSH0 NOT PUSH1 0x20 SHR NOT
  PUSH0 NOT PUSH1 0x88 SHR NOT AND
vs
  PUSH0 NOT PUSH1 0x78 SHL AND

@moh-eulith

moh-eulith commented Jun 26, 2026

Copy link
Copy Markdown
Author

PR #16729 is current outperforming this PR on bytecode

Nice catch. They're all actually the same underlying cause. I was able to resolve them by adding a not(0) left shift generator. Interestingly, for bytecode size, neither solution was optimal for the middle case. It should be good now.

Looks like I need to regen the test results.

@clonker clonker removed their assignment Jun 30, 2026
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.

4 participants