Skip to content
Merged
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
1 change: 1 addition & 0 deletions libyul/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -99,6 +99,7 @@ add_library(yul
backends/evm/ssa/StackLayoutGenerator.h
backends/evm/ssa/StackShuffler.cpp
backends/evm/ssa/StackShuffler.h
backends/evm/ssa/StackToMemorySpilling.h
backends/evm/ssa/StackUtils.cpp
backends/evm/ssa/StackUtils.h
backends/evm/ssa/io/DotExporterBase.cpp
Expand Down
23 changes: 17 additions & 6 deletions libyul/backends/evm/ssa/StackShuffler.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -24,23 +24,31 @@
using namespace solidity::yul::ssa;
using namespace solidity::yul::ssa::detail;

Target::Target(StackData const& _args, LivenessAnalysis::LivenessData const& _liveOut, std::size_t const _targetSize):
Target::Target(
StackData const& _args,
LivenessAnalysis::LivenessData const& _liveOut,
std::size_t const _targetSize,
SpilledVariables const* _spilledVariables
):
args(_args),
liveOut(_liveOut),
spilledVariables(_spilledVariables),
size(_targetSize),
tailSize(_targetSize - _args.size())
{
minCount.reserve(_args.size() + _liveOut.size());
for (auto const& arg: _args)
if (!arg.isJunk())
if (!arg.isJunk() && !slotIsSpilled(arg, _spilledVariables))
++minCount[arg];
for (auto const& _liveValueId: _liveOut | ranges::views::keys)
++minCount[StackSlot::makeValueID(_liveValueId)];
for (auto const& liveValueId: _liveOut | ranges::views::keys)
if (!(_spilledVariables && _spilledVariables->isSpilled(liveValueId)))
++minCount[StackSlot::makeValueID(liveValueId)];
}

State::State(StackData const& _stackData, Target const& _target, std::size_t const _reachableStackDepth):
State::State(StackData const& _stackData, Target const& _target, SpilledVariables const* const _spilledVariables, std::size_t const _reachableStackDepth):
m_stackData(_stackData),
m_target(_target),
m_spilledVariables(_spilledVariables),
m_reachableStackDepth(_reachableStackDepth)
{
m_histogram.reserve(_stackData.size());
Expand Down Expand Up @@ -125,7 +133,10 @@ bool State::requiredInArgs(StackSlot const& _slot) const

bool State::requiredInTail(StackSlot const& _slot) const
{
return _slot.isValueID() && m_target.liveOut.contains(_slot.valueID());
if (!_slot.isValueID() || !m_target.liveOut.contains(_slot.valueID()))
return false;
// Spilled values can be rematerialized, so they need not occupy a tail slot.
return !slotIsSpilled(_slot);
}

bool State::offsetInTargetArgsRegion(StackOffset const _offset) const
Expand Down
97 changes: 74 additions & 23 deletions libyul/backends/evm/ssa/StackShuffler.h
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@
#pragma once

#include <libyul/backends/evm/ssa/LivenessAnalysis.h>
#include <libyul/backends/evm/ssa/StackToMemorySpilling.h>
#include <libyul/backends/evm/ssa/Stack.h>

#include <boost/container/flat_map.hpp>
Expand All @@ -36,14 +37,31 @@ namespace solidity::yul::ssa

namespace detail
{

inline bool slotIsSpilled(StackSlot const& _slot, SpilledVariables const* const _spilledVariables)
{
return _spilledVariables && _slot.isValueID() && _spilledVariables->isSpilled(_slot.valueID());
}

inline bool slotCanBeLoadedOrPushed(StackSlot const& _slot, SpilledVariables const* const _spilledVariables)
{
return Stack<>::canBeFreelyGenerated(_slot) || slotIsSpilled(_slot, _spilledVariables);
}

/// Contains information about the shuffling target, aggregates over args and live out to
/// provide a lower bound for the slot distribution.
struct Target
{
Target(StackData const& _args, LivenessAnalysis::LivenessData const& _liveOut, std::size_t _targetSize);
Target(
StackData const& _args,
LivenessAnalysis::LivenessData const& _liveOut,
std::size_t _targetSize,
SpilledVariables const* _spilledVariables = nullptr
);

StackData const& args;
LivenessAnalysis::LivenessData const& liveOut;
SpilledVariables const* const spilledVariables;
Comment thread
clonker marked this conversation as resolved.
std::size_t const size;
std::size_t const tailSize;
boost::container::flat_map<StackSlot, size_t> minCount;
Expand All @@ -52,7 +70,7 @@ struct Target
class State
{
public:
State(StackData const& _stackData, Target const& _target, std::size_t _reachableStackDepth);
State(StackData const& _stackData, Target const& _target, SpilledVariables const* const _spilledVariables, std::size_t _reachableStackDepth);

std::size_t size() const;
/// How many of `_slot` do we have on stack
Expand Down Expand Up @@ -128,9 +146,20 @@ class State
/// Depth of the deepest arg slot incompatible with target or Nothing for no incompatibility in current state
std::optional<StackDepth> findDeepestIncorrectArgSlot() const;

bool slotCanBeLoadedOrPushed(StackSlot const& _slot) const
{
return detail::slotCanBeLoadedOrPushed(_slot, m_spilledVariables);
}

bool slotIsSpilled(StackSlot const& _slot) const
{
return detail::slotIsSpilled(_slot, m_spilledVariables);
}

private:
StackData const& m_stackData;
Target const& m_target;
SpilledVariables const* const m_spilledVariables;
std::size_t const m_reachableStackDepth;
boost::container::flat_map<StackSlot, size_t> m_histogramTail;
boost::container::flat_map<StackSlot, size_t> m_histogramArgs;
Expand All @@ -156,25 +185,32 @@ class StackShuffler
Stack<Callback>& _stack,
StackData const& _args,
LivenessAnalysis::LivenessData const& _liveOut,
std::size_t _targetStackSize
std::size_t _targetStackSize,
SpilledVariables const* const _spilledVariables = nullptr
)
{
detail::Target const target(_args, _liveOut, _targetStackSize);
yulAssert(_liveOut.size() <= target.size, "not enough tail space");
detail::Target const target(_args, _liveOut, _targetStackSize, _spilledVariables);
// If the caller has wired up a spill set, the shuffler can reduce the effective liveOut
// size by spilling; otherwise the liveOut must fit into the target up front.
if (!_spilledVariables)
yulAssert(_liveOut.size() <= target.size, "not enough tail space");
{
// check that all required values are on stack
detail::State const state(_stack.data(), target, ReachableStackDepth);
detail::State const state(_stack.data(), target, _spilledVariables, ReachableStackDepth);
for (auto const& liveVariable: _liveOut | ranges::views::keys | ranges::views::transform(Slot::makeValueID))
yulAssert(!_stack.canBeFreelyGenerated(liveVariable) && ranges::contains(_stack.data(), liveVariable));
yulAssert(
!_stack.canBeFreelyGenerated(liveVariable) &&
(ranges::contains(_stack.data(), liveVariable) || detail::slotIsSpilled(liveVariable, _spilledVariables))
);
for (auto const& arg: _args)
yulAssert(_stack.canBeFreelyGenerated(arg) || ranges::contains(_stack.data(), arg));
yulAssert(detail::slotCanBeLoadedOrPushed(arg, _spilledVariables) || ranges::contains(_stack.data(), arg));
}

static std::size_t constexpr maxIterations = 1000;
std::size_t i = 0;
while (true)
{
detail::State const state(_stack.data(), target, ReachableStackDepth);
detail::State const state(_stack.data(), target, _spilledVariables, ReachableStackDepth);
auto result = shuffleStep(_stack, state);
if (result.status == StackShufflerResult::Status::Admissible)
{
Expand Down Expand Up @@ -228,13 +264,13 @@ class StackShuffler
return {StackShufflerResult::Status::Continue};

// after this, all current slots are either in acceptable positions or at least dup-reachable
if (auto unreachableOffset = allNecessarySlotsReachableOrFinal(_stack, _state))
if (auto culprit = allNecessarySlotsReachableOrFinal(_stack, _state))
{
// !allNecessarySlotsReachableOrFinal(ops) ≡ ¬(∀s: reachable(s) ∨ final(s)) ≡ ∃s: ¬reachable(s) ∧ ¬final(s)
if (shrinkStack(_stack, _state))
return {StackShufflerResult::Status::Continue};

return {StackShufflerResult::Status::StackTooDeep, _stack[*unreachableOffset]};
return {StackShufflerResult::Status::StackTooDeep, _stack.top()};
}

// this will either grow the tail as needed, swap down something from args that needs to be in the tail,
Expand Down Expand Up @@ -273,6 +309,18 @@ class StackShuffler
if (shrinkStack(_stack, _state))
return {StackShufflerResult::Status::Continue};

// if we couldn't shrink the stack we surface this failed state as stack too deep
for (StackOffset const offset: _state.stackRange() | ranges::views::reverse)
{
Slot const& candidate = _stack[offset];
if (
candidate.isValueID() &&
!candidate.isLiteralValueID() &&
!_state.slotIsSpilled(candidate)
)
return {StackShufflerResult::Status::StackTooDeep, candidate};
}

yulAssert(false, "reached final and forbidden state");
}

Expand Down Expand Up @@ -580,10 +628,10 @@ class StackShuffler
_stack.dup(*sourceDepth);
return {ShuffleHelperResult::Status::StackModified};
}
if (!_stack.canBeFreelyGenerated(arg))
if (!_state.slotCanBeLoadedOrPushed(arg))
return {ShuffleHelperResult::Status::StackTooDeep, arg};
}
yulAssert(_stack.canBeFreelyGenerated(arg));
yulAssert(_state.slotCanBeLoadedOrPushed(arg));
_stack.push(arg);
return {ShuffleHelperResult::Status::StackModified};
}
Expand Down Expand Up @@ -621,7 +669,7 @@ class StackShuffler
}
else
{
if (!_stack.canBeFreelyGenerated(arg))
if (!_state.slotCanBeLoadedOrPushed(arg))
return {ShuffleHelperResult::Status::StackTooDeep, arg};
auto result = dupDeepSlotIfRequired(_stack, _state);
if (result.status == ShuffleHelperResult::Status::StackTooDeep)
Expand Down Expand Up @@ -671,7 +719,7 @@ class StackShuffler
for (StackOffset tailOffset: _state.stackTailRange())
if (
_stack.isValidSwapTarget(tailOffset) &&
_stack.canBeFreelyGenerated(_stack[tailOffset]) &&
_state.slotCanBeLoadedOrPushed(_stack[tailOffset]) &&
!_stack[tailOffset].isLiteralValueID()
)
{
Expand Down Expand Up @@ -830,8 +878,11 @@ class StackShuffler
return false;
}

/// Checks if all current slots are either in a position that is compatible with the target or, if not, are dup-reachable.
static std::optional<StackOffset> allNecessarySlotsReachableOrFinal(Stack<Callback> const& _stack, detail::State const& _state)
/// Checks if all current slots are either in a position that is compatible with the target or, if not, are
/// dup-reachable.
/// Returns the culprit slot (guaranteed to be non-junk) that cannot be placed or duplicated, or `std::nullopt`
/// if every slot is reachable-or-final.
static std::optional<StackSlot> allNecessarySlotsReachableOrFinal(Stack<Callback> const& _stack, detail::State const& _state)
{
// check that args are either in position or reachable
for (StackOffset offset{_state.target().tailSize}; offset < _state.target().size; ++offset.value)
Expand All @@ -846,19 +897,19 @@ class StackShuffler

// the target offset itself is out of swap range, we must shrink to reach it
if (offset.value < _stack.size() && _stack.isBeyondSwapRange(offset))
return offset;
return targetArg;

// find first occurrence of the slot
std::optional<StackDepth> const depth = _stack.findSlotDepth(targetArg);
if (!depth)
{
// if there is no occurrence of the slot anywhere, we must be able to freely generate it
yulAssert(_stack.canBeFreelyGenerated(targetArg));
yulAssert(_state.slotCanBeLoadedOrPushed(targetArg));
}
else
{
if (_stack.isBeyondSwapRange(*depth))
return _stack.depthToOffset(*depth);
if (_stack.isBeyondSwapRange(*depth) && !_state.slotCanBeLoadedOrPushed(targetArg))
return targetArg;
}
}
// distribution check: all we have to dup can be duped
Expand All @@ -875,8 +926,8 @@ class StackShuffler
std::optional<StackDepth> depth = _stack.findSlotDepth(slotAtOffset);
// it must exist
yulAssert(depth);
if (!_stack.dupReachable(*depth))
return _stack.depthToOffset(*depth);
if (!_stack.dupReachable(*depth) && !_state.slotCanBeLoadedOrPushed(slotAtOffset))
return slotAtOffset;
}
}

Expand Down
59 changes: 59 additions & 0 deletions libyul/backends/evm/ssa/StackToMemorySpilling.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
/*
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/backends/evm/ssa/SSACFG.h>

#include <range/v3/view/map.hpp>

#include <cstdint>
#include <map>

namespace solidity::yul::ssa
{
class SpilledVariables
{
public:

void spill(SSACFG::ValueId const _valueId)
{
bool const emplaced = m_spillSet.try_emplace(_valueId, m_currentSpillSlot).second;
yulAssert(emplaced, fmt::format("can't spill a value ({}) twice", _valueId));
++m_currentSpillSlot;
}

bool isSpilled(SSACFG::ValueId const _valueId) const
{
return m_spillSet.contains(_valueId);
}

std::size_t numSpilled() const
{
return m_spillSet.size();
}

auto spilledValues() const
{
return m_spillSet | ranges::views::keys;
}
private:
std::uint32_t m_currentSpillSlot{0};
std::map<SSACFG::ValueId, std::uint32_t> m_spillSet; // map valueId -> ssa-cfg-local slot
};
}
37 changes: 31 additions & 6 deletions libyul/backends/evm/ssa/StackUtils.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -104,12 +104,37 @@ std::size_t solidity::yul::ssa::findOptimalTargetSize
data.reserve(startSize + maxUpwardExpansion);
auto const evaluateCost = [&](std::size_t const _targetSize) -> std::size_t
{
data = _stackData;
Stack<OpsCountingCallbacks> countOpsStack(data, {});
auto const shuffleResult = StackShuffler<OpsCountingCallbacks>::shuffle(countOpsStack, _targetArgs, _targetLiveOut, _targetSize);
yulAssert(shuffleResult.status == StackShufflerResult::Status::Admissible);
yulAssert(countOpsStack.size() == _targetSize);
return countOpsStack.callbacks().numOps;
StackShufflerResult result;
SpilledVariables spillSet;
OpsCountingCallbacks callbacks;
do
{
data = _stackData;
Stack<OpsCountingCallbacks> countOpsStack(data, {});
result = StackShuffler<OpsCountingCallbacks>::shuffle(countOpsStack, _targetArgs, _targetLiveOut, _targetSize, &spillSet);
callbacks = countOpsStack.callbacks();
switch (result.status)
{
case StackShufflerResult::Status::Continue:
yulAssert(false);
case StackShufflerResult::Status::Admissible:
break;
case StackShufflerResult::Status::StackTooDeep:
{
yulAssert(result.culprit.isValueID() && !result.culprit.isLiteralValueID());
yulAssert(!spillSet.isSpilled(result.culprit.valueID()));
spillSet.spill(result.culprit.valueID());
break;
}
case StackShufflerResult::Status::MaxIterationsReached:
break;
}
}
while (result.status == StackShufflerResult::Status::StackTooDeep);
yulAssert(data.size() == _targetSize);
yulAssert(result.status == StackShufflerResult::Status::Admissible);
std::size_t const cost = callbacks.numOps + 1000 * spillSet.numSpilled();
return cost;
};

std::size_t bestCost = evaluateCost(startSize);
Expand Down
Loading