Skip to content

Redundant store eliminator.#11352

Closed
chriseth wants to merge 11 commits into
developfrom
redundantStoreEliminator
Closed

Redundant store eliminator.#11352
chriseth wants to merge 11 commits into
developfrom
redundantStoreEliminator

Conversation

@chriseth

@chriseth chriseth commented May 5, 2021

Copy link
Copy Markdown
Contributor

No description provided.

Comment thread test/libyul/yulOptimizerTests/redundantStoreEliminator/remove_before_revert.yul Outdated
Comment thread test/libyul/yulOptimizerTests/redundantStoreEliminator/function_end.yul Outdated
@chriseth chriseth force-pushed the redundantStoreEliminator branch 2 times, most recently from fd742a6 to 4231df3 Compare May 6, 2021 14:24

// TODO handle reverts of user-defined functions

// TODO is this all correct WRT termination side-effects of function

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Probably not important because all we care about are ExpressionStatements.

object "C_12" {
code {
{
mstore(64, 128)

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

This removal is what most of the gas savings are about...

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Will that still work with enabled stack-limit-evasion, with which this will have to become mstore(64, memoryguard(128)) and be propagated properly?

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.

If we replace mstore(64, 128) with mstore(64, memoryguard(128)), then none of the mload(64) would be resolved. Which in turn means that this cannot be removed. I think we cannot use memory optimizations if we need to use stack limit evader.

Comment thread test/cmdlineTests/yul_stack_opt/output Outdated
code {
{
let _1 := 1
sstore(_1, _1)

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

shrugs

@chriseth chriseth force-pushed the redundantStoreEliminator branch from 4231df3 to 76dfd1d Compare May 6, 2021 14:33
@ekpyron ekpyron self-requested a review May 6, 2021 14:35
@chriseth chriseth force-pushed the redundantStoreEliminator branch 2 times, most recently from ff59f4e to 799d907 Compare May 10, 2021 11:51
@chriseth

Copy link
Copy Markdown
Contributor Author

For this code, it would be very useful to somehow combine SideEffects and ControlFlowSideEffects, or at least make ControlFlowSideEffects available for user-defined functions instead of just for builtins.

@chriseth chriseth force-pushed the redundantStoreEliminator branch from 799d907 to 49df2f2 Compare May 10, 2021 16:16

@cameel cameel left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Just a few initial remarks. I'm planning to do a full review later.

Comment on lines +93 to +101
inline bool operator==(State _other) const { return m_value == _other.m_value; }
inline bool operator!=(State _other) const { return !operator==(_other); }
static inline void join(State& _a, State const& _b)

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Does inline make any difference here? These should be considred inline by default.

Suggested change
inline bool operator==(State _other) const { return m_value == _other.m_value; }
inline bool operator!=(State _other) const { return !operator==(_other); }
static inline void join(State& _a, State const& _b)
bool operator==(State _other) const { return m_value == _other.m_value; }
bool operator!=(State _other) const { return !operator==(_other); }
static void join(State& _a, State const& _b)

static inline void join(State& _a, State const& _b)
{
// Using "max" works here because of the order of the values in the enum.
_a.m_value = Value(std::max(int(_a.m_value), int(_b.m_value)));

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Suggested change
_a.m_value = Value(std::max(int(_a.m_value), int(_b.m_value)));
_a.m_value = Value(std::max(int(_a.m_value), int(_b.m_value)));

Comment on lines +106 to +105
/// Joins the assignment mapping of @a _source into @a _target according to the rules laid out
/// above.
/// Will destroy @a _source.
static void merge(TrackedStores& _target, TrackedStores&& _source);
static void merge(TrackedStores& _target, std::vector<TrackedStores>&& _source);

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Which rules is this referring to? Was RedundantStoreEliminator meant to have a docstring describing them?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

This is copied from redundant assign eliminator. I guess it is documented there. I still need to combine the code.

Comment thread libyul/optimiser/RedundantStoreEliminator.cpp Outdated
Comment thread libyul/optimiser/RedundantStoreEliminator.cpp Outdated
Comment thread libyul/optimiser/RedundantStoreEliminator.cpp Outdated
Comment thread libyul/optimiser/RedundantStoreEliminator.cpp Outdated
Comment thread libyul/optimiser/RedundantStoreEliminator.cpp Outdated
return false;
if (_covered.start && _covered.length && _covered.start == _covering.start && _covered.length == _covering.length)
return true;
if (_covered.location == Location::Storage && _covered.start && _covering.start && *_covered.start == *_covering.start)

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.

Why not use knowledge base here to check equality?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Because the CSE will have done the work for us already.

Comment thread test/cmdlineTests/standard_ewasm_requested/output.json
Comment thread libyul/optimiser/RedundantStoreEliminator.cpp Outdated
Comment on lines +492 to +496
if (optional<u256> startDiff = knowledge.differenceIsKnownConstant(*_covered.start, *_covering.start))
if (optional<u256> lengthDiff = knowledge.differenceIsKnownConstant(*_covering.length, *_covered.length))
if (*startDiff <= *lengthDiff && *startDiff <= largestPositive && *lengthDiff <= largestPositive)
return true;

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.

Maybe you can add a test that would lead to an incorrect removal and mention EIP-1985?

{
    let x := 0

    calldatacopy(0, 0, 115792089237316195423570985008687907853269984665640564039457584007913129639935)
    mstore(x, 20)
    return(0, 32)
}
// ----
// step: redundantStoreEliminator
//
// {
//     let x := 0
//     let _1 := 115792089237316195423570985008687907853269984665640564039457584007913129639935
//     let _2 := 0
//     let _3 := 0
//     mstore(x, 20)
//     return(0, 32)
// }

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Interesting! So it seems there is a bug in my code?

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.

I think it can have bugs like these when writing to locations beyond 2**128 - 1. This is a grey area because of EIP-1985.

@chriseth chriseth force-pushed the redundantStoreEliminator branch from 49df2f2 to a86689a Compare May 26, 2021 14:30
@chriseth

Copy link
Copy Markdown
Contributor Author

Rebased and added a feature to only ignore memory if msize is found.

@axic

axic commented May 26, 2021

Copy link
Copy Markdown
Contributor

Rebased

You surely didn't rebase it to the head given all these conflicts displayed 😅

@chriseth

Copy link
Copy Markdown
Contributor Author

Now rebased.

@chriseth chriseth force-pushed the redundantStoreEliminator branch 2 times, most recently from ef143c8 to 0a57b2a Compare May 27, 2021 09:22
@chriseth chriseth force-pushed the redundantStoreEliminator branch 2 times, most recently from 26c80b8 to b2d5289 Compare August 9, 2021 19:08
@chriseth chriseth force-pushed the redundantStoreEliminator branch from 792e9f0 to 573d3de Compare August 11, 2021 16:59
@chriseth chriseth changed the base branch from develop to refactorRedundantAssign August 11, 2021 16:59
@chriseth

Copy link
Copy Markdown
Contributor Author

Rebased this on top of the refactor.

@chriseth chriseth force-pushed the redundantStoreEliminator branch 2 times, most recently from 9758f5b to f9bc427 Compare August 12, 2021 09:17
// sstore(0, length)
// mstore(64, newFreePtr)
// mstore(memPtr, length)
// extcodecopy(value, add(memPtr, 32), 0, length)

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

:(

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

I hope we can prove memPtr + 32 <= memPtr + 32 with the linear solver soon...

optional<u256> startDiff = knowledge.differenceIfKnownConstant(*_covered.start, *_covering.start);
optional<u256> coveredLength = knowledge.valueIfKnownConstant(*_covered.length);
optional<u256> lengthDiff = knowledge.differenceIfKnownConstant(*_covering.length, *_covered.length);
if (

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

There is no proof for this yet.

@chriseth

chriseth commented Jan 6, 2022

Copy link
Copy Markdown
Contributor Author

Note that this step is less powerful than it could be due to the following reason:
Memory stores at the end of a function cannot be removed because we don't analyze the context in which the function is used so we mark all memory at the end of a function as potentially used.
What could improve this would be if we pull the return or stop instruction at the end of an external or public function into the function itself. Currently it is inside the dispatch routine and functions will likely not be inlined into the dispatch routine. It could help to create another function that is called from the dispatch routine and has

  • abi decode
  • call internal part of function
  • abi encode and return

This would also more nicely structure the code.

if (*length2 <= *diff && *length2 <= largestPositive && *diff <= largestPositive)
return true;
}
// We use diff >= _op1.length && diff <= 2**256 - _op2.length

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

This is so annoying. This is not enough to remove the mstore in

mstore(0, 0)
let length := calldataload(0)
calldatacopy(0x20, 0x20, length)
return(0x20, length)

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Just to check that I understand this thing correctly (which I might not yet)... what currently happens is that both the mstore and the calldatacopy are Undecided when we hit the return and then the return does not know that it won't read from 0 because of the potential overflow?

Couldn't we when hitting the return walk the previous memory writes in backwards order and check for knownCovered writes and if we find one stop?

I.e. return(0x20, length) would first look at calldatacopy(0x20, 0x20, length), realize that its entire read is covered by that write and be satisfied and not even consider earlier writes, i.e. not even consider the mstore(0,0)...
Then the mstore(0,0) would remain Undecided and be removed, right?

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

I mean: the reason that we can remove mstore(0,0) is not that the return won't read from offset 0, but that it will only read what calldatacopy wrote, isn't it?

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

The order of writes is not preserved in m_stores, though, is it? Hm...

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

The reason is that knownUnrelated does not return true for the mstore(0, 0) and return(0x20, length).

@ekpyron ekpyron Jan 6, 2022

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

That's one reason why we can remove mstore(0,0)... the other reason is that the memory read by return(0x20, length) is fully covered by calldatacopy(0x20, 0x20, length), so no earlier writes are relevant.

Ideally we'd be able to exploit both of those reasons...

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

If you had mstore(0x20, 0) instead, then return(0x20, length) would not be unrelated, but the mstore could still be removed.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

I could actually imagine that that reason is the more useful one in general, since we'll probably rarely just read random stuff that wasn't written specifically... so in general, I think it will more often be the case that something only reads stuff covered by earlier writes rather than that the locations are known to be disjoint...

In general, though, we would have to track this across multiple writes - it's just a coincidence that it's a single write that covers the entire read in this example... i.e. in

mstore(y, 0x42)
mstore(x, 0x20)
mstore(add(x, 0x20), 0x20)
return(x, 0x40)

I'd ideally want to recognize that the last two mstores together cover everything that's read by the return and be able to remove the first mstore without any knowledge about x or y... but yeah, just ideally, this may be infeasible to do...

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Well, actually, probably not infeasible... but at least tricky :-).

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Note from call: maybe it's worth differentiating between diff being positive or negative. So with an s256 diff, it may be

if (diff >= 0 && diff >= length1)
    return true;
if (diff < 0 && -diff >= length2)
    return true;

But at least I haven't convinced myself yet that there's no other weird overflow cases to consider.

Comment on lines +144 to +149
*instruction == Instruction::EXTCODECOPY ||
*instruction == Instruction::CODECOPY ||
*instruction == Instruction::CALLDATACOPY ||
*instruction == Instruction::RETURNDATACOPY ||
*instruction == Instruction::MSTORE ||
*instruction == Instruction::MSTORE8

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Don't we have some abstract semantic property to cover this? Hm, in those "writing to memory" includes the call opcodes...

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

You are right, even if we don't have such a property yet, we should create it. "No side effects apart from writing to memory".

scheduleUnusedForDeletion();
}

vector<UnusedStoreEliminator::Operation> UnusedStoreEliminator::operationsFromFunctionCall(

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

I wonder whether we should move the main part of this to some abstract semantic property as well.
I.e. just have the builtins tell us "I'm reading the memory range [arg0, arg1)" instead of just "I'm reading memory".

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

It might be useful to have that information elsewhere and also if e.g. there's ever a new EVM opcode that accesses memory or storage, we'll definitely define its semantic properties, but it's much easier to miss editing stuff elsewhere like here.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Yep, true! We could use that routine in/as replacement for isSimpleLoad / isSimpleStore

// sstore(not(gcd(_3, _2)), _1)
// sstore(2, _1)
// extcodecopy(_1, msize(), _1, _1)
// sstore(0, 0)

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

I have NO idea why this was missing before - maybe there is a really bad bug lurking here somewhere...

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

That's really a bit weird...

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Or wait... did "before" have the equal store eliminator merged :-)? If so, then that's the reason I guess.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Yeah, I guess that's it. And the equal store eliminator removes the second copy, while the redundant store eliminator removes the first...

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

But even the equal store eliminator cannot remove this, can it?
It might be that the test results were just wrong for an earlier commit...

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

ok sure, but you cannot have both of them missing?

By the way, do we actually still need the equal store eliminator?

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

The diff I'm seeing merely moves the sstore(0, 0) three lines down...

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

The input contains two sstore(0, 0), the output contained one of them before the diff, and still contains one of them after the diff, it just removes the other one.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

But yeah, I didn't get the chance to look at the equal store eliminator, but it did sound redundant to me relative to the redundant store eliminator :-).


k = Int('k')
diff = Int('diff')
solver.add(diff == start2 - start1 + k * 2**n_bits)

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

If we go with the assumption that all memory access is "small", this file needs to be updated to show that the condition is correct under that assumption.

@chriseth

chriseth commented Feb 3, 2022

Copy link
Copy Markdown
Contributor Author

Do we want to split this into storage and memory and wait with the memory part until we have the linear solver?

@ekpyron

ekpyron commented Feb 3, 2022

Copy link
Copy Markdown
Collaborator

Do we want to split this into storage and memory and wait with the memory part until we have the linear solver?

That may make sense, yes - if only, s.t. it's only one of the two to think about for the first version :-). We can also see what Fe says on Monday about the entire #11352 (comment) discussion...

@chriseth chriseth force-pushed the redundantStoreEliminator branch from 4eebb71 to fdee68f Compare February 15, 2022 10:05
@chriseth

Copy link
Copy Markdown
Contributor Author

Replaced by #12672

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.

8 participants