-
Notifications
You must be signed in to change notification settings - Fork 6.1k
feat(yul): optimize switches with bisection #16845
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Open
wjmelements
wants to merge
11
commits into
argotorg:develop
Choose a base branch
from
wjmelements:yul-switch-tree
base: develop
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
+1,165
−138
Open
Changes from all commits
Commits
Show all changes
11 commits
Select commit
Hold shift + click to select a range
50bca3d
feat(yul): add SwitchSplitter optimizer pass
wjmelements fa6b156
test(yulPhaser): update abbreviation string for SwitchSplitter step
wjmelements 899d1e9
test: regenerate gas expectations for SwitchSplitter
wjmelements 65b8cdb
fix(yul): size SwitchSplitter overhead to the fulcrum's actual byte w…
wjmelements 77a2050
refactor(yul): size SwitchSplitter default-body overhead via CodeSize…
wjmelements aabc676
docs(yul): clarify ExpressionSplitter is not a hard prerequisite for …
wjmelements a6e0b6c
fix(yul): price SwitchSplitter's creation-code switches at the callda…
wjmelements 3d40321
docs: add changelog entry for SwitchSplitter optimization
wjmelements 0918a6a
Merge remote-tracking branch 'upstream/develop' into yul-switch-tree
wjmelements 6049337
fix(externalTests): lower zeppelin optimizer runs to fix contract siz…
wjmelements fce3939
test(semanticTests): update gas costs for storage boundary struct arr…
wjmelements File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Some comments aren't visible on the classic Files Changed page.
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,185 @@ | ||
| /* | ||
| 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 | ||
| #include <libyul/optimiser/SwitchSplitter.h> | ||
|
|
||
| #include <libyul/optimiser/ASTCopier.h> | ||
| #include <libyul/optimiser/Metrics.h> | ||
| #include <libyul/optimiser/OptimiserStep.h> | ||
| #include <libyul/AST.h> | ||
| #include <libyul/Dialect.h> | ||
| #include <libyul/backends/evm/EVMDialect.h> | ||
|
|
||
| #include <libevmasm/GasMeter.h> | ||
| #include <libsolutil/CommonData.h> | ||
| #include <libsolutil/Numeric.h> | ||
|
|
||
| #include <algorithm> | ||
| #include <optional> | ||
| #include <span> | ||
|
|
||
| using namespace solidity; | ||
| using namespace solidity::util; | ||
| using namespace solidity::yul; | ||
|
|
||
| using OptionalStatements = std::optional<std::vector<Statement>>; | ||
|
|
||
| void SwitchSplitter::run(OptimiserStepContext& _context, Block& _ast) | ||
| { | ||
| SwitchSplitter{_context}(_ast); | ||
| } | ||
|
|
||
| SwitchSplitter::SwitchSplitter(OptimiserStepContext const& _context): | ||
| // nullopt means creation code | ||
| m_runs(_context.expectedExecutionsPerDeployment.value_or(1)), | ||
| m_isCreation(!_context.expectedExecutionsPerDeployment) | ||
| { | ||
| if (auto const* evmDialect = dynamic_cast<EVMDialect const*>(&_context.dialect)) | ||
| { | ||
| m_gtHandle = evmDialect->findBuiltin("gt"); | ||
| m_evmVersion = evmDialect->evmVersion(); | ||
| } | ||
| } | ||
|
|
||
| void SwitchSplitter::operator()(Block& _block) | ||
| { | ||
| simplify(_block.statements); | ||
| } | ||
|
|
||
| void SwitchSplitter::simplify(std::vector<Statement>& _statements) | ||
| { | ||
| iterateReplacing( | ||
| _statements, | ||
| [&](Statement& _stmt) -> OptionalStatements | ||
| { | ||
| if (auto* sw = std::get_if<Switch>(&_stmt)) | ||
| if (auto result = tryTransform(*sw)) | ||
| { | ||
| simplify(*result); | ||
| return result; | ||
| } | ||
| this->visit(_stmt); | ||
| return {}; | ||
| } | ||
| ); | ||
| } | ||
|
|
||
| std::optional<std::vector<Statement>> SwitchSplitter::tryTransform(Switch& _switch) | ||
| { | ||
| if (!m_gtHandle) | ||
| return {}; | ||
|
|
||
| // Only switches on a simple identifier are transformed; without ExpressionSplitter | ||
| // having run first, more complex expressions are left untouched here. | ||
| auto const* exprIdent = std::get_if<Identifier>(_switch.expression.get()); | ||
| if (!exprIdent) | ||
| return {}; | ||
|
|
||
| // Separate literal cases from the default case. | ||
| std::vector<Case const*> literalCases; | ||
| Block const* defaultBody = nullptr; | ||
| static Block const emptyBlock{}; | ||
|
|
||
| for (auto const& c: _switch.cases) | ||
| if (c.value) | ||
| literalCases.push_back(&c); | ||
| else | ||
| defaultBody = &c.body; | ||
|
|
||
| if (!defaultBody) | ||
| defaultBody = &emptyBlock; | ||
|
|
||
| std::sort(literalCases.begin(), literalCases.end(), [](Case const* _a, Case const* _b) { | ||
| return _a->value->value.value() < _b->value->value.value(); | ||
| }); | ||
|
|
||
| // Only transform if the top-level split is profitable. | ||
| if (!shouldSplit(literalCases, *defaultBody)) | ||
| return {}; | ||
|
|
||
| return buildTree(literalCases, *exprIdent, *defaultBody, _switch.debugData); | ||
| } | ||
|
|
||
| std::vector<Statement> SwitchSplitter::buildTree( | ||
| std::span<Case const* const> _cases, | ||
| Identifier const& _expr, | ||
| Block const& _defaultBody, | ||
| langutil::DebugData::ConstPtr _debugData | ||
| ) | ||
| { | ||
| size_t n = _cases.size(); | ||
| bool const hasDefault = !_defaultBody.statements.empty(); | ||
|
|
||
| if (!shouldSplit(_cases, _defaultBody)) | ||
| { | ||
| // Leaf: small switch; default case added only when present. | ||
| std::vector<Case> switchCases; | ||
| switchCases.reserve(n + (hasDefault ? 1 : 0)); | ||
| for (auto const* c: _cases) | ||
| switchCases.push_back(Case{ | ||
| c->debugData, | ||
| std::make_unique<Literal>(*c->value), | ||
| ASTCopier{}.translate(c->body) | ||
| }); | ||
| if (hasDefault) | ||
| switchCases.push_back(Case{_debugData, nullptr, ASTCopier{}.translate(_defaultBody)}); | ||
| std::vector<Statement> result; | ||
| result.emplace_back(Switch{_debugData, std::make_unique<Expression>(_expr), std::move(switchCases)}); | ||
| return result; | ||
| } | ||
|
|
||
| // Split: pivot stays in lower half → switch gt(expr, pivot) case 1 { upper } default { lower }. | ||
| size_t pivotIdx = (n - 1) / 2; | ||
| Case const* pivot = _cases[pivotIdx]; | ||
|
|
||
| auto lowerStmts = buildTree(_cases.subspan(0, pivotIdx + 1), _expr, _defaultBody, _debugData); | ||
| auto upperStmts = buildTree(_cases.subspan(pivotIdx + 1), _expr, _defaultBody, _debugData); | ||
|
|
||
| std::vector<Case> switchCases; | ||
| switchCases.reserve(2); | ||
| switchCases.push_back(Case{ | ||
| _debugData, | ||
| std::make_unique<Literal>(Literal{_debugData, LiteralKind::Number, LiteralValue{u256(1)}}), | ||
| Block{_debugData, std::move(upperStmts)} | ||
| }); | ||
| switchCases.push_back(Case{_debugData, nullptr, Block{_debugData, std::move(lowerStmts)}}); | ||
|
|
||
| std::vector<Statement> result; | ||
| result.emplace_back(Switch{ | ||
| _debugData, | ||
| std::make_unique<Expression>(FunctionCall{ | ||
| _debugData, | ||
| BuiltinName{_debugData, *m_gtHandle}, | ||
| {Expression{_expr}, Expression{*pivot->value}} | ||
| }), | ||
| std::move(switchCases) | ||
| }); | ||
| return result; | ||
| } | ||
|
|
||
| bool SwitchSplitter::shouldSplit(std::span<Case const* const> _cases, Block const& _defaultBody) const | ||
| { | ||
| size_t const n = _cases.size(); | ||
| if (n <= 4) | ||
| return false; | ||
|
|
||
| // Overhead scales with the fulcrum's actual encoding size. | ||
| size_t const pivotIdx = (n - 1) / 2; | ||
| unsigned const fulcrumSize = numberEncodingSize(_cases[pivotIdx]->value->value.value()); | ||
| uint64_t const overhead = 13 + fulcrumSize + CodeSize::codeSize(_defaultBody); | ||
| return m_runs * 6 * (n - 4) > evmasm::GasMeter::dataGas(overhead, m_isCreation, m_evmVersion); | ||
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,98 @@ | ||
| /* | ||
| 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 | ||
| #pragma once | ||
|
|
||
| #include <libyul/optimiser/ASTWalker.h> | ||
| #include <libyul/optimiser/OptimiserStep.h> | ||
| #include <libyul/Dialect.h> | ||
|
|
||
| #include <liblangutil/DebugData.h> | ||
| #include <liblangutil/EVMVersion.h> | ||
|
|
||
| #include <optional> | ||
| #include <span> | ||
| #include <vector> | ||
|
|
||
| namespace solidity::yul | ||
| { | ||
|
|
||
| /** | ||
| * Transforms a large Yul switch statement into a binary search tree of nested switches, | ||
| * reducing runtime dispatch from O(n) to O(log n). | ||
| * | ||
| * The transformation is applied only when the runtime gas savings over | ||
| * expectedExecutionsPerDeployment executions outweigh the extra code size (creation gas): | ||
| * runs * 6 * (n - 4) > (13 + f + k) * createDataGas | ||
| * where f is the encoding size in bytes of the fulcrum (pivot) literal and k is the | ||
| * CodeSize estimate for the default body (proxy for bytecode size). The 13-byte base | ||
| * overhead comes from the gt-switch structure; k accounts for each split duplicating | ||
| * the default body into one additional leaf. | ||
| * | ||
| * This mirrors the formula ContractCompiler::appendInternalSelector uses to decide | ||
| * whether to binary-split the legacy function selector dispatch (ContractCompiler.cpp, | ||
| * see the comment above that function for the full per-opcode derivation of the 6 and 4 | ||
| * constants). That formula fixes the split overhead at 17 bytes because selectors are | ||
| * always 4-byte literals; here the fulcrum can be any literal, so the fixed 4 is broken | ||
| * out into f and the remaining 13-byte base overhead is unchanged (13 + 4 == 17). | ||
| * | ||
| * Applied recursively: each sub-slice re-evaluates the formula. | ||
| * The default case body is duplicated into each leaf branch when present. | ||
| * | ||
| * Prerequisite: Disambiguator. | ||
| * | ||
| * ExpressionSplitter is recommended, though not required for correctness: this step only | ||
| * transforms switches whose expression is already a simple identifier, so without | ||
| * ExpressionSplitter having run first, switches with more complex expressions are simply | ||
| * left untouched rather than causing incorrect output. | ||
| * | ||
| * Important: Can only be used on EVM code. | ||
| */ | ||
| class SwitchSplitter: public ASTModifier | ||
| { | ||
| public: | ||
| static constexpr char const* name{"SwitchSplitter"}; | ||
| static void run(OptimiserStepContext& _context, Block& _ast); | ||
|
|
||
| using ASTModifier::operator(); | ||
| void operator()(Block& _block) override; | ||
|
|
||
| private: | ||
| explicit SwitchSplitter(OptimiserStepContext const& _context); | ||
|
|
||
| void simplify(std::vector<Statement>& _statements); | ||
|
|
||
| // Returns replacement statements if the switch should be transformed, nullopt otherwise. | ||
| std::optional<std::vector<Statement>> tryTransform(Switch& _switch); | ||
|
|
||
| // Recursively builds the binary search tree for a sorted, non-empty slice of literal cases. | ||
| std::vector<Statement> buildTree( | ||
| std::span<Case const* const> _cases, | ||
| Identifier const& _expr, | ||
| Block const& _defaultBody, | ||
| langutil::DebugData::ConstPtr _debugData | ||
| ); | ||
|
|
||
| bool shouldSplit(std::span<Case const* const> _cases, Block const& _defaultBody) const; | ||
|
|
||
| std::optional<BuiltinHandle> m_gtHandle; | ||
| size_t m_runs = 0; | ||
| bool m_isCreation = false; | ||
| langutil::EVMVersion m_evmVersion; | ||
| }; | ||
|
|
||
| } | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Note that the encoding size is not fully known until ConstantOptimizer runs. And the exact workings of that component are subject to change (#16814).
If the step is simply using literals, then I guess we can assume we know the maximum encoding size, which might be good enough, but conservative.
BTW, what is the full reasoning behind this particular formula? Why 6 and 4 specifically?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Adding explanation in next commit:
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
aabc676