Rewrite constant optimizer#16788
Conversation
|
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 Some notes that might help:
These types of things cause the stack to potentially be organized differently (e.g. push 0xff dup, instead of push 0xff push 0xffffff..ff00). |
|
I'd appreciate a bit of help with the test failures:
|
e7f0e73 to
8f01c14
Compare
I removed the check in the test and added that constant to the asm test instead. |
passed on second run |
|
Benchmark results (updated as of 2026-06-26):
|
| 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
left a comment
There was a problem hiding this comment.
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!
| std::optional<BuiltinHandle> add; | ||
| std::optional<BuiltinHandle> exp; | ||
| std::optional<BuiltinHandle> mul; | ||
| std::optional<BuiltinHandle> not_; | ||
| std::optional<BuiltinHandle> shl; | ||
| std::optional<BuiltinHandle> sub; |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
Just cleaning up. Happy to revert that bit.
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. |
|
I'll look at this tomorrow. |
|
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. |
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.
|
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. |
|
At least in my measuring, this PR makes a fairly dramatic compilation time slowdown.
Looking at the FiatTokenV2_2 contract with --via-ir and --optimize in a profiler, the region in blue is the non-yul constant optimizer. If we compile with just --optimize: |
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? |
|
Sure! This is the USDC implementation contract ported to compile on the current version of solidity. Contract here |
8f01c14 to
87af0af
Compare
|
@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. |
6f63253 to
c5d5e53
Compare
|
|
@DanielVF thanks for the run. AccessControlledOCR2Aggregator is bigger; I'll take a closer look there. |
c5d5e53 to
e7d6352
Compare
|
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) |
I pushed a change that seems to be on par now. If you have a chance, please test. |
|
Hi @moh-eulith , @DanielVF! Thanks for all the effort you are putting into improving |
|
It's clearly faster than it was.
|
|
The simple change, PR #16729 is current outperforming this PR on bytecode. Grabbed examples from WETH9: |
e7d6352 to
fa08e30
Compare
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. |
fa08e30 to
8d45ee5
Compare
8d45ee5 to
06efc1c
Compare


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:
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