mirror of
https://github.com/ethereum/solidity
synced 2023-10-03 13:03:40 +00:00
Add inlining for old optimizer.
This commit is contained in:
@@ -25,6 +25,7 @@
|
||||
#include <libevmasm/CommonSubexpressionEliminator.h>
|
||||
#include <libevmasm/ControlFlowGraph.h>
|
||||
#include <libevmasm/PeepholeOptimiser.h>
|
||||
#include <libevmasm/Inliner.h>
|
||||
#include <libevmasm/JumpdestRemover.h>
|
||||
#include <libevmasm/BlockDeduplicator.h>
|
||||
#include <libevmasm/ConstantOptimiser.h>
|
||||
@@ -375,6 +376,7 @@ Assembly& Assembly::optimise(bool _enable, EVMVersion _evmVersion, bool _isCreat
|
||||
{
|
||||
OptimiserSettings settings;
|
||||
settings.isCreation = _isCreation;
|
||||
settings.runInliner = true;
|
||||
settings.runJumpdestRemover = true;
|
||||
settings.runPeephole = true;
|
||||
if (_enable)
|
||||
@@ -421,6 +423,15 @@ map<u256, u256> Assembly::optimiseInternal(
|
||||
{
|
||||
count = 0;
|
||||
|
||||
if (_settings.runInliner)
|
||||
Inliner{
|
||||
m_items,
|
||||
_tagsReferencedFromOutside,
|
||||
_settings.expectedExecutionsPerDeployment,
|
||||
_settings.isCreation,
|
||||
_settings.evmVersion
|
||||
}.optimise();
|
||||
|
||||
if (_settings.runJumpdestRemover)
|
||||
{
|
||||
JumpdestRemover jumpdestOpt{m_items};
|
||||
|
||||
@@ -106,6 +106,7 @@ public:
|
||||
struct OptimiserSettings
|
||||
{
|
||||
bool isCreation = false;
|
||||
bool runInliner = false;
|
||||
bool runJumpdestRemover = false;
|
||||
bool runPeephole = false;
|
||||
bool runDeduplicate = false;
|
||||
|
||||
@@ -16,6 +16,8 @@ set(sources
|
||||
ExpressionClasses.h
|
||||
GasMeter.cpp
|
||||
GasMeter.h
|
||||
Inliner.cpp
|
||||
Inliner.h
|
||||
Instruction.cpp
|
||||
Instruction.h
|
||||
JumpdestRemover.cpp
|
||||
|
||||
@@ -286,3 +286,11 @@ u256 GasMeter::dataGas(bytes const& _data, bool _inCreation, langutil::EVMVersio
|
||||
assertThrow(gas < bigint(u256(-1)), OptimizerException, "Gas cost exceeds 256 bits.");
|
||||
return u256(gas);
|
||||
}
|
||||
|
||||
|
||||
u256 GasMeter::dataGas(uint64_t _length, bool _inCreation, langutil::EVMVersion _evmVersion)
|
||||
{
|
||||
bigint gas = bigint(_length) * (_inCreation ? GasCosts::txDataNonZeroGas(_evmVersion) : GasCosts::createDataGas);
|
||||
assertThrow(gas < bigint(u256(-1)), OptimizerException, "Gas cost exceeds 256 bits.");
|
||||
return u256(gas);
|
||||
}
|
||||
|
||||
@@ -125,6 +125,12 @@ public:
|
||||
static GasConsumption infinite() { return GasConsumption(0, true); }
|
||||
|
||||
GasConsumption& operator+=(GasConsumption const& _other);
|
||||
GasConsumption operator+(GasConsumption const& _other) const
|
||||
{
|
||||
GasConsumption result = *this;
|
||||
result += _other;
|
||||
return result;
|
||||
}
|
||||
bool operator<(GasConsumption const& _other) const
|
||||
{
|
||||
return std::make_pair(isInfinite, value) < std::make_pair(_other.isInfinite, _other.value);
|
||||
@@ -154,6 +160,11 @@ public:
|
||||
/// otherwise code will be stored and have to pay "createDataGas" cost.
|
||||
static u256 dataGas(bytes const& _data, bool _inCreation, langutil::EVMVersion _evmVersion);
|
||||
|
||||
/// @returns the gas cost of non-zero data of the supplied length, depending whether it is in creation code, or not.
|
||||
/// In case of @a _inCreation, the data is only sent as a transaction and is not stored, whereas
|
||||
/// otherwise code will be stored and have to pay "createDataGas" cost.
|
||||
static u256 dataGas(uint64_t _length, bool _inCreation, langutil::EVMVersion _evmVersion);
|
||||
|
||||
private:
|
||||
/// @returns _multiplier * (_value + 31) / 32, if _value is a known constant and infinite otherwise.
|
||||
GasConsumption wordGas(u256 const& _multiplier, ExpressionClasses::Id _value);
|
||||
|
||||
@@ -0,0 +1,259 @@
|
||||
/*
|
||||
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
|
||||
/**
|
||||
* @file Inliner.cpp
|
||||
* Inlines small code snippets by replacing JUMP with a copy of the code jumped to.
|
||||
*/
|
||||
|
||||
#include <libevmasm/Inliner.h>
|
||||
|
||||
#include <libevmasm/AssemblyItem.h>
|
||||
#include <libevmasm/GasMeter.h>
|
||||
#include <libevmasm/KnownState.h>
|
||||
#include <libevmasm/SemanticInformation.h>
|
||||
|
||||
#include <libsolutil/CommonData.h>
|
||||
|
||||
#include <range/v3/numeric/accumulate.hpp>
|
||||
#include <range/v3/view/drop_last.hpp>
|
||||
#include <range/v3/view/enumerate.hpp>
|
||||
#include <range/v3/view/slice.hpp>
|
||||
#include <range/v3/view/transform.hpp>
|
||||
|
||||
#include <optional>
|
||||
|
||||
|
||||
using namespace std;
|
||||
using namespace solidity;
|
||||
using namespace solidity::evmasm;
|
||||
|
||||
|
||||
namespace
|
||||
{
|
||||
/// @returns an estimation of the runtime gas cost of the AsssemblyItems in @a _itemRange.
|
||||
template<typename RangeType>
|
||||
u256 executionCost(RangeType const& _itemRange, langutil::EVMVersion _evmVersion)
|
||||
{
|
||||
GasMeter gasMeter{std::make_shared<KnownState>(), _evmVersion};
|
||||
auto gasConsumption = ranges::accumulate(_itemRange | ranges::views::transform(
|
||||
[&gasMeter](auto const& _item) { return gasMeter.estimateMax(_item, false); }
|
||||
), GasMeter::GasConsumption());
|
||||
if (gasConsumption.isInfinite)
|
||||
return numeric_limits<u256>::max();
|
||||
else
|
||||
return gasConsumption.value;
|
||||
}
|
||||
/// @returns an estimation of the code size in bytes needed for the AssemblyItems in @a _itemRange.
|
||||
template<typename RangeType>
|
||||
uint64_t codeSize(RangeType const& _itemRange)
|
||||
{
|
||||
return ranges::accumulate(_itemRange | ranges::views::transform(
|
||||
[](auto const& _item) { return _item.bytesRequired(2); }
|
||||
), 0u);
|
||||
}
|
||||
/// @returns the tag id, if @a _item is a PushTag or Tag into the current subassembly, nullopt otherwise.
|
||||
optional<size_t> getLocalTag(AssemblyItem const& _item)
|
||||
{
|
||||
if (_item.type() != PushTag && _item.type() != Tag)
|
||||
return nullopt;
|
||||
auto [subId, tag] = _item.splitForeignPushTag();
|
||||
if (subId != numeric_limits<size_t>::max())
|
||||
return nullopt;
|
||||
return tag;
|
||||
}
|
||||
}
|
||||
|
||||
bool Inliner::isInlineCandidate(size_t _tag, ranges::span<AssemblyItem const> _items) const
|
||||
{
|
||||
assertThrow(_items.size() > 0, OptimizerException, "");
|
||||
|
||||
// Only consider blocks that end in a JUMP for now. This can e.g. be extended to include transaction terminating
|
||||
// instructions as well in the future.
|
||||
if (_items.back() != Instruction::JUMP)
|
||||
return false;
|
||||
|
||||
// Never inline tags that reference themselves.
|
||||
for (AssemblyItem const& item: _items)
|
||||
if (item.type() == PushTag)
|
||||
if (getLocalTag(item) == _tag)
|
||||
return false;
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
map<size_t, Inliner::InlinableBlock> Inliner::determineInlinableBlocks(AssemblyItems const& _items) const
|
||||
{
|
||||
std::map<size_t, ranges::span<AssemblyItem const>> inlinableBlockItems;
|
||||
std::map<size_t, uint64_t> numPushTags;
|
||||
std::optional<size_t> lastTag;
|
||||
for (auto&& [index, item]: _items | ranges::views::enumerate)
|
||||
{
|
||||
// The number of PushTags approximates the number of calls to a block.
|
||||
if (item.type() == PushTag)
|
||||
if (optional<size_t> tag = getLocalTag(item))
|
||||
++numPushTags[*tag];
|
||||
|
||||
// We can only inline blocks with straight control flow that end in a jump.
|
||||
// Using breaksCSEAnalysisBlock will hopefully allow the return jump to be optimized after inlining.
|
||||
if (lastTag && SemanticInformation::breaksCSEAnalysisBlock(item, false))
|
||||
{
|
||||
ranges::span<AssemblyItem const> block = _items | ranges::views::slice(*lastTag + 1, index + 1);
|
||||
if (optional<size_t> tag = getLocalTag(_items[*lastTag]))
|
||||
if (isInlineCandidate(*tag, block))
|
||||
inlinableBlockItems[*tag] = block;
|
||||
lastTag.reset();
|
||||
}
|
||||
|
||||
if (item.type() == Tag)
|
||||
{
|
||||
assertThrow(getLocalTag(item), OptimizerException, "");
|
||||
lastTag = index;
|
||||
}
|
||||
}
|
||||
|
||||
// Store the number of PushTags alongside the assembly items and discard tags that are never pushed.
|
||||
map<size_t, InlinableBlock> result;
|
||||
for (auto&& [tag, items]: inlinableBlockItems)
|
||||
if (uint64_t const* numPushes = util::valueOrNullptr(numPushTags, tag))
|
||||
result.emplace(tag, InlinableBlock{items, *numPushes});
|
||||
return result;
|
||||
}
|
||||
|
||||
bool Inliner::shouldInlineFullFunctionBody(size_t _tag, ranges::span<AssemblyItem const> _block, uint64_t _pushTagCount) const
|
||||
{
|
||||
// Accumulate size of the inline candidate block in bytes (without the return jump).
|
||||
uint64_t functionBodySize = codeSize(ranges::views::drop_last(_block, 1));
|
||||
|
||||
// Use the number of push tags as approximation of the average number of calls to the function per run.
|
||||
uint64_t numberOfCalls = _pushTagCount;
|
||||
// Also use the number of push tags as approximation of the number of call sites to the function.
|
||||
uint64_t numberOfCallSites = _pushTagCount;
|
||||
|
||||
static AssemblyItems const uninlinedCallSitePattern = {
|
||||
AssemblyItem{PushTag},
|
||||
AssemblyItem{PushTag},
|
||||
AssemblyItem{Instruction::JUMP},
|
||||
AssemblyItem{Tag}
|
||||
};
|
||||
static AssemblyItems const uninlinedFunctionPattern = {
|
||||
AssemblyItem{Tag},
|
||||
// Actual function body of size functionBodySize. Handled separately below.
|
||||
AssemblyItem{Instruction::JUMP}
|
||||
};
|
||||
|
||||
// Both the call site and jump site pattern is executed for each call.
|
||||
// Since the function body has to be executed equally often both with and without inlining,
|
||||
// it can be ignored.
|
||||
bigint uninlinedExecutionCost = numberOfCalls * (
|
||||
executionCost(uninlinedCallSitePattern, m_evmVersion) +
|
||||
executionCost(uninlinedFunctionPattern, m_evmVersion)
|
||||
);
|
||||
// Each call site deposits the call site pattern, whereas the jump site pattern and the function itself are deposited once.
|
||||
bigint uninlinedDepositCost = GasMeter::dataGas(
|
||||
numberOfCallSites * codeSize(uninlinedCallSitePattern) +
|
||||
codeSize(uninlinedFunctionPattern) +
|
||||
functionBodySize,
|
||||
m_isCreation,
|
||||
m_evmVersion
|
||||
);
|
||||
// When inlining the execution cost beyond the actual function execution is zero,
|
||||
// but for each call site a copy of the function is deposited.
|
||||
bigint inlinedDepositCost = GasMeter::dataGas(
|
||||
numberOfCallSites * functionBodySize,
|
||||
m_isCreation,
|
||||
m_evmVersion
|
||||
);
|
||||
// If the block is referenced from outside the current subassembly, the original function cannot be removed.
|
||||
// Note that the function also cannot always be removed, if it is not referenced from outside, but in that case
|
||||
// the heuristics is optimistic.
|
||||
if (m_tagsReferencedFromOutside.count(_tag))
|
||||
inlinedDepositCost += GasMeter::dataGas(
|
||||
uninlinedFunctionPattern.size() + functionBodySize,
|
||||
m_isCreation,
|
||||
m_evmVersion
|
||||
);
|
||||
|
||||
// If the estimated runtime cost over the lifetime of the contract plus the deposit cost in the uninlined case
|
||||
// exceed the inlined deposit costs, it is beneficial to inline.
|
||||
if (bigint(m_runs) * uninlinedExecutionCost + uninlinedDepositCost > inlinedDepositCost)
|
||||
return true;
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
|
||||
optional<AssemblyItem::JumpType> Inliner::shouldInline(size_t _tag, AssemblyItem const& _jump, InlinableBlock const& _block) const
|
||||
{
|
||||
AssemblyItem exitJump = _block.items.back();
|
||||
assertThrow(_jump == Instruction::JUMP && exitJump == Instruction::JUMP, OptimizerException, "");
|
||||
|
||||
if (
|
||||
_jump.getJumpType() == AssemblyItem::JumpType::IntoFunction &&
|
||||
exitJump.getJumpType() == AssemblyItem::JumpType::OutOfFunction
|
||||
)
|
||||
return
|
||||
shouldInlineFullFunctionBody(_tag, _block.items, _block.pushTagCount) ?
|
||||
make_optional(AssemblyItem::JumpType::Ordinary) : nullopt;
|
||||
|
||||
return nullopt;
|
||||
}
|
||||
|
||||
|
||||
void Inliner::optimise()
|
||||
{
|
||||
std::map<size_t, InlinableBlock> inlinableBlocks = determineInlinableBlocks(m_items);
|
||||
|
||||
if (inlinableBlocks.empty())
|
||||
return;
|
||||
|
||||
AssemblyItems newItems;
|
||||
for (auto it = m_items.begin(); it != m_items.end(); ++it)
|
||||
{
|
||||
AssemblyItem const& item = *it;
|
||||
if (next(it) != m_items.end())
|
||||
{
|
||||
AssemblyItem const& nextItem = *next(it);
|
||||
if (item.type() == PushTag && nextItem == Instruction::JUMP)
|
||||
{
|
||||
if (optional<size_t> tag = getLocalTag(item))
|
||||
if (auto* inlinableBlock = util::valueOrNullptr(inlinableBlocks, *tag))
|
||||
if (auto exitJumpType = shouldInline(*tag, nextItem, *inlinableBlock))
|
||||
{
|
||||
newItems += inlinableBlock->items;
|
||||
newItems.back().setJumpType(*exitJumpType);
|
||||
|
||||
// We are removing one push tag to the block we inline.
|
||||
--inlinableBlock->pushTagCount;
|
||||
// We might increase the number of push tags to other blocks.
|
||||
for (AssemblyItem const& inlinedItem: inlinableBlock->items)
|
||||
if (inlinedItem.type() == PushTag)
|
||||
if (optional<size_t> duplicatedTag = getLocalTag(inlinedItem))
|
||||
if (auto* block = util::valueOrNullptr(inlinableBlocks, *duplicatedTag))
|
||||
++block->pushTagCount;
|
||||
|
||||
// Skip the original jump to the inlined tag and continue.
|
||||
++it;
|
||||
continue;
|
||||
}
|
||||
}
|
||||
}
|
||||
newItems.emplace_back(item);
|
||||
}
|
||||
|
||||
m_items = move(newItems);
|
||||
}
|
||||
@@ -0,0 +1,83 @@
|
||||
/*
|
||||
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
|
||||
/**
|
||||
* @file Inliner.h
|
||||
* Inlines small code snippets by replacing JUMP with a copy of the code jumped to.
|
||||
*/
|
||||
#pragma once
|
||||
|
||||
#include <libsolutil/Common.h>
|
||||
#include <libevmasm/AssemblyItem.h>
|
||||
#include <liblangutil/EVMVersion.h>
|
||||
|
||||
#include <range/v3/view/span.hpp>
|
||||
#include <map>
|
||||
#include <set>
|
||||
#include <vector>
|
||||
|
||||
namespace solidity::evmasm
|
||||
{
|
||||
|
||||
class Inliner
|
||||
{
|
||||
public:
|
||||
explicit Inliner(
|
||||
AssemblyItems& _items,
|
||||
std::set<size_t> const& _tagsReferencedFromOutside,
|
||||
size_t _runs,
|
||||
bool _isCreation,
|
||||
langutil::EVMVersion _evmVersion
|
||||
):
|
||||
m_items(_items),
|
||||
m_tagsReferencedFromOutside(_tagsReferencedFromOutside),
|
||||
m_runs(_runs),
|
||||
m_isCreation(_isCreation),
|
||||
m_evmVersion(_evmVersion)
|
||||
{
|
||||
}
|
||||
virtual ~Inliner() = default;
|
||||
|
||||
void optimise();
|
||||
|
||||
private:
|
||||
struct InlinableBlock
|
||||
{
|
||||
ranges::span<AssemblyItem const> items;
|
||||
uint64_t pushTagCount = 0;
|
||||
};
|
||||
|
||||
/// @returns the exit jump type for the block to be inlined, if a particular jump to it should be inlined, otherwise nullopt.
|
||||
std::optional<AssemblyItem::JumpType> shouldInline(size_t _tag, AssemblyItem const& _jump, InlinableBlock const& _block) const;
|
||||
/// @returns true, if the full function at tag @a _tag with body @a _block that is referenced @a _pushTagCount times
|
||||
/// should be inlined, false otherwise. @a _block should start at the first instruction after the function entry tag
|
||||
/// up to and including the return jump.
|
||||
bool shouldInlineFullFunctionBody(size_t _tag, ranges::span<AssemblyItem const> _block, uint64_t _pushTagCount) const;
|
||||
/// @returns true, if the @a _items at @a _tag are a potential candidate for inlining.
|
||||
bool isInlineCandidate(size_t _tag, ranges::span<AssemblyItem const> _items) const;
|
||||
/// @returns a map from tags that can potentially be inlined to the inlinable item range behind that tag and the
|
||||
/// number of times the tag in question was referenced.
|
||||
std::map<size_t, InlinableBlock> determineInlinableBlocks(AssemblyItems const& _items) const;
|
||||
|
||||
AssemblyItems& m_items;
|
||||
std::set<size_t> const& m_tagsReferencedFromOutside;
|
||||
size_t const m_runs = 200;
|
||||
bool const m_isCreation = false;
|
||||
langutil::EVMVersion const m_evmVersion;
|
||||
};
|
||||
|
||||
}
|
||||
Reference in New Issue
Block a user