Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions Changelog.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,11 @@
Language Features:

Compiler Features:
* Commandline Interface: `--optimize-runs` now also accepts values from the interval [INT64_MAX, UINT64_MAX].
* General: Speed up SHA-256 hashing (`picosha2`).
* General: Remove support for the experimental EOF (EVM Object Format) backend.
* SMTChecker: Emit a deprecation warning for the BMC engine.
* Yul Optimizer: Split large switch statements into a binary search tree of smaller switches, reducing dispatch cost from linear to logarithmic.

Bugfixes:
* Code Generator: Fix ICE on parenthesized custom error construction in require statement.
Expand Down
4 changes: 2 additions & 2 deletions libsolidity/interface/OptimiserSettings.h
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@ struct OptimiserSettings
static char constexpr DefaultYulOptimiserSteps[] =
"dfDvulfnTUtnIf" // None of these can make stack problems worse

"xa[r]EscLM" // Turn into SSA and simplify
"xBa[r]EscLM" // Turn into SSA and simplify
"Vcul [j]" // Reverse SSA

// should have good "compilability" property here.
Expand All @@ -55,7 +55,7 @@ struct OptimiserSettings

"scCTUt"
"vifM" // Run full inliner
"x[scCTUt] TOntnfDIul" // Perform structural simplification
"x[scCTUtB] TOntnfDIul" // Perform structural simplification
"vifM" // Run full inliner

"jmul[jul] VcTOcul jmul"; // Make source short and pretty
Expand Down
2 changes: 2 additions & 0 deletions libyul/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -245,6 +245,8 @@ add_library(yul
optimiser/StackToMemoryMover.h
optimiser/StructuralSimplifier.cpp
optimiser/StructuralSimplifier.h
optimiser/SwitchSplitter.cpp
optimiser/SwitchSplitter.h
optimiser/Substitution.cpp
optimiser/Substitution.h
optimiser/Suite.cpp
Expand Down
3 changes: 3 additions & 0 deletions libyul/optimiser/Suite.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,7 @@
#include <libyul/optimiser/StackCompressor.h>
#include <libyul/optimiser/StackLimitEvader.h>
#include <libyul/optimiser/StructuralSimplifier.h>
#include <libyul/optimiser/SwitchSplitter.h>
#include <libyul/optimiser/SyntacticalEquality.h>
#include <libyul/optimiser/UnusedAssignEliminator.h>
#include <libyul/optimiser/UnusedStoreEliminator.h>
Expand Down Expand Up @@ -260,6 +261,7 @@ std::map<std::string, std::unique_ptr<OptimiserStep>> const& OptimiserSuite::all
SSAReverser,
SSATransform,
StructuralSimplifier,
SwitchSplitter,
UnusedFunctionParameterPruner,
UnusedPruner,
VarDeclInitializer
Expand Down Expand Up @@ -301,6 +303,7 @@ std::map<std::string, char> const& OptimiserSuite::stepNameToAbbreviationMap()
{SSAReverser::name, 'V'},
{SSATransform::name, 'a'},
{StructuralSimplifier::name, 't'},
{SwitchSplitter::name, 'B'},
{UnusedFunctionParameterPruner::name, 'p'},
{UnusedPruner::name, 'u'},
{VarDeclInitializer::name, 'd'},
Expand Down
185 changes: 185 additions & 0 deletions libyul/optimiser/SwitchSplitter.cpp
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);
}
98 changes: 98 additions & 0 deletions libyul/optimiser/SwitchSplitter.h
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

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Note 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?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Adding explanation in next commit:

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).

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

* 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;
};

}
5 changes: 3 additions & 2 deletions test/externalTests/zeppelin.sh
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@ function zeppelin_test
local repo="https://github.com/OpenZeppelin/openzeppelin-contracts.git"
local ref="<latest-release>"
local config_file="hardhat.config.js"
local extra_optimizer_settings="runs: 175"

local compile_only_presets=(
#ir-no-optimize # Compilation fails with "Contract initcode size is 49410 bytes and exceeds 49152 bytes (a limit introduced in Shanghai)."
Expand Down Expand Up @@ -129,13 +130,13 @@ EOF

neutralize_package_json_hooks
force_hardhat_compiler_binary "$config_file" "$BINARY_TYPE" "$BINARY_PATH"
force_hardhat_compiler_settings "$config_file" "$(first_word "$SELECTED_PRESETS")"
force_hardhat_compiler_settings "$config_file" "$(first_word "$SELECTED_PRESETS")" "" "$CURRENT_EVM_VERSION" "" "$extra_optimizer_settings"
npm install
npm install hardhat

replace_version_pragmas
for preset in $SELECTED_PRESETS; do
hardhat_run_test "$config_file" "$preset" "${compile_only_presets[*]}" compile_fn test_fn
hardhat_run_test "$config_file" "$preset" "${compile_only_presets[*]}" compile_fn test_fn "" "" "$extra_optimizer_settings"
store_benchmark_report hardhat zeppelin "$repo" "$preset"
done
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -41,10 +41,10 @@ contract c {
// ----
// getLengths() -> 0, 0
// setLengths(uint256,uint256): 48, 49 ->
// gas irOptimized: 112674
// gas irOptimized: 112710
// gas legacy: 108272
// gas legacyOptimized: 100268
// gas ssaCFGOptimized: 112668
// gas ssaCFGOptimized: 112699
// getLengths() -> 48, 49
// setIDStatic(uint256): 11 ->
// getID(uint256): 2 -> 11
Expand Down
Loading