Re-implement unused assign / unused store eliminator.

This commit is contained in:
chriseth 2022-11-23 15:28:11 +01:00
parent 960889f532
commit fac5666dc9
13 changed files with 342 additions and 204 deletions

View File

@ -6,6 +6,7 @@ Language Features:
Compiler Features: Compiler Features:
* SMTChecker: Properties that are proved safe are now reported explicitly at the end of the analysis. By default, only the number of safe properties is shown. The CLI option ``--model-checker-show-proved-safe`` and the JSON option ``settings.modelChecker.showProvedSafe`` can be enabled to show the full list of safe properties. * SMTChecker: Properties that are proved safe are now reported explicitly at the end of the analysis. By default, only the number of safe properties is shown. The CLI option ``--model-checker-show-proved-safe`` and the JSON option ``settings.modelChecker.showProvedSafe`` can be enabled to show the full list of safe properties.
* SMTChecker: Group all messages about unsupported language features in a single warning. The CLI option ``--model-checker-show-unsupported`` and the JSON option ``settings.modelChecker.showUnsupported`` can be enabled to show the full list. * SMTChecker: Group all messages about unsupported language features in a single warning. The CLI option ``--model-checker-show-unsupported`` and the JSON option ``settings.modelChecker.showUnsupported`` can be enabled to show the full list.
* Optimizer: Re-implement simplified version of UnusedAssignEliminator and UnusedStoreEliminator. It can correctly remove some unused assignments in deeply nested loops that were ignored by the old version.
Bugfixes: Bugfixes:

View File

@ -24,48 +24,49 @@
#include <libyul/optimiser/Semantics.h> #include <libyul/optimiser/Semantics.h>
#include <libyul/optimiser/OptimizerUtilities.h> #include <libyul/optimiser/OptimizerUtilities.h>
#include <libyul/ControlFlowSideEffectsCollector.h>
#include <libyul/AST.h> #include <libyul/AST.h>
#include <libyul/AsmPrinter.h>
#include <libsolutil/CommonData.h> #include <libsolutil/CommonData.h>
#include <range/v3/action/remove_if.hpp> #include <range/v3/action/remove_if.hpp>
#include <iostream>
using namespace std; using namespace std;
using namespace solidity; using namespace solidity;
using namespace solidity::yul; using namespace solidity::yul;
void UnusedAssignEliminator::run(OptimiserStepContext& _context, Block& _ast) void UnusedAssignEliminator::run(OptimiserStepContext& _context, Block& _ast)
{ {
UnusedAssignEliminator rae{_context.dialect}; UnusedAssignEliminator uae{
rae(_ast); _context.dialect,
ControlFlowSideEffectsCollector{_context.dialect, _ast}.functionSideEffectsNamed()
};
uae(_ast);
StatementRemover remover{rae.m_pendingRemovals}; uae.m_storesToRemove += uae.m_allStores - uae.m_usedStores;
set<Statement const*> toRemove{uae.m_storesToRemove.begin(), uae.m_storesToRemove.end()};
StatementRemover remover{toRemove};
remover(_ast); remover(_ast);
} }
void UnusedAssignEliminator::operator()(Identifier const& _identifier) void UnusedAssignEliminator::operator()(Identifier const& _identifier)
{ {
changeUndecidedTo(_identifier.name, State::Used); markUsed(_identifier.name);
}
void UnusedAssignEliminator::operator()(VariableDeclaration const& _variableDeclaration)
{
UnusedStoreBase::operator()(_variableDeclaration);
for (auto const& var: _variableDeclaration.variables)
m_declaredVariables.emplace(var.name);
} }
void UnusedAssignEliminator::operator()(Assignment const& _assignment) void UnusedAssignEliminator::operator()(Assignment const& _assignment)
{ {
visit(*_assignment.value); visit(*_assignment.value);
for (auto const& var: _assignment.variableNames) // Do not visit the variables because they are Identifiers
changeUndecidedTo(var.name, State::Unused);
} }
void UnusedAssignEliminator::operator()(FunctionDefinition const& _functionDefinition) void UnusedAssignEliminator::operator()(FunctionDefinition const& _functionDefinition)
{ {
ScopedSaveAndRestore outerDeclaredVariables(m_declaredVariables, {});
ScopedSaveAndRestore outerReturnVariables(m_returnVariables, {}); ScopedSaveAndRestore outerReturnVariables(m_returnVariables, {});
for (auto const& retParam: _functionDefinition.returnVariables) for (auto const& retParam: _functionDefinition.returnVariables)
@ -74,20 +75,37 @@ void UnusedAssignEliminator::operator()(FunctionDefinition const& _functionDefin
UnusedStoreBase::operator()(_functionDefinition); UnusedStoreBase::operator()(_functionDefinition);
} }
void UnusedAssignEliminator::operator()(FunctionCall const& _functionCall)
{
UnusedStoreBase::operator()(_functionCall);
ControlFlowSideEffects sideEffects;
if (auto builtin = m_dialect.builtin(_functionCall.functionName.name))
sideEffects = builtin->controlFlowSideEffects;
else
sideEffects = m_controlFlowSideEffects.at(_functionCall.functionName.name);
if (!sideEffects.canContinue)
// We do not return from the current function, so it is OK to also
// clear the return variables.
m_activeStores.clear();
}
void UnusedAssignEliminator::operator()(Leave const&) void UnusedAssignEliminator::operator()(Leave const&)
{ {
for (YulString name: m_returnVariables) for (YulString name: m_returnVariables)
changeUndecidedTo(name, State::Used); markUsed(name);
m_activeStores.clear();
} }
void UnusedAssignEliminator::operator()(Block const& _block) void UnusedAssignEliminator::operator()(Block const& _block)
{ {
ScopedSaveAndRestore outerDeclaredVariables(m_declaredVariables, {});
UnusedStoreBase::operator()(_block); UnusedStoreBase::operator()(_block);
for (auto const& var: m_declaredVariables) for (auto const& statement: _block.statements)
finalize(var, State::Unused); if (auto const* varDecl = get_if<VariableDeclaration>(&statement))
for (auto const& var: varDecl->variables)
m_activeStores.erase(var.name);
} }
void UnusedAssignEliminator::visit(Statement const& _statement) void UnusedAssignEliminator::visit(Statement const& _statement)
@ -95,63 +113,49 @@ void UnusedAssignEliminator::visit(Statement const& _statement)
UnusedStoreBase::visit(_statement); UnusedStoreBase::visit(_statement);
if (auto const* assignment = get_if<Assignment>(&_statement)) if (auto const* assignment = get_if<Assignment>(&_statement))
if (assignment->variableNames.size() == 1) {
// Default-construct it in "Undecided" state if it does not yet exist. // We do not remove assignments whose values might have side-effects,
m_stores[assignment->variableNames.front().name][&_statement]; // but clear the active stores to the assigned variables in any case.
if (SideEffectsCollector{m_dialect, *assignment->value}.movable())
{
m_allStores.insert(&_statement);
for (auto const& var: assignment->variableNames)
m_activeStores[var.name] = {&_statement};
}
else
for (auto const& var: assignment->variableNames)
m_activeStores[var.name].clear();
}
} }
void UnusedAssignEliminator::shortcutNestedLoop(TrackedStores const& _zeroRuns) void UnusedAssignEliminator::shortcutNestedLoop(ActiveStores const& _zeroRuns)
{ {
// Shortcut to avoid horrible runtime: // Shortcut to avoid horrible runtime:
// Change all assignments that were newly introduced in the for loop to "used". // Change all assignments that were newly introduced in the for loop to "used".
// We do not have to do that with the "break" or "continue" paths, because // We do not have to do that with the "break" or "continue" paths, because
// they will be joined later anyway. // they will be joined later anyway.
// TODO parallel traversal might be more efficient here.
for (auto& [variable, stores]: m_stores) for (auto& [variable, stores]: m_activeStores)
{
auto zeroIt = _zeroRuns.find(variable);
for (auto& assignment: stores) for (auto& assignment: stores)
{ {
auto zeroIt = _zeroRuns.find(variable); if (zeroIt != _zeroRuns.end() && zeroIt->second.count(assignment))
if (zeroIt != _zeroRuns.end() && zeroIt->second.count(assignment.first))
continue; continue;
assignment.second = State::Value::Used; m_usedStores.insert(assignment);
} }
}
} }
void UnusedAssignEliminator::finalizeFunctionDefinition(FunctionDefinition const& _functionDefinition) void UnusedAssignEliminator::finalizeFunctionDefinition(FunctionDefinition const& _functionDefinition)
{ {
for (auto const& param: _functionDefinition.parameters)
finalize(param.name, State::Unused);
for (auto const& retParam: _functionDefinition.returnVariables) for (auto const& retParam: _functionDefinition.returnVariables)
finalize(retParam.name, State::Used); markUsed(retParam.name);
} }
void UnusedAssignEliminator::changeUndecidedTo(YulString _variable, UnusedAssignEliminator::State _newState) void UnusedAssignEliminator::markUsed(YulString _variable)
{ {
for (auto& assignment: m_stores[_variable]) for (auto& assignment: m_activeStores[_variable])
if (assignment.second == State::Undecided) m_usedStores.insert(assignment);
assignment.second = _newState; m_activeStores.erase(_variable);
}
void UnusedAssignEliminator::finalize(YulString _variable, UnusedAssignEliminator::State _finalState)
{
std::map<Statement const*, State> stores = std::move(m_stores[_variable]);
m_stores.erase(_variable);
for (auto& breakAssignments: m_forLoopInfo.pendingBreakStmts)
{
util::joinMap(stores, std::move(breakAssignments[_variable]), State::join);
breakAssignments.erase(_variable);
}
for (auto& continueAssignments: m_forLoopInfo.pendingContinueStmts)
{
util::joinMap(stores, std::move(continueAssignments[_variable]), State::join);
continueAssignments.erase(_variable);
}
for (auto&& [statement, state]: stores)
if (
(state == State::Unused || (state == State::Undecided && _finalState == State::Unused)) &&
SideEffectsCollector{m_dialect, *std::get<Assignment>(*statement).value}.movable()
)
m_pendingRemovals.insert(statement);
} }

View File

@ -26,6 +26,7 @@
#include <libyul/optimiser/ASTWalker.h> #include <libyul/optimiser/ASTWalker.h>
#include <libyul/optimiser/OptimiserStep.h> #include <libyul/optimiser/OptimiserStep.h>
#include <libyul/optimiser/UnusedStoreBase.h> #include <libyul/optimiser/UnusedStoreBase.h>
#include <libyul/optimiser/Semantics.h>
#include <map> #include <map>
#include <vector> #include <vector>
@ -62,28 +63,34 @@ struct Dialect;
* Detailed rules: * Detailed rules:
* *
* The AST is traversed twice: in an information gathering step and in the * The AST is traversed twice: in an information gathering step and in the
* actual removal step. During information gathering, we maintain a * actual removal step. During information gathering, assignment statements
* mapping from assignment statements to the three states * can be marked as "potentially unused" or as "used".
* "unused", "undecided" and "used". *
* When an assignment is visited, it is added to the mapping in the "undecided" state * When an assignment is visited, it is stored in the "set of all stores" and
* (see remark about for loops below) and every other assignment to the same variable * added to the branch-dependent "active" sets for the assigned variables. This active
* that is still in the "undecided" state is changed to "unused". * set for a variable contains all statements where that variable was last assigned to, i.e.
* When a variable is referenced, the state of any assignment to that variable still * where a read from that variable could read from.
* in the "undecided" state is changed to "used". * Furthermore, all other active sets for the assigned variables are cleared.
* At points where control flow splits, a copy *
* of the mapping is handed over to each branch. At points where control flow * When a reference to a variable is visited, the active assignments to that variable
* joins, the two mappings coming from the two branches are combined in the following way: * in the current branch are marked as "used". This mark is permanent.
* Statements that are only in one mapping or have the same state are used unchanged. * Also, the active set for this variable in the current branch is cleared.
* Conflicting values are resolved in the following way: *
* "unused", "undecided" -> "undecided" * At points where control-flow splits, we maintain a copy of the active set
* "unused", "used" -> "used" * (all other data structures are shared across branches).
* "undecided, "used" -> "used". *
* At control-flow joins, we combine the sets of active stores for each variable.
*
* In the example above, the active set right after the assignment "b := mload(a)" (but before
* the control-flow join) is "b := mload(a)"; the assignment "b := 2" was removed.
* After the control-flow join it will contain both "b := mload(a)" and "b := 2", coming from
* the two branches.
* *
* For for-loops, the condition, body and post-part are visited twice, taking * For for-loops, the condition, body and post-part are visited twice, taking
* the joining control-flow at the condition into account. * the joining control-flow at the condition into account.
* In other words, we create three control flow paths: Zero runs of the loop, * In other words, we create three control flow paths: Zero runs of the loop,
* one run and two runs and then combine them at the end. * one run and two runs and then combine them at the end.
* Running at most twice is enough because there are only three different states. * Running at most twice is enough because this takes into account all possible control-flow connections.
* *
* Since this algorithm has exponential runtime in the nesting depth of for loops, * Since this algorithm has exponential runtime in the nesting depth of for loops,
* a shortcut is taken at a certain nesting level: We only use the zero- and * a shortcut is taken at a certain nesting level: We only use the zero- and
@ -93,14 +100,13 @@ struct Dialect;
* For switch statements that have a "default"-case, there is no control-flow * For switch statements that have a "default"-case, there is no control-flow
* part that skips the switch. * part that skips the switch.
* *
* At ``leave`` statements, all return variables are set to "used". * At ``leave`` statements, all return variables are set to "used" and the set of active statements
* is cleared.
* *
* When a variable goes out of scope, all statements still in the "undecided" * If a function or builtin is called that does not continue, the set of active statements is
* state are changed to "unused", unless the variable is the return * cleared for all variables.
* parameter of a function - there, the state changes to "used".
*
* In the second traversal, all assignments that are in the "unused" state are removed.
* *
* In the second traversal, all assignments that are not marked as "used" are removed.
* *
* This step is usually run right after the SSA transform to complete * This step is usually run right after the SSA transform to complete
* the generation of the pseudo-SSA. * the generation of the pseudo-SSA.
@ -113,12 +119,18 @@ public:
static constexpr char const* name{"UnusedAssignEliminator"}; static constexpr char const* name{"UnusedAssignEliminator"};
static void run(OptimiserStepContext&, Block& _ast); static void run(OptimiserStepContext&, Block& _ast);
explicit UnusedAssignEliminator(Dialect const& _dialect): UnusedStoreBase(_dialect) {} explicit UnusedAssignEliminator(
Dialect const& _dialect,
std::map<YulString, ControlFlowSideEffects> _controlFlowSideEffects
):
UnusedStoreBase(_dialect),
m_controlFlowSideEffects(_controlFlowSideEffects)
{}
void operator()(Identifier const& _identifier) override; void operator()(Identifier const& _identifier) override;
void operator()(VariableDeclaration const& _variableDeclaration) override;
void operator()(Assignment const& _assignment) override; void operator()(Assignment const& _assignment) override;
void operator()(FunctionDefinition const&) override; void operator()(FunctionDefinition const&) override;
void operator()(FunctionCall const& _functionCall) override;
void operator()(Leave const&) override; void operator()(Leave const&) override;
void operator()(Block const& _block) override; void operator()(Block const& _block) override;
@ -126,18 +138,13 @@ public:
void visit(Statement const& _statement) override; void visit(Statement const& _statement) override;
private: private:
void shortcutNestedLoop(TrackedStores const& _beforeLoop) override; void shortcutNestedLoop(ActiveStores const& _beforeLoop) override;
void finalizeFunctionDefinition(FunctionDefinition const& _functionDefinition) override; void finalizeFunctionDefinition(FunctionDefinition const& _functionDefinition) override;
void changeUndecidedTo(YulString _variable, State _newState); void markUsed(YulString _variable);
/// Called when a variable goes out of scope. Sets the state of all still undecided
/// assignments to the final state. In this case, this also applies to pending
/// break and continue TrackedStores.
void finalize(YulString _variable, State _finalState);
std::set<YulString> m_declaredVariables;
std::set<YulString> m_returnVariables; std::set<YulString> m_returnVariables;
std::map<YulString, ControlFlowSideEffects> m_controlFlowSideEffects;
}; };
} }

View File

@ -37,47 +37,50 @@ void UnusedStoreBase::operator()(If const& _if)
{ {
visit(*_if.condition); visit(*_if.condition);
TrackedStores skipBranch{m_stores}; ActiveStores skipBranch{m_activeStores};
(*this)(_if.body); (*this)(_if.body);
merge(m_stores, std::move(skipBranch)); merge(m_activeStores, std::move(skipBranch));
} }
void UnusedStoreBase::operator()(Switch const& _switch) void UnusedStoreBase::operator()(Switch const& _switch)
{ {
visit(*_switch.expression); visit(*_switch.expression);
TrackedStores const preState{m_stores}; ActiveStores const preState{m_activeStores};
bool hasDefault = false; bool hasDefault = false;
vector<TrackedStores> branches; vector<ActiveStores> branches;
for (auto const& c: _switch.cases) for (auto const& c: _switch.cases)
{ {
if (!c.value) if (!c.value)
hasDefault = true; hasDefault = true;
(*this)(c.body); (*this)(c.body);
branches.emplace_back(std::move(m_stores)); branches.emplace_back(std::move(m_activeStores));
m_stores = preState; m_activeStores = preState;
} }
if (hasDefault) if (hasDefault)
{ {
m_stores = std::move(branches.back()); m_activeStores = std::move(branches.back());
branches.pop_back(); branches.pop_back();
} }
for (auto& branch: branches) for (auto& branch: branches)
merge(m_stores, std::move(branch)); merge(m_activeStores, std::move(branch));
} }
void UnusedStoreBase::operator()(FunctionDefinition const& _functionDefinition) void UnusedStoreBase::operator()(FunctionDefinition const& _functionDefinition)
{ {
ScopedSaveAndRestore outerAssignments(m_stores, {}); ScopedSaveAndRestore allStores(m_allStores, {});
ScopedSaveAndRestore usedStores(m_usedStores, {});
ScopedSaveAndRestore outerAssignments(m_activeStores, {});
ScopedSaveAndRestore forLoopInfo(m_forLoopInfo, {}); ScopedSaveAndRestore forLoopInfo(m_forLoopInfo, {});
ScopedSaveAndRestore forLoopNestingDepth(m_forLoopNestingDepth, 0); ScopedSaveAndRestore forLoopNestingDepth(m_forLoopNestingDepth, 0);
(*this)(_functionDefinition.body); (*this)(_functionDefinition.body);
finalizeFunctionDefinition(_functionDefinition); finalizeFunctionDefinition(_functionDefinition);
m_storesToRemove += m_allStores - m_usedStores;
} }
void UnusedStoreBase::operator()(ForLoop const& _forLoop) void UnusedStoreBase::operator()(ForLoop const& _forLoop)
@ -94,10 +97,10 @@ void UnusedStoreBase::operator()(ForLoop const& _forLoop)
visit(*_forLoop.condition); visit(*_forLoop.condition);
TrackedStores zeroRuns{m_stores}; ActiveStores zeroRuns{m_activeStores};
(*this)(_forLoop.body); (*this)(_forLoop.body);
merge(m_stores, std::move(m_forLoopInfo.pendingContinueStmts)); merge(m_activeStores, std::move(m_forLoopInfo.pendingContinueStmts));
m_forLoopInfo.pendingContinueStmts = {}; m_forLoopInfo.pendingContinueStmts = {};
(*this)(_forLoop.post); (*this)(_forLoop.post);
@ -106,54 +109,54 @@ void UnusedStoreBase::operator()(ForLoop const& _forLoop)
if (m_forLoopNestingDepth < 6) if (m_forLoopNestingDepth < 6)
{ {
// Do the second run only for small nesting depths to avoid horrible runtime. // Do the second run only for small nesting depths to avoid horrible runtime.
TrackedStores oneRun{m_stores}; ActiveStores oneRun{m_activeStores};
(*this)(_forLoop.body); (*this)(_forLoop.body);
merge(m_stores, std::move(m_forLoopInfo.pendingContinueStmts)); merge(m_activeStores, std::move(m_forLoopInfo.pendingContinueStmts));
m_forLoopInfo.pendingContinueStmts.clear(); m_forLoopInfo.pendingContinueStmts.clear();
(*this)(_forLoop.post); (*this)(_forLoop.post);
visit(*_forLoop.condition); visit(*_forLoop.condition);
// Order of merging does not matter because "max" is commutative and associative. // Order of merging does not matter because "max" is commutative and associative.
merge(m_stores, std::move(oneRun)); merge(m_activeStores, std::move(oneRun));
} }
else else
// Shortcut to avoid horrible runtime. // Shortcut to avoid horrible runtime.
shortcutNestedLoop(zeroRuns); shortcutNestedLoop(zeroRuns);
// Order of merging does not matter because "max" is commutative and associative. // Order of merging does not matter because "max" is commutative and associative.
merge(m_stores, std::move(zeroRuns)); merge(m_activeStores, std::move(zeroRuns));
merge(m_stores, std::move(m_forLoopInfo.pendingBreakStmts)); merge(m_activeStores, std::move(m_forLoopInfo.pendingBreakStmts));
m_forLoopInfo.pendingBreakStmts.clear(); m_forLoopInfo.pendingBreakStmts.clear();
} }
void UnusedStoreBase::operator()(Break const&) void UnusedStoreBase::operator()(Break const&)
{ {
m_forLoopInfo.pendingBreakStmts.emplace_back(std::move(m_stores)); m_forLoopInfo.pendingBreakStmts.emplace_back(std::move(m_activeStores));
m_stores.clear(); m_activeStores.clear();
} }
void UnusedStoreBase::operator()(Continue const&) void UnusedStoreBase::operator()(Continue const&)
{ {
m_forLoopInfo.pendingContinueStmts.emplace_back(std::move(m_stores)); m_forLoopInfo.pendingContinueStmts.emplace_back(std::move(m_activeStores));
m_stores.clear(); m_activeStores.clear();
} }
void UnusedStoreBase::merge(TrackedStores& _target, TrackedStores&& _other) void UnusedStoreBase::merge(ActiveStores& _target, ActiveStores&& _other)
{ {
util::joinMap(_target, std::move(_other), []( util::joinMap(_target, std::move(_other), [](
map<Statement const*, State>& _assignmentHere, set<Statement const*>& _storesHere,
map<Statement const*, State>&& _assignmentThere set<Statement const*>&& _storesThere
) )
{ {
return util::joinMap(_assignmentHere, std::move(_assignmentThere), State::join); _storesHere += _storesThere;
}); });
} }
void UnusedStoreBase::merge(TrackedStores& _target, vector<TrackedStores>&& _source) void UnusedStoreBase::merge(ActiveStores& _target, vector<ActiveStores>&& _source)
{ {
for (TrackedStores& ts: _source) for (ActiveStores& ts: _source)
merge(_target, std::move(ts)); merge(_target, std::move(ts));
_source.clear(); _source.clear();
} }

View File

@ -38,8 +38,11 @@ struct Dialect;
* *
* The class tracks the state of abstract "stores" (assignments or mstore/sstore * The class tracks the state of abstract "stores" (assignments or mstore/sstore
* statements) across the control-flow. It is the job of the derived class to create * statements) across the control-flow. It is the job of the derived class to create
* the stores and track references, but the base class adjusts their "used state" at * the stores and track references, but the base class manages control-flow splits and joins.
* control-flow splits and joins. *
* In general, active stores are those where it has not yet been determined if they are used
* or not. Those are split and joined at control-flow forks. Once a store has been deemed
* used, it is removed from the active set and marked as used and this will never change.
* *
* Prerequisite: Disambiguator, ForLoopInitRewriter. * Prerequisite: Disambiguator, ForLoopInitRewriter.
*/ */
@ -57,28 +60,12 @@ public:
void operator()(Continue const&) override; void operator()(Continue const&) override;
protected: protected:
class State using ActiveStores = std::map<YulString, std::set<Statement const*>>;
{
public:
enum Value { Unused, Undecided, Used };
State(Value _value = Undecided): m_value(_value) {}
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)
{
// 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)));
}
private:
Value m_value = Undecided;
};
using TrackedStores = std::map<YulString, std::map<Statement const*, State>>;
/// This function is called for a loop that is nested too deep to avoid /// This function is called for a loop that is nested too deep to avoid
/// horrible runtime and should just resolve the situation in a pragmatic /// horrible runtime and should just resolve the situation in a pragmatic
/// and correct manner. /// and correct manner.
virtual void shortcutNestedLoop(TrackedStores const& _beforeLoop) = 0; virtual void shortcutNestedLoop(ActiveStores const& _beforeLoop) = 0;
/// This function is called right before the scoped restore of the function definition. /// This function is called right before the scoped restore of the function definition.
virtual void finalizeFunctionDefinition(FunctionDefinition const& /*_functionDefinition*/) {} virtual void finalizeFunctionDefinition(FunctionDefinition const& /*_functionDefinition*/) {}
@ -86,20 +73,26 @@ protected:
/// Joins the assignment mapping of @a _source into @a _target according to the rules laid out /// Joins the assignment mapping of @a _source into @a _target according to the rules laid out
/// above. /// above.
/// Will destroy @a _source. /// Will destroy @a _source.
static void merge(TrackedStores& _target, TrackedStores&& _source); static void merge(ActiveStores& _target, ActiveStores&& _source);
static void merge(TrackedStores& _target, std::vector<TrackedStores>&& _source); static void merge(ActiveStores& _target, std::vector<ActiveStores>&& _source);
Dialect const& m_dialect; Dialect const& m_dialect;
std::set<Statement const*> m_pendingRemovals; /// Set of all stores encountered during the traversal (in the current function).
TrackedStores m_stores; std::set<Statement const*> m_allStores;
/// Set of stores that are marked as being used (in the current function).
std::set<Statement const*> m_usedStores;
/// List of stores that can be removed (globally).
std::vector<Statement const*> m_storesToRemove;
/// Active (undecided) stores in the current branch.
ActiveStores m_activeStores;
/// Working data for traversing for-loops. /// Working data for traversing for-loops.
struct ForLoopInfo struct ForLoopInfo
{ {
/// Tracked assignment states for each break statement. /// Tracked assignment states for each break statement.
std::vector<TrackedStores> pendingBreakStmts; std::vector<ActiveStores> pendingBreakStmts;
/// Tracked assignment states for each continue statement. /// Tracked assignment states for each continue statement.
std::vector<TrackedStores> pendingContinueStmts; std::vector<ActiveStores> pendingContinueStmts;
}; };
ForLoopInfo m_forLoopInfo; ForLoopInfo m_forLoopInfo;
size_t m_forLoopNestingDepth = 0; size_t m_forLoopNestingDepth = 0;

View File

@ -78,17 +78,17 @@ void UnusedStoreEliminator::run(OptimiserStepContext& _context, Block& _ast)
ignoreMemory ignoreMemory
}; };
rse(_ast); rse(_ast);
if (
auto evmDialect = dynamic_cast<EVMDialect const*>(&_context.dialect);
evmDialect && evmDialect->providesObjectAccess()
)
rse.changeUndecidedTo(State::Unused, Location::Memory);
else
rse.changeUndecidedTo(State::Used, Location::Memory);
rse.changeUndecidedTo(State::Used, Location::Storage);
rse.scheduleUnusedForDeletion();
StatementRemover remover(rse.m_pendingRemovals); auto evmDialect = dynamic_cast<EVMDialect const*>(&_context.dialect);
if (evmDialect && evmDialect->providesObjectAccess())
rse.clearActive(Location::Memory);
else
rse.markActiveAsUsed(Location::Memory);
rse.markActiveAsUsed(Location::Storage);
rse.m_storesToRemove += rse.m_allStores - rse.m_usedStores;
set<Statement const*> toRemove{rse.m_storesToRemove.begin(), rse.m_storesToRemove.end()};
StatementRemover remover{toRemove};
remover(_ast); remover(_ast);
} }
@ -121,12 +121,12 @@ void UnusedStoreEliminator::operator()(FunctionCall const& _functionCall)
sideEffects = m_controlFlowSideEffects.at(_functionCall.functionName.name); sideEffects = m_controlFlowSideEffects.at(_functionCall.functionName.name);
if (sideEffects.canTerminate) if (sideEffects.canTerminate)
changeUndecidedTo(State::Used, Location::Storage); markActiveAsUsed(Location::Storage);
if (!sideEffects.canContinue) if (!sideEffects.canContinue)
{ {
changeUndecidedTo(State::Unused, Location::Memory); clearActive(Location::Memory);
if (!sideEffects.canTerminate) if (!sideEffects.canTerminate)
changeUndecidedTo(State::Unused, Location::Storage); clearActive(Location::Storage);
} }
} }
@ -139,7 +139,7 @@ void UnusedStoreEliminator::operator()(FunctionDefinition const& _functionDefini
void UnusedStoreEliminator::operator()(Leave const&) void UnusedStoreEliminator::operator()(Leave const&)
{ {
changeUndecidedTo(State::Used); markActiveAsUsed();
} }
void UnusedStoreEliminator::visit(Statement const& _statement) void UnusedStoreEliminator::visit(Statement const& _statement)
@ -183,10 +183,14 @@ void UnusedStoreEliminator::visit(Statement const& _statement)
yulAssert(isCandidateForRemoval == (isStorageWrite || (!m_ignoreMemory && isMemoryWrite))); yulAssert(isCandidateForRemoval == (isStorageWrite || (!m_ignoreMemory && isMemoryWrite)));
if (isCandidateForRemoval) if (isCandidateForRemoval)
{ {
State initialState = State::Undecided;
if (*instruction == Instruction::RETURNDATACOPY) if (*instruction == Instruction::RETURNDATACOPY)
{ {
initialState = State::Used; // Out-of-bounds access to the returndata buffer results in a revert,
// so we are careful not to remove a potentially reverting call to a builtin.
// The only way the Solidity compiler uses `returndatacopy` is
// `returndatacopy(X, 0, returndatasize())`, so we only allow to remove this pattern
// (which is guaranteed to never cause an out-of-bounds revert).
bool allowReturndatacopyToBeRemoved = false;
auto startOffset = identifierNameIfSSA(funCall->arguments.at(1)); auto startOffset = identifierNameIfSSA(funCall->arguments.at(1));
auto length = identifierNameIfSSA(funCall->arguments.at(2)); auto length = identifierNameIfSSA(funCall->arguments.at(2));
if (length && startOffset) if (length && startOffset)
@ -197,22 +201,22 @@ void UnusedStoreEliminator::visit(Statement const& _statement)
lengthCall && lengthCall &&
toEVMInstruction(m_dialect, lengthCall->functionName.name) == Instruction::RETURNDATASIZE toEVMInstruction(m_dialect, lengthCall->functionName.name) == Instruction::RETURNDATASIZE
) )
initialState = State::Undecided; allowReturndatacopyToBeRemoved = true;
} }
if (!allowReturndatacopyToBeRemoved)
return;
} }
m_stores[YulString{}].insert({&_statement, initialState}); m_allStores.insert(&_statement);
vector<Operation> operations = operationsFromFunctionCall(*funCall); vector<Operation> operations = operationsFromFunctionCall(*funCall);
yulAssert(operations.size() == 1, ""); yulAssert(operations.size() == 1, "");
if (operations.front().location == Location::Storage)
activeStorageStores().insert(&_statement);
else
activeMemoryStores().insert(&_statement);
m_storeOperations[&_statement] = std::move(operations.front()); m_storeOperations[&_statement] = std::move(operations.front());
} }
} }
void UnusedStoreEliminator::finalizeFunctionDefinition(FunctionDefinition const&)
{
changeUndecidedTo(State::Used);
scheduleUnusedForDeletion();
}
vector<UnusedStoreEliminator::Operation> UnusedStoreEliminator::operationsFromFunctionCall( vector<UnusedStoreEliminator::Operation> UnusedStoreEliminator::operationsFromFunctionCall(
FunctionCall const& _functionCall FunctionCall const& _functionCall
) const ) const
@ -265,15 +269,28 @@ vector<UnusedStoreEliminator::Operation> UnusedStoreEliminator::operationsFromFu
void UnusedStoreEliminator::applyOperation(UnusedStoreEliminator::Operation const& _operation) void UnusedStoreEliminator::applyOperation(UnusedStoreEliminator::Operation const& _operation)
{ {
for (auto& [statement, state]: m_stores[YulString{}]) set<Statement const*>& active =
if (state == State::Undecided) _operation.location == Location::Storage ?
activeStorageStores() :
activeMemoryStores();
for (auto it = active.begin(); it != active.end();)
{
Statement const* statement = *it;
Operation const& storeOperation = m_storeOperations.at(statement);
if (_operation.effect == Effect::Read && !knownUnrelated(storeOperation, _operation))
{ {
Operation const& storeOperation = m_storeOperations.at(statement); // This store is read from, mark it as used and remove it from the active set.
if (_operation.effect == Effect::Read && !knownUnrelated(storeOperation, _operation)) m_usedStores.insert(statement);
state = State::Used; it = active.erase(it);
else if (_operation.effect == Effect::Write && knownCovered(storeOperation, _operation))
state = State::Unused;
} }
else if (_operation.effect == Effect::Write && knownCovered(storeOperation, _operation))
// This store is overwritten before being read, remove it from the active set.
it = active.erase(it);
else
++it;
}
} }
bool UnusedStoreEliminator::knownUnrelated( bool UnusedStoreEliminator::knownUnrelated(
@ -390,16 +407,27 @@ bool UnusedStoreEliminator::knownCovered(
return false; return false;
} }
void UnusedStoreEliminator::changeUndecidedTo( void UnusedStoreEliminator::markActiveAsUsed(
State _newState, optional<UnusedStoreEliminator::Location> _onlyLocation
optional<UnusedStoreEliminator::Location> _onlyLocation) )
{ {
for (auto& [statement, state]: m_stores[YulString{}]) if (_onlyLocation == nullopt || _onlyLocation == Location::Memory)
if ( for (Statement const* statement: activeMemoryStores())
state == State::Undecided && m_usedStores.insert(statement);
(_onlyLocation == nullopt || *_onlyLocation == m_storeOperations.at(statement).location) if (_onlyLocation == nullopt || _onlyLocation == Location::Storage)
) for (Statement const* statement: activeStorageStores())
state = _newState; m_usedStores.insert(statement);
clearActive(_onlyLocation);
}
void UnusedStoreEliminator::clearActive(
optional<UnusedStoreEliminator::Location> _onlyLocation
)
{
if (_onlyLocation == nullopt || _onlyLocation == Location::Memory)
activeMemoryStores() = {};
if (_onlyLocation == nullopt || _onlyLocation == Location::Storage)
activeStorageStores() = {};
} }
optional<YulString> UnusedStoreEliminator::identifierNameIfSSA(Expression const& _expression) const optional<YulString> UnusedStoreEliminator::identifierNameIfSSA(Expression const& _expression) const
@ -409,10 +437,3 @@ optional<YulString> UnusedStoreEliminator::identifierNameIfSSA(Expression const&
return {identifier->name}; return {identifier->name};
return nullopt; return nullopt;
} }
void UnusedStoreEliminator::scheduleUnusedForDeletion()
{
for (auto const& [statement, state]: m_stores[YulString{}])
if (state == State::Unused)
m_pendingRemovals.insert(statement);
}

View File

@ -50,8 +50,7 @@ struct AssignedValue;
* to sstore, as we don't know whether the memory location will be read once we leave the function's scope, * to sstore, as we don't know whether the memory location will be read once we leave the function's scope,
* so the statement will be removed only if all code code paths lead to a memory overwrite. * so the statement will be removed only if all code code paths lead to a memory overwrite.
* *
* The m_store member of UnusedStoreBase is only used with the empty yul string * The m_store member of UnusedStoreBase uses the key "m" for memory and "s" for storage stores.
* as key in the first dimension.
* *
* Best run in SSA form. * Best run in SSA form.
* *
@ -93,20 +92,26 @@ public:
}; };
private: private:
void shortcutNestedLoop(TrackedStores const&) override std::set<Statement const*>& activeMemoryStores() { return m_activeStores["m"_yulstring]; }
std::set<Statement const*>& activeStorageStores() { return m_activeStores["s"_yulstring]; }
void shortcutNestedLoop(ActiveStores const&) override
{ {
// We might only need to do this for newly introduced stores in the loop. // We might only need to do this for newly introduced stores in the loop.
changeUndecidedTo(State::Used); markActiveAsUsed();
}
void finalizeFunctionDefinition(FunctionDefinition const&) override
{
markActiveAsUsed();
} }
void finalizeFunctionDefinition(FunctionDefinition const&) override;
std::vector<Operation> operationsFromFunctionCall(FunctionCall const& _functionCall) const; std::vector<Operation> operationsFromFunctionCall(FunctionCall const& _functionCall) const;
void applyOperation(Operation const& _operation); void applyOperation(Operation const& _operation);
bool knownUnrelated(Operation const& _op1, Operation const& _op2) const; bool knownUnrelated(Operation const& _op1, Operation const& _op2) const;
bool knownCovered(Operation const& _covered, Operation const& _covering) const; bool knownCovered(Operation const& _covered, Operation const& _covering) const;
void changeUndecidedTo(State _newState, std::optional<Location> _onlyLocation = std::nullopt); void markActiveAsUsed(std::optional<Location> _onlyLocation = std::nullopt);
void scheduleUnusedForDeletion(); void clearActive(std::optional<Location> _onlyLocation = std::nullopt);
std::optional<YulString> identifierNameIfSSA(Expression const& _expression) const; std::optional<YulString> identifierNameIfSSA(Expression const& _expression) const;

View File

@ -176,7 +176,7 @@ contract DepositContract is IDepositContract, ERC165 {
} }
// ---- // ----
// constructor() // constructor()
// gas irOptimized: 1430741 // gas irOptimized: 1419712
// gas legacy: 2427905 // gas legacy: 2427905
// gas legacyOptimized: 1773081 // gas legacyOptimized: 1773081
// supportsInterface(bytes4): 0x0 -> 0 // supportsInterface(bytes4): 0x0 -> 0

View File

@ -0,0 +1,33 @@
{
function g() {
if calldataload(10) { revert(0, 0) }
}
function f() {
let a := calldataload(0)
if calldataload(1) {
// this can NOT be removed
a := 2
g()
}
sstore(0, a)
}
}
// ----
// step: unusedAssignEliminator
//
// {
// function g()
// {
// if calldataload(10) { revert(0, 0) }
// }
// function f()
// {
// let a := calldataload(0)
// if calldataload(1)
// {
// a := 2
// g()
// }
// sstore(0, a)
// }
// }

View File

@ -0,0 +1,22 @@
{
function f() {
let a := calldataload(0)
if calldataload(1) {
// this can be removed
a := 2
leave
}
sstore(0, a)
}
}
// ----
// step: unusedAssignEliminator
//
// {
// function f()
// {
// let a := calldataload(0)
// if calldataload(1) { leave }
// sstore(0, a)
// }
// }

View File

@ -0,0 +1,17 @@
{
let a := calldataload(0)
if calldataload(1) {
// this can be removed
a := 2
revert(0, 0)
}
sstore(0, a)
}
// ----
// step: unusedAssignEliminator
//
// {
// let a := calldataload(0)
// if calldataload(1) { revert(0, 0) }
// sstore(0, a)
// }

View File

@ -46,10 +46,7 @@
// for { } 1 { } // for { } 1 { }
// { // {
// for { } 1 { a := 10 } // for { } 1 { a := 10 }
// { // { b := 11 }
// b := 12
// b := 11
// }
// } // }
// } // }
// } // }

View File

@ -0,0 +1,35 @@
{
function g() -> x {
x := 7
if calldataload(0) {
x := 3
reverting()
}
if calldataload(1) {
x := 3
leave
}
x := 2
reverting()
}
function reverting() { revert(0, 0) }
sstore(0, g())
}
// ----
// step: unusedAssignEliminator
//
// {
// function g() -> x
// {
// if calldataload(0) { reverting() }
// if calldataload(1)
// {
// x := 3
// leave
// }
// reverting()
// }
// function reverting()
// { revert(0, 0) }
// sstore(0, g())
// }