Skip to content

Commit 5cd9800

Browse files
committed
Fill in junk in stack layouts on terminating control flow paths.
1 parent b6cd3e1 commit 5cd9800

File tree

139 files changed

+433
-276
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

139 files changed

+433
-276
lines changed

Changelog.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ Compiler Features:
1616
* JSON-AST: Added selector field for errors and events.
1717
* LSP: Implements goto-definition.
1818
* Peephole Optimizer: Optimize comparisons in front of conditional jumps and conditional jumps across a single unconditional jump.
19+
* Yul EVM Code Transform: Avoid unnecessary ``pop``s on terminating control flow.
1920
* Yul Optimizer: Remove ``sstore`` and ``mstore`` operations that are never read from.
2021

2122

libevmasm/PeepholeOptimiser.cpp

Lines changed: 30 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -117,6 +117,35 @@ struct OpPop: SimplePeepholeOptimizerMethod<OpPop>
117117
}
118118
};
119119

120+
struct OpStop: SimplePeepholeOptimizerMethod<OpStop>
121+
{
122+
static bool applySimple(
123+
AssemblyItem const& _op,
124+
AssemblyItem const& _stop,
125+
std::back_insert_iterator<AssemblyItems> _out
126+
)
127+
{
128+
if (_stop == Instruction::STOP)
129+
{
130+
if (_op.type() == Operation)
131+
{
132+
Instruction instr = _op.instruction();
133+
if (!instructionInfo(instr).sideEffects)
134+
{
135+
*_out = {Instruction::STOP, _op.location()};
136+
return true;
137+
}
138+
}
139+
else if (_op.type() == Push)
140+
{
141+
*_out = {Instruction::STOP, _op.location()};
142+
return true;
143+
}
144+
}
145+
return false;
146+
}
147+
};
148+
120149
struct DoubleSwap: SimplePeepholeOptimizerMethod<DoubleSwap>
121150
{
122151
static size_t applySimple(AssemblyItem const& _s1, AssemblyItem const& _s2, std::back_insert_iterator<AssemblyItems>)
@@ -430,7 +459,7 @@ bool PeepholeOptimiser::optimise()
430459
while (state.i < m_items.size())
431460
applyMethods(
432461
state,
433-
PushPop(), OpPop(), DoublePush(), DoubleSwap(), CommutativeSwap(), SwapComparison(),
462+
PushPop(), OpPop(), OpStop(), DoublePush(), DoubleSwap(), CommutativeSwap(), SwapComparison(),
434463
DupSwap(), IsZeroIsZeroJumpI(), EqIsZeroJumpI(), DoubleJump(), JumpToNext(), UnreachableCode(),
435464
TagConjunctions(), TruthyAnd(), Identity()
436465
);

libyul/backends/evm/ControlFlowGraph.h

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -195,6 +195,14 @@ struct CFG
195195
std::shared_ptr<DebugData const> debugData;
196196
std::vector<BasicBlock*> entries;
197197
std::vector<Operation> operations;
198+
/// True, if the block is the beginning of a disconnected subgraph. That is, if no block that is reachable
199+
/// from this block is an ancestor of this block. In other words, this is true, if this block is the target
200+
/// of a cut-edge/bridge in the CFG or if the block itself terminates.
201+
bool isStartOfSubGraph = false;
202+
/// True, if there is a path from this block to a function return.
203+
bool needsCleanStack = false;
204+
/// If the block starts a sub-graph and does not lead to a function return, we are free to add junk to it.
205+
bool allowsJunk() const { return isStartOfSubGraph && !needsCleanStack; }
198206
std::variant<MainExit, Jump, ConditionalJump, FunctionReturn, Terminated> exit = MainExit{};
199207
};
200208

@@ -205,6 +213,7 @@ struct CFG
205213
BasicBlock* entry = nullptr;
206214
std::vector<VariableSlot> parameters;
207215
std::vector<VariableSlot> returnVariables;
216+
std::vector<BasicBlock*> exits;
208217
};
209218

210219
/// The main entry point, i.e. the start of the outermost Yul block.

libyul/backends/evm/ControlFlowGraphBuilder.cpp

Lines changed: 87 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -48,7 +48,7 @@ using namespace std;
4848

4949
namespace
5050
{
51-
// Removes edges to blocks that are not reachable.
51+
/// Removes edges to blocks that are not reachable.
5252
void cleanUnreachable(CFG& _cfg)
5353
{
5454
// Determine which blocks are reachable from the entry.
@@ -77,7 +77,8 @@ void cleanUnreachable(CFG& _cfg)
7777
return !reachabilityCheck.visited.count(entry);
7878
});
7979
}
80-
// Sets the ``recursive`` member to ``true`` for all recursive function calls.
80+
81+
/// Sets the ``recursive`` member to ``true`` for all recursive function calls.
8182
void markRecursiveCalls(CFG& _cfg)
8283
{
8384
map<CFG::BasicBlock*, vector<CFG::FunctionCall*>> callsPerBlock;
@@ -124,6 +125,84 @@ void markRecursiveCalls(CFG& _cfg)
124125
});
125126
}
126127
}
128+
129+
/// Marks each cut-vertex in the CFG, i.e. each block that begins a disconnected sub-graph of the CFG.
130+
/// Entering such a block means that control flow will never return to a previously visited block.
131+
void markStartsOfSubGraphs(CFG& _cfg)
132+
{
133+
vector<CFG::BasicBlock*> entries;
134+
entries.emplace_back(_cfg.entry);
135+
for (auto&& functionInfo: _cfg.functionInfo | ranges::views::values)
136+
entries.emplace_back(functionInfo.entry);
137+
for (auto& entry: entries)
138+
{
139+
/**
140+
* Detect bridges following Algorithm 1 in https://arxiv.org/pdf/2108.07346.pdf
141+
* and mark the bridge targets as starts of sub-graphs.
142+
*/
143+
set<CFG::BasicBlock*> visited;
144+
map<CFG::BasicBlock*, size_t> disc;
145+
map<CFG::BasicBlock*, size_t> low;
146+
map<CFG::BasicBlock*, CFG::BasicBlock*> parent;
147+
size_t time = 0;
148+
auto dfs = [&](CFG::BasicBlock* _u, auto _recurse) -> void {
149+
visited.insert(_u);
150+
disc[_u] = low[_u] = time;
151+
time++;
152+
153+
vector<CFG::BasicBlock*> children = _u->entries;
154+
visit(util::GenericVisitor{
155+
[&](CFG::BasicBlock::Jump const& _jump) {
156+
children.emplace_back(_jump.target);
157+
},
158+
[&](CFG::BasicBlock::ConditionalJump const& _jump) {
159+
children.emplace_back(_jump.zero);
160+
children.emplace_back(_jump.nonZero);
161+
},
162+
[&](CFG::BasicBlock::FunctionReturn const&) {},
163+
[&](CFG::BasicBlock::Terminated const&) { _u->isStartOfSubGraph = true; },
164+
[&](CFG::BasicBlock::MainExit const&) { _u->isStartOfSubGraph = true; }
165+
}, _u->exit);
166+
yulAssert(!util::contains(children, _u));
167+
168+
for (CFG::BasicBlock* v: children)
169+
if (!visited.count(v))
170+
{
171+
parent[v] = _u;
172+
_recurse(v, _recurse);
173+
low[_u] = min(low[_u], low[v]);
174+
if (low[v] > disc[_u])
175+
{
176+
// _u <-> v is a cut edge in the undirected graph
177+
bool edgeVtoU = util::contains(_u->entries, v);
178+
bool edgeUtoV = util::contains(v->entries, _u);
179+
if (edgeVtoU && !edgeUtoV)
180+
// Cut edge v -> _u
181+
_u->isStartOfSubGraph = true;
182+
else if (edgeUtoV && !edgeVtoU)
183+
// Cut edge _u -> v
184+
v->isStartOfSubGraph = true;
185+
}
186+
}
187+
else if (v != parent[_u])
188+
low[_u] = min(low[_u], disc[v]);
189+
};
190+
dfs(entry, dfs);
191+
}
192+
}
193+
194+
/// Marks each block that needs to maintain a clean stack. That is each block that has an outgoing
195+
/// path to a function return.
196+
void markNeedsCleanStack(CFG& _cfg)
197+
{
198+
for (auto& functionInfo: _cfg.functionInfo | ranges::views::values)
199+
for (CFG::BasicBlock* exit: functionInfo.exits)
200+
util::BreadthFirstSearch<CFG::BasicBlock*>{{exit}}.run([&](CFG::BasicBlock* _block, auto _addChild) {
201+
_block->needsCleanStack = true;
202+
for (CFG::BasicBlock* entry: _block->entries)
203+
_addChild(entry);
204+
});
205+
}
127206
}
128207

129208
std::unique_ptr<CFG> ControlFlowGraphBuilder::build(
@@ -141,6 +220,8 @@ std::unique_ptr<CFG> ControlFlowGraphBuilder::build(
141220

142221
cleanUnreachable(*result);
143222
markRecursiveCalls(*result);
223+
markStartsOfSubGraphs(*result);
224+
markNeedsCleanStack(*result);
144225

145226
// TODO: It might be worthwhile to run some further simplifications on the graph itself here.
146227
// E.g. if there is a jump to a node that has the jumping node as its only entry, the nodes can be fused, etc.
@@ -379,6 +460,7 @@ void ControlFlowGraphBuilder::operator()(Leave const& leave_)
379460
{
380461
yulAssert(m_currentFunction.has_value(), "");
381462
m_currentBlock->exit = CFG::BasicBlock::FunctionReturn{debugDataOf(leave_), *m_currentFunction};
463+
(*m_currentFunction)->exits.emplace_back(m_currentBlock);
382464
m_currentBlock = &m_graph.makeBlock(debugDataOf(*m_currentBlock));
383465
}
384466

@@ -395,6 +477,7 @@ void ControlFlowGraphBuilder::operator()(FunctionDefinition const& _function)
395477
builder.m_currentFunction = &functionInfo;
396478
builder.m_currentBlock = functionInfo.entry;
397479
builder(_function.body);
480+
functionInfo.exits.emplace_back(builder.m_currentBlock);
398481
builder.m_currentBlock->exit = CFG::BasicBlock::FunctionReturn{debugDataOf(_function), &functionInfo};
399482
}
400483

@@ -423,7 +506,8 @@ void ControlFlowGraphBuilder::registerFunction(FunctionDefinition const& _functi
423506
std::get<Scope::Variable>(virtualFunctionScope->identifiers.at(_retVar.name)),
424507
_retVar.debugData
425508
};
426-
}) | ranges::to<vector>
509+
}) | ranges::to<vector>,
510+
{}
427511
})).second;
428512
yulAssert(inserted);
429513
}

libyul/backends/evm/StackLayoutGenerator.cpp

Lines changed: 110 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,8 @@
2323

2424
#include <libyul/backends/evm/StackHelpers.h>
2525

26+
#include <libevmasm/GasMeter.h>
27+
2628
#include <libsolutil/Algorithms.h>
2729
#include <libsolutil/cxx20.h>
2830
#include <libsolutil/Visitor.h>
@@ -400,6 +402,7 @@ void StackLayoutGenerator::processEntryPoint(CFG::BasicBlock const& _entry)
400402
}
401403

402404
stitchConditionalJumps(_entry);
405+
fillInJunk(_entry);
403406
}
404407

405408
optional<Stack> StackLayoutGenerator::getExitLayoutOrStageDependencies(
@@ -703,3 +706,110 @@ Stack StackLayoutGenerator::compressStack(Stack _stack)
703706
while (firstDupOffset);
704707
return _stack;
705708
}
709+
710+
void StackLayoutGenerator::fillInJunk(CFG::BasicBlock const& _block)
711+
{
712+
/// Recursively adds junk to the subgraph starting on @a _entry.
713+
/// Since it is only called on cut-vertices, the full subgraph retains proper stack balance.
714+
auto addJunkRecursive = [&](CFG::BasicBlock const* _entry, size_t _numJunk) {
715+
util::BreadthFirstSearch<CFG::BasicBlock const*> breadthFirstSearch{{_entry}};
716+
breadthFirstSearch.run([&](CFG::BasicBlock const* _block, auto _addChild) {
717+
auto& blockInfo = m_layout.blockInfos.at(_block);
718+
blockInfo.entryLayout = Stack{_numJunk, JunkSlot{}} + move(blockInfo.entryLayout);
719+
for (auto const& operation: _block->operations)
720+
{
721+
auto& operationEntryLayout = m_layout.operationEntryLayout.at(&operation);
722+
operationEntryLayout = Stack{_numJunk, JunkSlot{}} + move(operationEntryLayout);
723+
}
724+
blockInfo.exitLayout = Stack{_numJunk, JunkSlot{}} + move(blockInfo.exitLayout);
725+
726+
std::visit(util::GenericVisitor{
727+
[&](CFG::BasicBlock::MainExit const&) {},
728+
[&](CFG::BasicBlock::Jump const& _jump)
729+
{
730+
_addChild(_jump.target);
731+
},
732+
[&](CFG::BasicBlock::ConditionalJump const& _conditionalJump)
733+
{
734+
_addChild(_conditionalJump.zero);
735+
_addChild(_conditionalJump.nonZero);
736+
},
737+
[&](CFG::BasicBlock::FunctionReturn const&) { yulAssert(false); },
738+
[&](CFG::BasicBlock::Terminated const&) {},
739+
}, _block->exit);
740+
});
741+
};
742+
/// @returns the number of operations required to transform @a _source to @a _target.
743+
auto evaluateTransform = [](Stack _source, Stack const& _target) -> size_t {
744+
size_t opGas = 0;
745+
auto swap = [&](unsigned _swapDepth)
746+
{
747+
if (_swapDepth > 16)
748+
opGas += 1000;
749+
else
750+
opGas += evmasm::GasMeter::runGas(evmasm::swapInstruction(_swapDepth));
751+
};
752+
auto dupOrPush = [&](StackSlot const& _slot)
753+
{
754+
if (canBeFreelyGenerated(_slot))
755+
opGas += evmasm::GasMeter::runGas(evmasm::pushInstruction(32));
756+
else
757+
{
758+
auto depth = util::findOffset(_source | ranges::views::reverse, _slot);
759+
yulAssert(depth);
760+
if (*depth < 16)
761+
opGas += evmasm::GasMeter::runGas(evmasm::dupInstruction(static_cast<unsigned>(*depth + 1)));
762+
else
763+
opGas += 1000;
764+
}
765+
};
766+
auto pop = [&]() { opGas += evmasm::GasMeter::runGas(evmasm::Instruction::POP); };
767+
createStackLayout(_source, _target, swap, dupOrPush, pop);
768+
return opGas;
769+
};
770+
/// Traverses the CFG and at each block that allows junk, i.e. that is a cut-vertex that never leads to a function
771+
/// return, checks if adding junk reduces the shuffling cost upon entering and if so recursively adds junk
772+
/// to the spanned subgraph.
773+
util::BreadthFirstSearch<CFG::BasicBlock const*>{{&_block}}.run([&](CFG::BasicBlock const* _block, auto _addChild) {
774+
std::visit(util::GenericVisitor{
775+
[&](CFG::BasicBlock::MainExit const&) {},
776+
[&](CFG::BasicBlock::Jump const& _jump)
777+
{
778+
_addChild(_jump.target);
779+
},
780+
[&](CFG::BasicBlock::ConditionalJump const& _conditionalJump)
781+
{
782+
for (CFG::BasicBlock* exit: {_conditionalJump.zero, _conditionalJump.nonZero})
783+
if (exit->allowsJunk())
784+
{
785+
auto& blockInfo = m_layout.blockInfos.at(exit);
786+
Stack entryLayout = blockInfo.entryLayout;
787+
Stack nextLayout = exit->operations.empty() ? blockInfo.exitLayout : m_layout.operationEntryLayout.at(&exit->operations.front());
788+
789+
size_t bestCost = evaluateTransform(entryLayout, nextLayout);
790+
size_t bestNumJunk = 0;
791+
size_t maxJunk = entryLayout.size();
792+
for (size_t numJunk = 1; numJunk <= maxJunk; ++numJunk)
793+
{
794+
size_t cost = evaluateTransform(entryLayout, Stack{numJunk, JunkSlot{}} + nextLayout);
795+
if (cost < bestCost)
796+
{
797+
bestCost = cost;
798+
bestNumJunk = numJunk;
799+
}
800+
}
801+
802+
if (bestNumJunk > 0)
803+
{
804+
addJunkRecursive(exit, bestNumJunk);
805+
blockInfo.entryLayout = entryLayout;
806+
}
807+
}
808+
_addChild(_conditionalJump.zero);
809+
_addChild(_conditionalJump.nonZero);
810+
},
811+
[&](CFG::BasicBlock::FunctionReturn const&) {},
812+
[&](CFG::BasicBlock::Terminated const&) {},
813+
}, _block->exit);
814+
});
815+
}

libyul/backends/evm/StackLayoutGenerator.h

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -111,6 +111,9 @@ class StackLayoutGenerator
111111
/// stack @a _stack.
112112
static Stack compressStack(Stack _stack);
113113

114+
//// Fills in junk when entering branches that do not need a clean stack in case the result is cheaper.
115+
void fillInJunk(CFG::BasicBlock const& _block);
116+
114117
StackLayout& m_layout;
115118
};
116119

test/cmdlineTests/dup_opt_peephole/output

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -46,8 +46,6 @@ sub_0: assembly {
4646
/* "dup_opt_peephole/input.sol":150:162 sstore(0, x) */
4747
sstore
4848
/* "dup_opt_peephole/input.sol":107:166 {... */
49-
pop
50-
/* "dup_opt_peephole/input.sol":60:171 contract C {... */
5149
stop
5250

5351
auxdata: <AUXDATA REMOVED>

test/cmdlineTests/function_debug_info_via_yul/output

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@
1313
},
1414
"calldata_array_index_access_uint256_dyn_calldata":
1515
{
16-
"entryPoint": 152,
16+
"entryPoint": 145,
1717
"parameterSlots": 2,
1818
"returnSlots": 1
1919
}

0 commit comments

Comments
 (0)