-
Notifications
You must be signed in to change notification settings - Fork 5.9k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Yul builtin for MCOPY
#14779
Yul builtin for MCOPY
#14779
Conversation
e9cffac
to
de53ce8
Compare
test/libyul/yulOptimizerTests/unusedStoreEliminator/mcopy_different_areas_full_words.yul
Outdated
Show resolved
Hide resolved
// let _42 := 42 | ||
// mstore(_32, _42) | ||
// mcopy(_0, _32, _32) | ||
// mcopy(_0, _32, _32) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
EqualStoreEliminator
on the other hand seems to ignore mcopy
.
Just FYI: I also just looked through potential effects on the |
I also just looked through the code, to see if an |
Good point about memory guard tests. I haven't actually considered that. For now I added extensive tests for mcopy in unused store eliminator and equal store eliminator in isolation, but I guess I should also add at least some full suite tests to catch anything that might be done by other steps I did not consider. |
{ | ||
calldatacopy(0, 0, 0x40) | ||
|
||
let _0 := 0 | ||
let _32 := 32 | ||
let _42 := 42 | ||
let _123 := 123 | ||
|
||
// Sanity check: MCOPY does not affect storage | ||
sstore(_0, _42) // Redundant. SSTORE overwrites it. | ||
mcopy(_32, _0, _32) | ||
sstore(_0, _123) | ||
|
||
return(0, 0x40) | ||
} | ||
// ==== | ||
// EVMVersion: >=cancun | ||
// ---- | ||
// step: unusedStoreEliminator | ||
// | ||
// { | ||
// { | ||
// calldatacopy(0, 0, 0x40) | ||
// let _0 := 0 | ||
// let _32 := 32 | ||
// let _42 := 42 | ||
// let _123 := 123 | ||
// mcopy(_32, _0, _32) | ||
// sstore(_0, _123) | ||
// return(0, 0x40) | ||
// } | ||
// } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The annoying thing here is that I had to manually create variables for constants here despite the fact that we run ExpressionSplitter
and SSATransform
in the test framework just before testing the step. Without this, the first sstore
was not being removed and it makes me worried that it could happen UnusedStoreEliminator
tests I added (i.e. that stores would remain untouched not because behavior is correct but because the extra transformations prevent it).
The way it is now, the code gets transformed into this just before we run UnusedStoreEliminator
:
{
let _1 := 0x40
let _2 := 0
let _3 := 0
calldatacopy(_3, _2, _1)
let _0 := 0
let _32 := 32
let _42 := 42
let _123 := 123
sstore(_0, _42)
mcopy(_32, _0, _32)
sstore(_0, _123)
let _4 := 0x40
let _5 := 0
return(_5, _4)
}
and the first sstore
gets removed by the step.
but when the input looked like this:
...
sstore(0, 42)
mcopy(32, 0, 32)
sstore(0, 123)
...
it would instead end up like this before UnusedStoreEliminator
:
{
let _1 := 0x40
let _2 := 0
let _3 := 0
calldatacopy(_3, _2, _1)
let _4 := 42
let _5 := 0
sstore(_5, _4)
let _6 := 32
let _7 := 0
let _8 := 32
mcopy(_8, _7, _6)
let _9 := 123
let _10 := 0
sstore(_10, _9)
let _11 := 0x40
let _12 := 0
return(_12, _11)
}
and both sstore
s would remain.
Unfortunately this means that these tests ended up being more verbose than they could be otherwise.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Running CommonSubexpressionEliminator
before UnusedStoredEliminator
will get rid of the redundant sstore
by the way. Obviously, injecting this into the test suite will likely yield changes in a lot of tests, which is not ideal, but we do indeed run CSE multiple times before the UnusedStoreEliminator
. I'll try to explain, although take everything I say with a grain of salt until Daniel (or maybe even Chris) chime in. The following is the above Yul code transformed after ExpressionSplitter
and SSATransform
are run:
{
let _1 := 0x40
let _2 := 0
let _3 := 0
calldatacopy(_3, _2, _1)
let _4 := 42
let _5 := 0
sstore(_5, _4)
let _6 := 32
let _7 := 0
let _8 := 32
mcopy(_8, _7, _6)
let _9 := 123
let _10 := 0
sstore(_10, _9)
}
Only pay attention to _2
and the destination for sstore
writes, in the above case, _5
for the first one, and _10
for the second. After CommonSubexpressionEliminator
is run, we get the following:
{
let _1 := 0x40
let _2 := 0
let _3 := _2
calldatacopy(_2, _2, _1)
let _4 := 42
let _5 := _2
sstore(_2, _4)
let _6 := 32
let _7 := _2
let _8 := _6
mcopy(_6, _2, _6)
let _9 := 123
let _10 := _2
sstore(_2, _9)
}
Notice that destinations for both sstore
s have changed from _5
and _10
to _2
. UnusedStoreEliminator
checks whether all code paths revert in order to remove an sstore
(not relevant in this case), and whether all code paths lead to the first sstore
being overwritten by a subsequent one (definitely relevant in this case). As you can see (after running the CSE), the second sstore
will clearly overwrite the first sstore
(_2
), which is why it will then remove the first one as it's marked as redundant. This is not possible (in this case) without running the CSE, since without it, the UnusedStoreEliminator
has no way to know whether _5
and _10
are the same destination (technically it does, but that's CSE's job).
Having written this up, I'm now fairly surprised we're not running the CommonSubexpressionEliminator
for UnusedEliminatorTests
(nor are we suggesting it in the docs).
This is what the code looks like afterwards by the way:
{
let _1 := 0x40
let _2 := 0
let _3 := _2
calldatacopy(_2, _2, _1)
let _4 := 42
let _5 := _2
let _6 := 32
let _7 := _2
let _8 := _6
mcopy(_6, _2, _6)
let _9 := 123
let _10 := _2
sstore(_2, _9)
}
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Now that I think about it, the above could use at least the UnusedPruner
to clean it up a bit, so I guess the assumption for UnusedStoreEliminator
tests is that the provided code is already in a 'post CSE` form (which is a terrible assumption to be honest, since you have to do so manually as you've done here). All in all, it's gonna make review this incredibly annoying :)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
(which is a terrible assumption to be honest, since you have to do so manually as you've done here)
On the other hand these test suites are meant to test a single step. So cramming too many steps into it, especially the more complex ones, might not be a good idea. You're always risking that it will hide a bug. Not sure about CSE specifically - may or may not be acceptable. We already run quite a few others before and after it.
In any case, it sounds like CSE should indeed be listed in the docs as a soft dependency. We should confirm with Daniel it makes sense and add it.
All in all, it's gonna make review this incredibly annoying :)
I mean, it's mostly annoying due to the extra verbosity. The test themselves are still pretty simple and you can pretty much ignore the declarations when reading since the names reflect the values.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
On the other hand these test suites are meant to test a single step. So cramming too many steps into it, especially the more complex ones, might not be a good idea. You're always risking that it will hide a bug. Not sure about CSE specifically - may or may not be acceptable. We already run quite a few others before and after it.
Yup, that's why left the second comment after giving it some thought.
In any case, it sounds like CSE should indeed be listed in the docs as a soft dependency. We should confirm with Daniel it makes sense and add it.
Yeah, or maybe we can get some input from @chriseth if he's not off as well?
We'll probably end up having more behaviour implicitly covered by tests than we had testing these components in total before ;-). Meaning: more tests won't hurt, but we also need to be mindful that a lot of this cannot be exhaustively tested, so we have to additionally rely on fuzzing and code path review as well. |
As for code path review: I haven't found anything in the Yul optimizer yet. |
These tests are still not even close to being exhaustive. I did try to err on the side of including too many tests rather than missing an important one, but I still ended up dropping like 2/3 of those I originally wanted to add. For example I dropped most of the tests checking the removal of It's hard to get a good balance though. I may have gone a bit overboard, but I really didn't want to risk adding too few and causing another important bug in unused store remover. The fact that I originally broke it in the PR made me really wary of this. Also I missed that overlap problem in the interpreter - in that case I assumed that a single test would be enough and I was wrong :) |
3f47c2d
to
5f5be2d
Compare
function expansion_on_write_only() public returns (uint newMsize) { | ||
assembly { | ||
mcopy(0xfff0, 0, 1) | ||
newMsize := msize() | ||
} | ||
} | ||
|
||
function expansion_on_read_only() public returns (uint newMsize) { | ||
assembly { | ||
mcopy(0, 0xfff0, 1) | ||
newMsize := msize() | ||
} | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Not sure what to do about this test. It won't pass soltest_all
because you cannot use msize()
with optimizer enabled. I could easily add an option to skip a semantic test when optimizer is enabled, but the fact that we don't have such an obvious option already makes me think that there could be some reason behind it.
On the other hand, this is really testing evmone's behavior rather than anything substantial in our implementation so maybe we're fine without it. I have gas tests, which check this indirectly, by comparing the cost of memory expansion so it is covered in that way already. I did like this one better though because it actually shows in which cases expansion happens.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
For now I added a fixup that removes the test.
)"; | ||
testCreationTimeGas(sourceCode); | ||
testRunTimeGas("no_overlap()", {encodeArgs()}); | ||
testRunTimeGas("overlap_right()", {encodeArgs()}); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think we should have this kind of test for all the Cancun opcodes - BLOBBASEFEE
, BLOBHASH
. TSTORE
, TLOAD
. The GasMeter
changes in other PRs are simple, so should be ok, but still, we don't have any coverage for them otherwise.
All the changes are done now. This can be now reviewed. Not marking as reviewable yet only because it depends on #14790. Also, I need to deal with the failing test (#14779 (comment)), but that will not really affect the PR in a significant way. |
2222ea4
to
b26d5fd
Compare
b26d5fd
to
7f79cd8
Compare
test/libyul/yulOptimizerTests/unusedStoreEliminator/mcopy_overwriting_mcopy.yul
Show resolved
Hide resolved
test/libyul/yulOptimizerTests/unusedStoreEliminator/mstore_overwriting_mcopy.yul
Show resolved
Hide resolved
17d1bf7
to
4b1535c
Compare
test/libsolidity/memoryGuardTests/unmarked_with_memory_access_mstore.sol
Outdated
Show resolved
Hide resolved
test/libsolidity/memoryGuardTests/unmarked_with_memory_access_mstore.sol
Outdated
Show resolved
Hide resolved
test/libyul/yulOptimizerTests/equalStoreEliminator/mcopy_same_read_area_mstore8_mstore8.yul
Show resolved
Hide resolved
test/libyul/yulOptimizerTests/equalStoreEliminator/mcopy_same_read_area_mstore_mstore.yul
Outdated
Show resolved
Hide resolved
test/libyul/yulOptimizerTests/equalStoreEliminator/mcopy_same_write_area_mcopy.yul
Show resolved
Hide resolved
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I've gone over everything, and would feel comfortable approving, but it would be nice for someone else to double check the gas and semantic tests (especially the ones with a lot of copying and memory dumps involved). The optimizer tests I've gone over in detail and they look good (or at least as good as they can in the current state, i.e. without MCOPY not being a removal candidate).
{ | ||
calldatacopy(0, 0, 0x20) | ||
|
||
mcopy(0, 0, 0x20) // Redundant. Does not change any values (only affects MSIZE). |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
If it affects MSIZE, is this then not a candidate for removal?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Honestly, I haven't verified this so I could be wrong, but I assume it still is as long as your code does not use msize
(and using that basically disables the optimizer). If you're not using msize
then changing how much the memory is expanded only really changes gas usage (and potentially whether you contract will revert with out of gas or not). These are fair game for optimization.
Still, in this example the mcopy
instruction does not really affect msize
since calldatacopy
already expands memory. The comment was more to indicate that it is the only thing mcopy
of this form could affect. I'll remove that bit to avoid confusion.
First part of #14741.
Depends on #14790.Still to do:
RedundantAssignEliminator
wrongly removingMCOPY
in some cases.RedundantAssignEliminator
EqualStoreEliminator
yulInterpreterTest
forMCOPY
memory expansion.