diff --git a/Changelog.md b/Changelog.md index f820c0ad8..054a8283e 100644 --- a/Changelog.md +++ b/Changelog.md @@ -7,6 +7,7 @@ Language Features: Compiler Features: * Yul: When compiling via Yul, string literals from the Solidity code are kept as string literals if every character is safely printable. + * Yul Optimizer: Perform loop-invariant code motion. Bugfixes: diff --git a/libyul/CMakeLists.txt b/libyul/CMakeLists.txt index f71171597..da45a2941 100644 --- a/libyul/CMakeLists.txt +++ b/libyul/CMakeLists.txt @@ -110,6 +110,8 @@ add_library(yul optimiser/KnowledgeBase.h optimiser/LoadResolver.cpp optimiser/LoadResolver.h + optimiser/LoopInvariantCodeMotion.cpp + optimiser/LoopInvariantCodeMotion.h optimiser/MainFunction.cpp optimiser/MainFunction.h optimiser/Metrics.cpp diff --git a/libyul/optimiser/LoopInvariantCodeMotion.cpp b/libyul/optimiser/LoopInvariantCodeMotion.cpp new file mode 100644 index 000000000..00375a71d --- /dev/null +++ b/libyul/optimiser/LoopInvariantCodeMotion.cpp @@ -0,0 +1,115 @@ +/* + This file is part of solidity. + + solidity is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + solidity is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with solidity. If not, see . +*/ + +#include + +#include +#include +#include +#include +#include +#include + +#include + +using namespace std; +using namespace dev; +using namespace yul; + +void LoopInvariantCodeMotion::run(OptimiserStepContext& _context, Block& _ast) +{ + map functionSideEffects = + SideEffectsPropagator::sideEffects(_context.dialect, CallGraphGenerator::callGraph(_ast)); + + set ssaVars = SSAValueTracker::ssaVariables(_ast); + LoopInvariantCodeMotion{_context.dialect, ssaVars, functionSideEffects}(_ast); +} + +void LoopInvariantCodeMotion::operator()(Block& _block) +{ + iterateReplacing( + _block.statements, + [&](Statement& _s) -> optional> + { + visit(_s); + if (holds_alternative(_s)) + return rewriteLoop(get(_s)); + else + return {}; + } + ); +} + +bool LoopInvariantCodeMotion::canBePromoted( + VariableDeclaration const& _varDecl, + set const& _varsDefinedInCurrentScope +) const +{ + // A declaration can be promoted iff + // 1. Its LHS is a SSA variable + // 2. Its RHS only references SSA variables declared outside of the current scope + // 3. Its RHS is movable + + for (auto const& var: _varDecl.variables) + if (!m_ssaVariables.count(var.name)) + return false; + if (_varDecl.value) + { + for (auto const& ref: ReferencesCounter::countReferences(*_varDecl.value, ReferencesCounter::OnlyVariables)) + if (_varsDefinedInCurrentScope.count(ref.first) || !m_ssaVariables.count(ref.first)) + return false; + if (!SideEffectsCollector{m_dialect, *_varDecl.value, &m_functionSideEffects}.movable()) + return false; + } + return true; +} + +optional> LoopInvariantCodeMotion::rewriteLoop(ForLoop& _for) +{ + assertThrow(_for.pre.statements.empty(), OptimizerException, ""); + vector replacement; + for (Block* block: {&_for.post, &_for.body}) + { + set varsDefinedInScope; + iterateReplacing( + block->statements, + [&](Statement& _s) -> optional> + { + if (holds_alternative(_s)) + { + VariableDeclaration const& varDecl = std::get(_s); + if (canBePromoted(varDecl, varsDefinedInScope)) + { + replacement.emplace_back(std::move(_s)); + // Do not add the variables declared here to varsDefinedInScope because we are moving them. + return vector{}; + } + for (auto const& var: varDecl.variables) + varsDefinedInScope.insert(var.name); + } + return {}; + } + ); + } + if (replacement.empty()) + return {}; + else + { + replacement.emplace_back(std::move(_for)); + return { std::move(replacement) }; + } +} diff --git a/libyul/optimiser/LoopInvariantCodeMotion.h b/libyul/optimiser/LoopInvariantCodeMotion.h new file mode 100644 index 000000000..f5f250515 --- /dev/null +++ b/libyul/optimiser/LoopInvariantCodeMotion.h @@ -0,0 +1,67 @@ +/* + This file is part of solidity. + + solidity is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + solidity is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with solidity. If not, see . +*/ +#pragma once + +#include +#include +#include + +namespace yul +{ + +/** + * Loop-invariant code motion. + * + * This optimization moves movable SSA variable declarations outside the loop. + * + * Only statements at the top level in a loop's body or post block are considered, i.e variable + * declarations inside conditional branches will not be moved out of the loop. + * + * Requirements: + * - The Disambiguator, ForLoopInitRewriter and FunctionHoister must be run upfront. + * - Expression splitter and SSA transform should be run upfront to obtain better result. + */ + +class LoopInvariantCodeMotion: public ASTModifier +{ +public: + static constexpr char const* name{"LoopInvariantCodeMotion"}; + static void run(OptimiserStepContext& _context, Block& _ast); + + void operator()(Block& _block) override; + +private: + explicit LoopInvariantCodeMotion( + Dialect const& _dialect, + std::set const& _ssaVariables, + std::map const& _functionSideEffects + ): + m_dialect(_dialect), + m_ssaVariables(_ssaVariables), + m_functionSideEffects(_functionSideEffects) + { } + + /// @returns true if the given variable declaration can be moved to in front of the loop. + bool canBePromoted(VariableDeclaration const& _varDecl, std::set const& _varsDefinedInCurrentScope) const; + std::optional> rewriteLoop(ForLoop& _for); + + Dialect const& m_dialect; + std::set const& m_ssaVariables; + std::map const& m_functionSideEffects; +}; + +} diff --git a/libyul/optimiser/NameCollector.cpp b/libyul/optimiser/NameCollector.cpp index 04631a86a..dab2f290a 100644 --- a/libyul/optimiser/NameCollector.cpp +++ b/libyul/optimiser/NameCollector.cpp @@ -49,27 +49,28 @@ void ReferencesCounter::operator()(Identifier const& _identifier) void ReferencesCounter::operator()(FunctionCall const& _funCall) { - ++m_references[_funCall.functionName.name]; + if (m_countWhat == VariablesAndFunctions) + ++m_references[_funCall.functionName.name]; ASTWalker::operator()(_funCall); } -map ReferencesCounter::countReferences(Block const& _block) +map ReferencesCounter::countReferences(Block const& _block, CountWhat _countWhat) { - ReferencesCounter counter; + ReferencesCounter counter(_countWhat); counter(_block); return counter.references(); } -map ReferencesCounter::countReferences(FunctionDefinition const& _function) +map ReferencesCounter::countReferences(FunctionDefinition const& _function, CountWhat _countWhat) { - ReferencesCounter counter; + ReferencesCounter counter(_countWhat); counter(_function); return counter.references(); } -map ReferencesCounter::countReferences(Expression const& _expression) +map ReferencesCounter::countReferences(Expression const& _expression, CountWhat _countWhat) { - ReferencesCounter counter; + ReferencesCounter counter(_countWhat); counter.visit(_expression); return counter.references(); } diff --git a/libyul/optimiser/NameCollector.h b/libyul/optimiser/NameCollector.h index b6b4e1e6c..46debdcba 100644 --- a/libyul/optimiser/NameCollector.h +++ b/libyul/optimiser/NameCollector.h @@ -54,16 +54,23 @@ private: class ReferencesCounter: public ASTWalker { public: + enum CountWhat { VariablesAndFunctions, OnlyVariables }; + + explicit ReferencesCounter(CountWhat _countWhat = VariablesAndFunctions): + m_countWhat(_countWhat) + {} + using ASTWalker::operator (); virtual void operator()(Identifier const& _identifier); virtual void operator()(FunctionCall const& _funCall); - static std::map countReferences(Block const& _block); - static std::map countReferences(FunctionDefinition const& _function); - static std::map countReferences(Expression const& _expression); + static std::map countReferences(Block const& _block, CountWhat _countWhat = VariablesAndFunctions); + static std::map countReferences(FunctionDefinition const& _function, CountWhat _countWhat = VariablesAndFunctions); + static std::map countReferences(Expression const& _expression, CountWhat _countWhat = VariablesAndFunctions); std::map const& references() const { return m_references; } private: + CountWhat m_countWhat = CountWhat::VariablesAndFunctions; std::map m_references; }; diff --git a/libyul/optimiser/SSAValueTracker.cpp b/libyul/optimiser/SSAValueTracker.cpp index d4feacbd9..3b599644c 100644 --- a/libyul/optimiser/SSAValueTracker.cpp +++ b/libyul/optimiser/SSAValueTracker.cpp @@ -49,6 +49,16 @@ void SSAValueTracker::operator()(VariableDeclaration const& _varDecl) setValue(_varDecl.variables.front().name, _varDecl.value.get()); } +set SSAValueTracker::ssaVariables(Block const& _ast) +{ + SSAValueTracker t; + t(_ast); + set ssaVars; + for (auto const& value: t.values()) + ssaVars.insert(value.first); + return ssaVars; +} + void SSAValueTracker::setValue(YulString _name, Expression const* _value) { assertThrow( diff --git a/libyul/optimiser/SSAValueTracker.h b/libyul/optimiser/SSAValueTracker.h index 1062ca8e7..7eac6b6a4 100644 --- a/libyul/optimiser/SSAValueTracker.h +++ b/libyul/optimiser/SSAValueTracker.h @@ -49,6 +49,8 @@ public: std::map const& values() const { return m_values; } Expression const* value(YulString _name) const { return m_values.at(_name); } + static std::set ssaVariables(Block const& _ast); + private: void setValue(YulString _name, Expression const* _value); diff --git a/libyul/optimiser/Suite.cpp b/libyul/optimiser/Suite.cpp index e35146e93..054aaf30c 100644 --- a/libyul/optimiser/Suite.cpp +++ b/libyul/optimiser/Suite.cpp @@ -52,6 +52,7 @@ #include #include #include +#include #include #include #include @@ -129,7 +130,8 @@ void OptimiserSuite::run( RedundantAssignEliminator::name, ExpressionSimplifier::name, CommonSubexpressionEliminator::name, - LoadResolver::name + LoadResolver::name, + LoopInvariantCodeMotion::name }, ast); } @@ -345,6 +347,7 @@ map> const& OptimiserSuite::allSteps() FunctionHoister, LiteralRematerialiser, LoadResolver, + LoopInvariantCodeMotion, RedundantAssignEliminator, Rematerialiser, SSAReverser, diff --git a/scripts/codespell_whitelist.txt b/scripts/codespell_whitelist.txt index bbfe3e05c..0409dc1a2 100644 --- a/scripts/codespell_whitelist.txt +++ b/scripts/codespell_whitelist.txt @@ -10,3 +10,4 @@ fo compilability errorstring hist +otion diff --git a/test/libyul/YulOptimizerTest.cpp b/test/libyul/YulOptimizerTest.cpp index 07dc8a5c3..39ab5d0fb 100644 --- a/test/libyul/YulOptimizerTest.cpp +++ b/test/libyul/YulOptimizerTest.cpp @@ -41,6 +41,7 @@ #include #include #include +#include #include #include #include @@ -279,6 +280,12 @@ TestCase::TestResult YulOptimizerTest::run(ostream& _stream, string const& _line ExpressionJoiner::run(*m_context, *m_ast); ExpressionJoiner::run(*m_context, *m_ast); } + else if (m_optimizerStep == "loopInvariantCodeMotion") + { + disambiguate(); + ForLoopInitRewriter::run(*m_context, *m_ast); + LoopInvariantCodeMotion::run(*m_context, *m_ast); + } else if (m_optimizerStep == "controlFlowSimplifier") { disambiguate(); diff --git a/test/libyul/yulOptimizerTests/fullSuite/abi_example1.yul b/test/libyul/yulOptimizerTests/fullSuite/abi_example1.yul index 4838eab02..0d37cf1b9 100644 --- a/test/libyul/yulOptimizerTests/fullSuite/abi_example1.yul +++ b/test/libyul/yulOptimizerTests/fullSuite/abi_example1.yul @@ -483,7 +483,7 @@ // let _5 := 0xffffffffffffffff // if gt(offset, _5) { revert(_1, _1) } // let value2 := abi_decode_t_array$_t_uint256_$dyn_memory_ptr(add(_4, offset), _3) -// let offset_1 := calldataload(add(_4, 96)) +// let offset_1 := calldataload(add(_4, 0x60)) // if gt(offset_1, _5) { revert(_1, _1) } // let value3 := abi_decode_t_array$_t_array$_t_uint256_$2_memory_$dyn_memory_ptr(add(_4, offset_1), _3) // sstore(calldataload(_4), calldataload(add(_4, 0x20))) diff --git a/test/libyul/yulOptimizerTests/fullSuite/aztec.yul b/test/libyul/yulOptimizerTests/fullSuite/aztec.yul index 0b82de7e9..35e1bb7b3 100644 --- a/test/libyul/yulOptimizerTests/fullSuite/aztec.yul +++ b/test/libyul/yulOptimizerTests/fullSuite/aztec.yul @@ -311,7 +311,7 @@ // } // b := add(b, _5) // } -// if lt(m, n) { validatePairing(0x64) } +// if lt(m, n) { validatePairing(100) } // if iszero(eq(mod(keccak256(0x2a0, add(b, not(671))), _2), challenge)) // { // mstore(0, 404) diff --git a/test/libyul/yulOptimizerTests/fullSuite/clear_after_if_continue.yul b/test/libyul/yulOptimizerTests/fullSuite/clear_after_if_continue.yul index 19816b445..d958a318f 100644 --- a/test/libyul/yulOptimizerTests/fullSuite/clear_after_if_continue.yul +++ b/test/libyul/yulOptimizerTests/fullSuite/clear_after_if_continue.yul @@ -12,7 +12,7 @@ // { // { // let y := mload(0x20) -// for { } and(y, 8) { if y { revert(0, 0) } } +// for { } iszero(iszero(and(y, 8))) { if y { revert(0, 0) } } // { // if y { continue } // sstore(1, 0) diff --git a/test/libyul/yulOptimizerTests/fullSuite/loopInvariantCodeMotion.yul b/test/libyul/yulOptimizerTests/fullSuite/loopInvariantCodeMotion.yul new file mode 100644 index 000000000..f570d2ecf --- /dev/null +++ b/test/libyul/yulOptimizerTests/fullSuite/loopInvariantCodeMotion.yul @@ -0,0 +1,34 @@ +{ + sstore(0, array_sum(calldataload(0))) + + function array_sum(x) -> sum { + let length := calldataload(x) + for { let i := 0 } lt(i, length) { i := add(i, 1) } { + sum := add(sum, array_load(x, i)) + } + } + function array_load(x, i) -> v { + let len := calldataload(x) + if iszero(lt(i, len)) { revert(0, 0) } + let data := add(x, 0x20) + v := calldataload(add(data, mul(i, 0x20))) + // this is just to have some additional code that + // can be moved out of the loop. + v := add(v, calldataload(7)) + } +} +// ==== +// step: fullSuite +// ---- +// { +// { +// let _1 := calldataload(0) +// let sum := 0 +// let i := sum +// for { } lt(i, calldataload(_1)) { i := add(i, 1) } +// { +// sum := add(sum, add(calldataload(add(add(_1, mul(i, 0x20)), 0x20)), calldataload(7))) +// } +// sstore(0, sum) +// } +// } diff --git a/test/libyul/yulOptimizerTests/loopInvariantCodeMotion/dependOnVarInLoop.yul b/test/libyul/yulOptimizerTests/loopInvariantCodeMotion/dependOnVarInLoop.yul new file mode 100644 index 000000000..82ea2bf78 --- /dev/null +++ b/test/libyul/yulOptimizerTests/loopInvariantCodeMotion/dependOnVarInLoop.yul @@ -0,0 +1,23 @@ +{ + let b := 1 + for { let a := 1 } iszero(eq(a, 10)) { a := add(a, 1) } { + let c := mload(3) // c cannot be moved because non-movable + let not_inv := add(b, c) // no_inv cannot be moved because its value depends on c + a := add(a, 1) + mstore(a, not_inv) + } +} +// ==== +// step: loopInvariantCodeMotion +// ---- +// { +// let b := 1 +// let a := 1 +// for { } iszero(eq(a, 10)) { a := add(a, 1) } +// { +// let c := mload(3) +// let not_inv := add(b, c) +// a := add(a, 1) +// mstore(a, not_inv) +// } +// } diff --git a/test/libyul/yulOptimizerTests/loopInvariantCodeMotion/multi.yul b/test/libyul/yulOptimizerTests/loopInvariantCodeMotion/multi.yul new file mode 100644 index 000000000..c7e39cc70 --- /dev/null +++ b/test/libyul/yulOptimizerTests/loopInvariantCodeMotion/multi.yul @@ -0,0 +1,26 @@ +{ + let b := 1 + // tests if c, d, and inv can be moved outside in single pass + for { let a := 1 } iszero(eq(a, 10)) { a := add(a, 1) } { + let c := b + let d := mul(c, 2) + let inv := add(c, d) + a := add(a, 1) + mstore(a, inv) + } +} +// ==== +// step: loopInvariantCodeMotion +// ---- +// { +// let b := 1 +// let a := 1 +// let c := b +// let d := mul(c, 2) +// let inv := add(c, d) +// for { } iszero(eq(a, 10)) { a := add(a, 1) } +// { +// a := add(a, 1) +// mstore(a, inv) +// } +// } diff --git a/test/libyul/yulOptimizerTests/loopInvariantCodeMotion/non-ssavar.yul b/test/libyul/yulOptimizerTests/loopInvariantCodeMotion/non-ssavar.yul new file mode 100644 index 000000000..86cf1274e --- /dev/null +++ b/test/libyul/yulOptimizerTests/loopInvariantCodeMotion/non-ssavar.yul @@ -0,0 +1,23 @@ +{ + let b := 1 + for { let a := 1 } iszero(eq(a, 10)) { a := add(a, 1) } { + let not_inv := add(b, 42) + not_inv := add(not_inv, 1) + a := add(a, 1) + mstore(a, not_inv) + } +} +// ==== +// step: loopInvariantCodeMotion +// ---- +// { +// let b := 1 +// let a := 1 +// for { } iszero(eq(a, 10)) { a := add(a, 1) } +// { +// let not_inv := add(b, 42) +// not_inv := add(not_inv, 1) +// a := add(a, 1) +// mstore(a, not_inv) +// } +// } diff --git a/test/libyul/yulOptimizerTests/loopInvariantCodeMotion/nonMovable.yul b/test/libyul/yulOptimizerTests/loopInvariantCodeMotion/nonMovable.yul new file mode 100644 index 000000000..787c1756b --- /dev/null +++ b/test/libyul/yulOptimizerTests/loopInvariantCodeMotion/nonMovable.yul @@ -0,0 +1,21 @@ +{ + let b := 0 + for { let a := 1 } iszero(eq(a, 10)) { a := add(a, 1) } { + let inv := mload(b) + a := add(a, 1) + mstore(a, inv) + } +} +// ==== +// step: loopInvariantCodeMotion +// ---- +// { +// let b := 0 +// let a := 1 +// for { } iszero(eq(a, 10)) { a := add(a, 1) } +// { +// let inv := mload(b) +// a := add(a, 1) +// mstore(a, inv) +// } +// } diff --git a/test/libyul/yulOptimizerTests/loopInvariantCodeMotion/recursive.yul b/test/libyul/yulOptimizerTests/loopInvariantCodeMotion/recursive.yul new file mode 100644 index 000000000..a489b134b --- /dev/null +++ b/test/libyul/yulOptimizerTests/loopInvariantCodeMotion/recursive.yul @@ -0,0 +1,25 @@ +{ + let b := 1 + for { let a := 1 } iszero(eq(a, 10)) { a := add(a, 1) } { + for { let a2 := 1 } iszero(eq(a2, 10)) { a2 := add(a2, 1) } { + let inv := add(b, 42) + mstore(a, inv) + } + a := add(a, 1) + } +} +// ==== +// step: loopInvariantCodeMotion +// ---- +// { +// let b := 1 +// let a := 1 +// let inv := add(b, 42) +// for { } iszero(eq(a, 10)) { a := add(a, 1) } +// { +// let a2 := 1 +// for { } iszero(eq(a2, 10)) { a2 := add(a2, 1) } +// { mstore(a, inv) } +// a := add(a, 1) +// } +// } diff --git a/test/libyul/yulOptimizerTests/loopInvariantCodeMotion/simple.yul b/test/libyul/yulOptimizerTests/loopInvariantCodeMotion/simple.yul new file mode 100644 index 000000000..970fec59f --- /dev/null +++ b/test/libyul/yulOptimizerTests/loopInvariantCodeMotion/simple.yul @@ -0,0 +1,21 @@ +{ + let b := 1 + for { let a := 1 } iszero(eq(a, 10)) { a := add(a, 1) } { + let inv := add(b, 42) + a := add(a, 1) + mstore(a, inv) + } +} +// ==== +// step: loopInvariantCodeMotion +// ---- +// { +// let b := 1 +// let a := 1 +// let inv := add(b, 42) +// for { } iszero(eq(a, 10)) { a := add(a, 1) } +// { +// a := add(a, 1) +// mstore(a, inv) +// } +// } diff --git a/test/tools/yulopti.cpp b/test/tools/yulopti.cpp index e77cbf02b..8534115c5 100644 --- a/test/tools/yulopti.cpp +++ b/test/tools/yulopti.cpp @@ -62,6 +62,7 @@ #include #include #include +#include #include @@ -142,7 +143,7 @@ public: cout << " (r)edundant assign elim./re(m)aterializer/f(o)r-loop-init-rewriter/for-loop-condition-(I)nto-body/" << endl; cout << " for-loop-condition-(O)ut-of-body/s(t)ructural simplifier/equi(v)alent function combiner/ssa re(V)erser/" << endl; cout << " co(n)trol flow simplifier/stack com(p)ressor/(D)ead code eliminator/(L)oad resolver/" << endl; - cout << " (C)onditional simplifier?" << endl; + cout << " (C)onditional simplifier/loop-invariant code (M)otion?" << endl; cout.flush(); int option = readStandardInputChar(); cout << ' ' << char(option) << endl; @@ -237,6 +238,9 @@ public: case 'L': LoadResolver::run(context, *m_ast); break; + case 'M': + LoopInvariantCodeMotion::run(context, *m_ast); + break; default: cout << "Unknown option." << endl; }