solidity/libyul/optimiser/Suite.cpp
Martin Blicha 29041c8101 Remove ReasoningBasedSimplifier from libyul
Due to a design decision to move away from a bundled SMT solver,
ReasoningBasedSimplifier in its current form cannot be use any longer.
This is a necessary step to allow a unified way to call solvers using
only SMTLIB interface.

Since this optimization pass has always been marked as highly
experimental and never turned on by default, it should be OK to remove
it.
2023-06-28 14:38:36 +02:00

508 lines
16 KiB
C++

/*
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 <http://www.gnu.org/licenses/>.
*/
// SPDX-License-Identifier: GPL-3.0
/**
* Optimiser suite that combines all steps and also provides the settings for the heuristics.
*/
#include <libyul/optimiser/Suite.h>
#include <libyul/optimiser/Disambiguator.h>
#include <libyul/optimiser/VarDeclInitializer.h>
#include <libyul/optimiser/BlockFlattener.h>
#include <libyul/optimiser/CallGraphGenerator.h>
#include <libyul/optimiser/CircularReferencesPruner.h>
#include <libyul/optimiser/ControlFlowSimplifier.h>
#include <libyul/optimiser/ConditionalSimplifier.h>
#include <libyul/optimiser/ConditionalUnsimplifier.h>
#include <libyul/optimiser/DeadCodeEliminator.h>
#include <libyul/optimiser/FunctionGrouper.h>
#include <libyul/optimiser/FunctionHoister.h>
#include <libyul/optimiser/EqualStoreEliminator.h>
#include <libyul/optimiser/EquivalentFunctionCombiner.h>
#include <libyul/optimiser/ExpressionSplitter.h>
#include <libyul/optimiser/ExpressionJoiner.h>
#include <libyul/optimiser/ExpressionInliner.h>
#include <libyul/optimiser/FullInliner.h>
#include <libyul/optimiser/ForLoopConditionIntoBody.h>
#include <libyul/optimiser/ForLoopConditionOutOfBody.h>
#include <libyul/optimiser/ForLoopInitRewriter.h>
#include <libyul/optimiser/ForLoopConditionIntoBody.h>
#include <libyul/optimiser/FunctionSpecializer.h>
#include <libyul/optimiser/Rematerialiser.h>
#include <libyul/optimiser/UnusedFunctionParameterPruner.h>
#include <libyul/optimiser/UnusedPruner.h>
#include <libyul/optimiser/ExpressionSimplifier.h>
#include <libyul/optimiser/CommonSubexpressionEliminator.h>
#include <libyul/optimiser/Semantics.h>
#include <libyul/optimiser/SSAReverser.h>
#include <libyul/optimiser/SSATransform.h>
#include <libyul/optimiser/StackCompressor.h>
#include <libyul/optimiser/StackLimitEvader.h>
#include <libyul/optimiser/StructuralSimplifier.h>
#include <libyul/optimiser/SyntacticalEquality.h>
#include <libyul/optimiser/UnusedAssignEliminator.h>
#include <libyul/optimiser/UnusedStoreEliminator.h>
#include <libyul/optimiser/VarNameCleaner.h>
#include <libyul/optimiser/LoadResolver.h>
#include <libyul/optimiser/LoopInvariantCodeMotion.h>
#include <libyul/optimiser/Metrics.h>
#include <libyul/optimiser/NameSimplifier.h>
#include <libyul/backends/evm/ConstantOptimiser.h>
#include <libyul/AsmAnalysis.h>
#include <libyul/AsmAnalysisInfo.h>
#include <libyul/AsmPrinter.h>
#include <libyul/AST.h>
#include <libyul/Object.h>
#include <libyul/backends/evm/NoOutputAssembly.h>
#include <libsolutil/CommonData.h>
#include <libyul/CompilabilityChecker.h>
#include <range/v3/view/map.hpp>
#include <range/v3/action/remove.hpp>
#include <limits>
#include <tuple>
#ifdef PROFILE_OPTIMIZER_STEPS
#include <chrono>
#include <fmt/format.h>
#endif
using namespace std;
using namespace solidity;
using namespace solidity::yul;
#ifdef PROFILE_OPTIMIZER_STEPS
using namespace std::chrono;
#endif
namespace
{
#ifdef PROFILE_OPTIMIZER_STEPS
void outputPerformanceMetrics(map<string, int64_t> const& _metrics)
{
vector<pair<string, int64_t>> durations(_metrics.begin(), _metrics.end());
sort(
durations.begin(),
durations.end(),
[](pair<string, int64_t> const& _lhs, pair<string, int64_t> const& _rhs) -> bool
{
return _lhs.second < _rhs.second;
}
);
int64_t totalDurationInMicroseconds = 0;
for (auto&& [step, durationInMicroseconds]: durations)
totalDurationInMicroseconds += durationInMicroseconds;
cerr << "Performance metrics of optimizer steps" << endl;
cerr << "======================================" << endl;
constexpr double microsecondsInSecond = 1000000;
for (auto&& [step, durationInMicroseconds]: durations)
{
double percentage = 100.0 * static_cast<double>(durationInMicroseconds) / static_cast<double>(totalDurationInMicroseconds);
double sec = static_cast<double>(durationInMicroseconds) / microsecondsInSecond;
cerr << fmt::format("{:>7.3f}% ({} s): {}", percentage, sec, step) << endl;
}
double totalDurationInSeconds = static_cast<double>(totalDurationInMicroseconds) / microsecondsInSecond;
cerr << "--------------------------------------" << endl;
cerr << fmt::format("{:>7}% ({:.3f} s)", 100, totalDurationInSeconds) << endl;
}
#endif
}
void OptimiserSuite::run(
Dialect const& _dialect,
GasMeter const* _meter,
Object& _object,
bool _optimizeStackAllocation,
string_view _optimisationSequence,
string_view _optimisationCleanupSequence,
optional<size_t> _expectedExecutionsPerDeployment,
set<YulString> const& _externallyUsedIdentifiers
)
{
EVMDialect const* evmDialect = dynamic_cast<EVMDialect const*>(&_dialect);
bool usesOptimizedCodeGenerator =
_optimizeStackAllocation &&
evmDialect &&
evmDialect->evmVersion().canOverchargeGasForCall() &&
evmDialect->providesObjectAccess();
set<YulString> reservedIdentifiers = _externallyUsedIdentifiers;
reservedIdentifiers += _dialect.fixedFunctionNames();
*_object.code = std::get<Block>(Disambiguator(
_dialect,
*_object.analysisInfo,
reservedIdentifiers
)(*_object.code));
Block& ast = *_object.code;
NameDispenser dispenser{_dialect, ast, reservedIdentifiers};
OptimiserStepContext context{_dialect, dispenser, reservedIdentifiers, _expectedExecutionsPerDeployment};
OptimiserSuite suite(context, Debug::None);
// Some steps depend on properties ensured by FunctionHoister, BlockFlattener, FunctionGrouper and
// ForLoopInitRewriter. Run them first to be able to run arbitrary sequences safely.
suite.runSequence("hgfo", ast);
NameSimplifier::run(suite.m_context, ast);
// Now the user-supplied part
suite.runSequence(_optimisationSequence, ast);
// This is a tuning parameter, but actually just prevents infinite loops.
size_t stackCompressorMaxIterations = 16;
suite.runSequence("g", ast);
// We ignore the return value because we will get a much better error
// message once we perform code generation.
if (!usesOptimizedCodeGenerator)
StackCompressor::run(
_dialect,
_object,
_optimizeStackAllocation,
stackCompressorMaxIterations
);
// Run the user-supplied clean up sequence
suite.runSequence(_optimisationCleanupSequence, ast);
// Hard-coded FunctionGrouper step is used to bring the AST into a canonical form required by the StackCompressor
// and StackLimitEvader. This is hard-coded as the last step, as some previously executed steps may break the
// aforementioned form, thus causing the StackCompressor/StackLimitEvader to throw.
suite.runSequence("g", ast);
if (evmDialect)
{
yulAssert(_meter, "");
ConstantOptimiser{*evmDialect, *_meter}(ast);
if (usesOptimizedCodeGenerator)
{
StackCompressor::run(
_dialect,
_object,
_optimizeStackAllocation,
stackCompressorMaxIterations
);
if (evmDialect->providesObjectAccess())
StackLimitEvader::run(suite.m_context, _object);
}
else if (evmDialect->providesObjectAccess() && _optimizeStackAllocation)
StackLimitEvader::run(suite.m_context, _object);
}
dispenser.reset(ast);
NameSimplifier::run(suite.m_context, ast);
VarNameCleaner::run(suite.m_context, ast);
#ifdef PROFILE_OPTIMIZER_STEPS
outputPerformanceMetrics(suite.m_durationPerStepInMicroseconds);
#endif
*_object.analysisInfo = AsmAnalyzer::analyzeStrictAssertCorrect(_dialect, _object);
}
namespace
{
template <class... Step>
map<string, unique_ptr<OptimiserStep>> optimiserStepCollection()
{
map<string, unique_ptr<OptimiserStep>> ret;
for (unique_ptr<OptimiserStep>& s: util::make_vector<unique_ptr<OptimiserStep>>(
(make_unique<OptimiserStepInstance<Step>>())...
))
{
yulAssert(!ret.count(s->name), "");
ret[s->name] = std::move(s);
}
return ret;
}
}
map<string, unique_ptr<OptimiserStep>> const& OptimiserSuite::allSteps()
{
static map<string, unique_ptr<OptimiserStep>> instance;
if (instance.empty())
instance = optimiserStepCollection<
BlockFlattener,
CircularReferencesPruner,
CommonSubexpressionEliminator,
ConditionalSimplifier,
ConditionalUnsimplifier,
ControlFlowSimplifier,
DeadCodeEliminator,
EqualStoreEliminator,
EquivalentFunctionCombiner,
ExpressionInliner,
ExpressionJoiner,
ExpressionSimplifier,
ExpressionSplitter,
ForLoopConditionIntoBody,
ForLoopConditionOutOfBody,
ForLoopInitRewriter,
FullInliner,
FunctionGrouper,
FunctionHoister,
FunctionSpecializer,
LiteralRematerialiser,
LoadResolver,
LoopInvariantCodeMotion,
UnusedAssignEliminator,
UnusedStoreEliminator,
Rematerialiser,
SSAReverser,
SSATransform,
StructuralSimplifier,
UnusedFunctionParameterPruner,
UnusedPruner,
VarDeclInitializer
>();
// Does not include VarNameCleaner because it destroys the property of unique names.
// Does not include NameSimplifier.
return instance;
}
map<string, char> const& OptimiserSuite::stepNameToAbbreviationMap()
{
static map<string, char> lookupTable{
{BlockFlattener::name, 'f'},
{CircularReferencesPruner::name, 'l'},
{CommonSubexpressionEliminator::name, 'c'},
{ConditionalSimplifier::name, 'C'},
{ConditionalUnsimplifier::name, 'U'},
{ControlFlowSimplifier::name, 'n'},
{DeadCodeEliminator::name, 'D'},
{EqualStoreEliminator::name, 'E'},
{EquivalentFunctionCombiner::name, 'v'},
{ExpressionInliner::name, 'e'},
{ExpressionJoiner::name, 'j'},
{ExpressionSimplifier::name, 's'},
{ExpressionSplitter::name, 'x'},
{ForLoopConditionIntoBody::name, 'I'},
{ForLoopConditionOutOfBody::name, 'O'},
{ForLoopInitRewriter::name, 'o'},
{FullInliner::name, 'i'},
{FunctionGrouper::name, 'g'},
{FunctionHoister::name, 'h'},
{FunctionSpecializer::name, 'F'},
{LiteralRematerialiser::name, 'T'},
{LoadResolver::name, 'L'},
{LoopInvariantCodeMotion::name, 'M'},
{UnusedAssignEliminator::name, 'r'},
{UnusedStoreEliminator::name, 'S'},
{Rematerialiser::name, 'm'},
{SSAReverser::name, 'V'},
{SSATransform::name, 'a'},
{StructuralSimplifier::name, 't'},
{UnusedFunctionParameterPruner::name, 'p'},
{UnusedPruner::name, 'u'},
{VarDeclInitializer::name, 'd'},
};
yulAssert(lookupTable.size() == allSteps().size(), "");
yulAssert((
util::convertContainer<set<char>>(string(NonStepAbbreviations)) -
util::convertContainer<set<char>>(lookupTable | ranges::views::values)
).size() == string(NonStepAbbreviations).size(),
"Step abbreviation conflicts with a character reserved for another syntactic element"
);
return lookupTable;
}
map<char, string> const& OptimiserSuite::stepAbbreviationToNameMap()
{
static map<char, string> lookupTable = util::invertMap(stepNameToAbbreviationMap());
return lookupTable;
}
void OptimiserSuite::validateSequence(string_view _stepAbbreviations)
{
int8_t nestingLevel = 0;
int8_t colonDelimiters = 0;
for (char abbreviation: _stepAbbreviations)
switch (abbreviation)
{
case ' ':
case '\n':
break;
case '[':
assertThrow(nestingLevel < numeric_limits<int8_t>::max(), OptimizerException, "Brackets nested too deep");
nestingLevel++;
break;
case ']':
nestingLevel--;
assertThrow(nestingLevel >= 0, OptimizerException, "Unbalanced brackets");
break;
case ':':
++colonDelimiters;
assertThrow(nestingLevel == 0, OptimizerException, "Cleanup sequence delimiter cannot be placed inside the brackets");
assertThrow(colonDelimiters <= 1, OptimizerException, "Too many cleanup sequence delimiters");
break;
default:
{
yulAssert(
string(NonStepAbbreviations).find(abbreviation) == string::npos,
"Unhandled syntactic element in the abbreviation sequence"
);
assertThrow(
stepAbbreviationToNameMap().find(abbreviation) != stepAbbreviationToNameMap().end(),
OptimizerException,
"'"s + abbreviation + "' is not a valid step abbreviation"
);
optional<string> invalid = allSteps().at(stepAbbreviationToNameMap().at(abbreviation))->invalidInCurrentEnvironment();
assertThrow(
!invalid.has_value(),
OptimizerException,
"'"s + abbreviation + "' is invalid in the current environment: " + *invalid
);
}
}
assertThrow(nestingLevel == 0, OptimizerException, "Unbalanced brackets");
}
void OptimiserSuite::runSequence(string_view _stepAbbreviations, Block& _ast, bool _repeatUntilStable)
{
validateSequence(_stepAbbreviations);
// This splits 'aaa[bbb]ccc...' into 'aaa' and '[bbb]ccc...'.
auto extractNonNestedPrefix = [](string_view _tail) -> tuple<string_view, string_view>
{
for (size_t i = 0; i < _tail.size(); ++i)
{
yulAssert(_tail[i] != ']');
if (_tail[i] == '[')
return {_tail.substr(0, i), _tail.substr(i)};
}
return {_tail, {}};
};
// This splits '[bbb]ccc...' into 'bbb' and 'ccc...'.
auto extractBracketContent = [](string_view _tail) -> tuple<string_view, string_view>
{
yulAssert(!_tail.empty() && _tail[0] == '[');
size_t contentLength = 0;
int8_t nestingLevel = 1;
for (char abbreviation: _tail.substr(1))
{
if (abbreviation == '[')
{
yulAssert(nestingLevel < numeric_limits<int8_t>::max());
++nestingLevel;
}
else if (abbreviation == ']')
{
--nestingLevel;
if (nestingLevel == 0)
break;
}
++contentLength;
}
yulAssert(nestingLevel == 0);
yulAssert(_tail[contentLength + 1] == ']');
return {_tail.substr(1, contentLength), _tail.substr(contentLength + 2)};
};
auto abbreviationsToSteps = [](string_view _sequence) -> vector<string>
{
vector<string> steps;
for (char abbreviation: _sequence)
if (abbreviation != ' ' && abbreviation != '\n')
steps.emplace_back(stepAbbreviationToNameMap().at(abbreviation));
return steps;
};
vector<tuple<string_view, bool>> subsequences;
string_view tail = _stepAbbreviations;
while (!tail.empty())
{
string_view subsequence;
tie(subsequence, tail) = extractNonNestedPrefix(tail);
if (subsequence.size() > 0)
subsequences.push_back({subsequence, false});
if (tail.empty())
break;
tie(subsequence, tail) = extractBracketContent(tail);
if (subsequence.size() > 0)
subsequences.push_back({subsequence, true});
}
size_t codeSize = 0;
for (size_t round = 0; round < MaxRounds; ++round)
{
for (auto const& [subsequence, repeat]: subsequences)
{
if (repeat)
runSequence(subsequence, _ast, true);
else
runSequence(abbreviationsToSteps(subsequence), _ast);
}
if (!_repeatUntilStable)
break;
size_t newSize = CodeSize::codeSizeIncludingFunctions(_ast);
if (newSize == codeSize)
break;
codeSize = newSize;
}
}
void OptimiserSuite::runSequence(std::vector<string> const& _steps, Block& _ast)
{
unique_ptr<Block> copy;
if (m_debug == Debug::PrintChanges)
copy = make_unique<Block>(std::get<Block>(ASTCopier{}(_ast)));
for (string const& step: _steps)
{
if (m_debug == Debug::PrintStep)
cout << "Running " << step << endl;
#ifdef PROFILE_OPTIMIZER_STEPS
steady_clock::time_point startTime = steady_clock::now();
#endif
allSteps().at(step)->run(m_context, _ast);
#ifdef PROFILE_OPTIMIZER_STEPS
steady_clock::time_point endTime = steady_clock::now();
m_durationPerStepInMicroseconds[step] += duration_cast<microseconds>(endTime - startTime).count();
#endif
if (m_debug == Debug::PrintChanges)
{
// TODO should add switch to also compare variable names!
if (SyntacticallyEqual{}.statementEqual(_ast, *copy))
cout << "== Running " << step << " did not cause changes." << endl;
else
{
cout << "== Running " << step << " changed the AST." << endl;
cout << AsmPrinter{}(_ast) << endl;
copy = make_unique<Block>(std::get<Block>(ASTCopier{}(_ast)));
}
}
}
}