Skip to content

Commit 1c4e9a5

Browse files
authored
Merge pull request #16767 from argotorg/ssa-cfg-kiss-memspill
SSA CFG: stack to memory spilling
2 parents 1b095c7 + c48728e commit 1c4e9a5

23 files changed

Lines changed: 1555 additions & 54 deletions

libyul/CMakeLists.txt

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -115,6 +115,10 @@ add_library(yul
115115
backends/evm/ssa/io/JSONExporter.h
116116
backends/evm/ssa/io/Printer.cpp
117117
backends/evm/ssa/io/Printer.h
118+
backends/evm/ssa/spill/Emitter.h
119+
backends/evm/ssa/spill/MemoryAddressing.cpp
120+
backends/evm/ssa/spill/MemoryAddressing.h
121+
backends/evm/ssa/spill/SpillSet.cpp
118122
backends/evm/ssa/spill/SpillSet.h
119123
backends/evm/ssa/transform/IdentityAndNopRemover.cpp
120124
backends/evm/ssa/transform/IdentityAndNopRemover.h

libyul/backends/evm/EVMObjectCompiler.cpp

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -96,6 +96,7 @@ void EVMObjectCompiler::run(Object const& _object, bool _optimize, bool _viaSSAC
9696
ssa::ControlFlowGraphsLiveness const liveness(*controlFlowGraphs);
9797
ssa::CodeTransform::run(
9898
m_assembly,
99+
*controlFlowGraphs,
99100
liveness,
100101
context
101102
);

libyul/backends/evm/ssa/CodeTransform.cpp

Lines changed: 109 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -45,36 +45,66 @@ void assertLayoutCompatibility(StackData const& _layout1, StackData const& _layo
4545
void CodeTransform::run
4646
(
4747
AbstractAssembly& _assembly,
48+
ControlFlowGraphs& _controlFlowGraphs,
4849
ControlFlowGraphsLiveness const& _controlFlowLiveness,
4950
BuiltinContext& _builtinContext
5051
)
5152
{
53+
yulAssert(&_controlFlowLiveness.controlFlowGraphs.get() == &_controlFlowGraphs);
5254
yulAssert(!_controlFlowLiveness.cfgLiveness.empty());
53-
ControlFlowGraphs const& controlFlowGraphs = _controlFlowLiveness.controlFlowGraphs.get();
54-
yulAssert(controlFlowGraphs.functionGraphs.size() == _controlFlowLiveness.cfgLiveness.size());
55-
FunctionLabels const functionLabels = registerFunctionLabels(_assembly, controlFlowGraphs);
56-
CallGraph const callGraph(controlFlowGraphs);
57-
58-
for (std::size_t functionIndex = 0; functionIndex < controlFlowGraphs.functionGraphs.size(); ++functionIndex)
55+
yulAssert(_controlFlowGraphs.functionGraphs.size() == _controlFlowLiveness.cfgLiveness.size());
56+
FunctionLabels const functionLabels = registerFunctionLabels(_assembly, _controlFlowGraphs);
57+
CallGraph const callGraph(_controlFlowGraphs);
58+
59+
std::size_t const numCFGs = _controlFlowGraphs.functionGraphs.size();
60+
std::vector<CallSites> callSitesPerCFG;
61+
std::vector<SSACFGStackLayout> layouts;
62+
std::vector<spill::SpillSet> spillSetsPerCFG;
63+
callSitesPerCFG.reserve(numCFGs);
64+
layouts.reserve(numCFGs);
65+
spillSetsPerCFG.reserve(numCFGs);
66+
67+
for (std::size_t functionIndex = 0; functionIndex < numCFGs; ++functionIndex)
5968
{
60-
std::unique_ptr<SSACFG> const& functionGraphPtr = controlFlowGraphs.functionGraphs[functionIndex];
69+
std::unique_ptr<SSACFG> const& functionGraphPtr = _controlFlowGraphs.functionGraphs[functionIndex];
6170
yulAssert(functionGraphPtr);
6271
SSACFG const& cfg = *functionGraphPtr;
63-
auto const callSites = gatherCallSites(cfg);
6472
auto const& liveness = _controlFlowLiveness.cfgLiveness[functionIndex];
6573
yulAssert(liveness);
6674
auto const graphID = static_cast<ControlFlowGraphs::FunctionGraphID>(functionIndex);
75+
callSitesPerCFG.push_back(gatherCallSites(cfg));
6776
bool const spillingAllowed = !callGraph.isRecursive(graphID);
68-
auto const stackLayoutGeneratorResult = StackLayoutGenerator::generate(*liveness, callSites, graphID, spillingAllowed);
77+
auto [layout, spillSet] = StackLayoutGenerator::generate(*liveness, callSitesPerCFG.back(), graphID, spillingAllowed);
78+
layouts.push_back(std::move(layout));
79+
spillSetsPerCFG.push_back(std::move(spillSet));
80+
}
81+
82+
for (std::size_t functionIndex = 0; functionIndex < numCFGs; ++functionIndex)
83+
spillSetsPerCFG[functionIndex].closeUnderReachabilityConstraints(
84+
*_controlFlowGraphs.functionGraphs[functionIndex],
85+
layouts[functionIndex]
86+
);
87+
88+
// build up global addressing based on the spill sets
89+
spill::MemoryAddressing const addressing(_controlFlowGraphs, spillSetsPerCFG);
90+
91+
for (std::size_t functionIndex = 0; functionIndex < _controlFlowGraphs.functionGraphs.size(); ++functionIndex)
92+
{
93+
SSACFG const& cfg = *_controlFlowGraphs.functionGraphs[functionIndex];
94+
auto const graphID = static_cast<ControlFlowGraphs::FunctionGraphID>(functionIndex);
95+
auto const& liveness = _controlFlowLiveness.cfgLiveness[functionIndex];
96+
yulAssert(liveness);
6997
CodeTransform transform(
7098
_assembly,
7199
_builtinContext,
72-
controlFlowGraphs,
100+
_controlFlowGraphs,
73101
functionLabels,
74-
callSites,
102+
callSitesPerCFG[functionIndex],
75103
cfg,
76-
stackLayoutGeneratorResult.layout,
77-
graphID
104+
layouts[functionIndex],
105+
spillSetsPerCFG[functionIndex],
106+
graphID,
107+
addressing
78108
);
79109
transform(cfg.entry);
80110
}
@@ -120,7 +150,9 @@ CodeTransform::CodeTransform(
120150
CallSites const& _callSites,
121151
SSACFG const& _cfg,
122152
SSACFGStackLayout const& _stackLayout,
123-
ControlFlowGraphs::FunctionGraphID _graphID
153+
spill::SpillSet const& _spillSet,
154+
ControlFlowGraphs::FunctionGraphID _graphID,
155+
spill::MemoryAddressing const& _addressing
124156
):
125157
m_assembly(_assembly),
126158
m_builtinContext(_builtinContext),
@@ -129,6 +161,7 @@ CodeTransform::CodeTransform(
129161
m_callSites(_callSites),
130162
m_cfg(_cfg),
131163
m_stackLayout(_stackLayout),
164+
m_spillSet(_spillSet),
132165
m_graphID(_graphID),
133166
m_blockIsTransformed(_cfg.numBlocks(), false),
134167
m_blockLabels([this] {
@@ -138,11 +171,17 @@ CodeTransform::CodeTransform(
138171
blockLabels.push_back(m_assembly.newLabelId());
139172
return blockLabels;
140173
}()),
174+
m_spillEmitter([&]() -> std::optional<spill::Emitter> {
175+
if (_spillSet.numSpilled() > 0)
176+
return spill::Emitter(_addressing, _graphID, _assembly);
177+
return std::nullopt;
178+
}()),
141179
m_assemblyCallbacks{
142180
.cfg = &_cfg,
143181
.assembly = &_assembly,
144182
.callSites = &_callSites,
145-
.returnLabels = &m_returnLabels
183+
.returnLabels = &m_returnLabels,
184+
.spillEmitter = m_spillEmitter ? &*m_spillEmitter : nullptr
146185
},
147186
m_stackData([&]
148187
{
@@ -167,6 +206,11 @@ CodeTransform::CodeTransform(
167206
for (auto const& arg: m_cfg.arguments | ranges::views::reverse)
168207
expectedStackTop.push_back(StackSlot::makeValue(_cfg, arg));
169208
assertLayoutCompatibility(m_stack.data(), expectedStackTop);
209+
210+
// Spilled function args need an `mstore` at function entry so later `mload`s see a populated slot
211+
if (m_spillEmitter && isFunctionGraph)
212+
for (InstId const argId: m_cfg.arguments)
213+
spillStore(argId);
170214
}
171215

172216
void CodeTransform::operator()(SSACFG::BlockId const _blockId)
@@ -184,18 +228,28 @@ void CodeTransform::operator()(SSACFG::BlockId const _blockId)
184228
auto const& block = m_cfg.block(_blockId);
185229

186230
std::size_t operationIndex = 0;
187-
m_cfg.forEachOperation(block, [&](InstId const instId, SSACFG::Inst const&) {
188-
yulAssert(operationIndex < blockLayout->operationIn.size());
189-
auto const& operationInLayout = blockLayout->operationIn[operationIndex];
190-
(*this)(instId, operationInLayout);
191-
++operationIndex;
192-
});
231+
232+
// Iterate every Inst in the block in scheduled order. Only Operations advance codegen;
233+
// Phis are otherwise pure stack assertions (already materialized on the block's stackIn).
234+
for (InstId const instId: block.instructions)
235+
{
236+
SSACFG::Inst const& inst = m_cfg.inst(instId);
237+
if (inst.isPhi())
238+
// this is a no-op for not spilled phis
239+
spillStore(instId);
240+
if (inst.isOperation())
241+
{
242+
yulAssert(operationIndex < blockLayout->operationIn.size());
243+
(*this)(instId, blockLayout->operationIn[operationIndex]);
244+
++operationIndex;
245+
}
246+
}
193247
yulAssert(operationIndex == blockLayout->operationIn.size());
194248

195249
// Shuffle to the block's exit layout before dispatching the exit.
196250
// This ensures the condition is on top for ConditionalJump, phi pre-images are
197251
// in the right positions for jumps, and return values are accessible for FunctionReturn.
198-
auto const shuffleResult = StackShuffler<AssemblyCallbacks>::shuffle(m_stack, blockLayout->exitIn);
252+
auto const shuffleResult = StackShuffler<AssemblyCallbacks>::shuffle(m_stack, blockLayout->exitIn, &m_spillSet);
199253
yulAssert(shuffleResult.status == StackShufflerResult::Status::Admissible);
200254

201255
// handle the block exit
@@ -221,7 +275,7 @@ void CodeTransform::operator()(InstId _instId, StackData const& _operationInputL
221275

222276
// prepare stack for operation
223277
{
224-
auto const shuffleResult = StackShuffler<AssemblyCallbacks>::shuffle(m_stack, _operationInputLayout);
278+
auto const shuffleResult = StackShuffler<AssemblyCallbacks>::shuffle(m_stack, _operationInputLayout, &m_spillSet);
225279
yulAssert(shuffleResult.status == StackShufflerResult::Status::Admissible);
226280
}
227281

@@ -322,6 +376,11 @@ void CodeTransform::operator()(InstId _instId, StackData const& _operationInputL
322376
for (InstId const id: m_cfg.outputsOf(_instId))
323377
m_stack.push<false>(StackSlot::makeValue(m_cfg, id));
324378

379+
// Each output the layout decided to spill gets its `mstore` here
380+
if (m_spillEmitter)
381+
for (InstId const outputId: m_cfg.outputsOf(_instId))
382+
spillStore(outputId);
383+
325384
yulAssert(m_stack.size() == baseHeight + numOutputs);
326385
for (auto const& [stackEntry, output]: ranges::views::zip(
327386
m_stack.data() | ranges::views::take_last(numOutputs),
@@ -334,6 +393,32 @@ void CodeTransform::operator()(InstId _instId, StackData const& _operationInputL
334393
);
335394
}
336395

396+
void CodeTransform::spillStore(InstId const _value)
397+
{
398+
if (!m_spillEmitter || !m_spillSet.isSpilled(_value))
399+
return;
400+
401+
// Bring `_value` to the stack top so that the `mstore` below consumes it
402+
StackData target = m_stack.data();
403+
target.push_back(StackSlot::makeValue(m_cfg, _value));
404+
// addr(_value) is the slot THIS `mstore` populates, so we have to exclude it from the spill set to
405+
// prevent the shuffler from just `mload`ing it back
406+
spill::SpillSet const spillSetExcludingSpillee = m_spillSet.without(_value);
407+
auto const shuffleResult = StackShuffler<AssemblyCallbacks>::shuffle(m_stack, target, &spillSetExcludingSpillee);
408+
yulAssert(
409+
shuffleResult.status == StackShufflerResult::Status::Admissible,
410+
fmt::format(
411+
"shuffler failed to bring spilled value {} to top (status={})",
412+
_value,
413+
static_cast<int>(shuffleResult.status)
414+
)
415+
);
416+
417+
m_spillEmitter->emitStore(_value);
418+
// `mstore` consumed the value; the address it pushed never persists on the symbolic stack
419+
m_stack.pop<false>();
420+
}
421+
337422
void CodeTransform::operator()(SSACFG::BlockId const&, SSACFG::BasicBlock::MainExit const&)
338423
{
339424
yulAssert(static_cast<int>(m_stack.size()) == m_assembly.stackHeight());
@@ -446,7 +531,7 @@ void CodeTransform::prepareBlockExitStack(StackData const& _target, PhiInverse c
446531
auto const pulledBackTarget = stackPreImage(m_cfg, _target, _phiInverse);
447532
// shuffle to target
448533
{
449-
auto const shuffleResult = StackShuffler<AssemblyCallbacks>::shuffle(m_stack, pulledBackTarget);
534+
auto const shuffleResult = StackShuffler<AssemblyCallbacks>::shuffle(m_stack, pulledBackTarget, &m_spillSet);
450535
yulAssert(shuffleResult.status == StackShufflerResult::Status::Admissible);
451536
}
452537
// check that shuffling was successful

libyul/backends/evm/ssa/CodeTransform.h

Lines changed: 24 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,8 @@
1818

1919
#pragma once
2020

21+
#include <libyul/backends/evm/ssa/spill/Emitter.h>
22+
2123
#include <libyul/backends/evm/ssa/PhiInverse.h>
2224
#include <libyul/backends/evm/ssa/Stack.h>
2325
#include <libyul/backends/evm/ssa/StackLayout.h>
@@ -52,9 +54,17 @@ struct AssemblyCallbacks
5254
case StackSlot::Kind::Value:
5355
{
5456
auto const id = _slot.value();
55-
yulAssert(cfg->isLiteral(id), fmt::format("Tried bringing up non-const {}", id));
56-
assembly->appendConstant(cfg->literalPayload(id));
57-
return;
57+
if (cfg->isLiteral(id))
58+
{
59+
assembly->appendConstant(cfg->literalPayload(id));
60+
return;
61+
}
62+
if (spillEmitter && spillEmitter->hasAddress(id))
63+
{
64+
spillEmitter->emitLoad(id);
65+
return;
66+
}
67+
yulAssert(false, fmt::format("Tried bringing up non-spilled non-const {}", id));
5868
}
5969
case StackSlot::Kind::Junk:
6070
{
@@ -87,6 +97,7 @@ struct AssemblyCallbacks
8797
AbstractAssembly* assembly{};
8898
CallSites const* callSites{};
8999
std::map<InstId, AbstractAssembly::LabelID> const* returnLabels{};
100+
spill::Emitter const* spillEmitter{};
90101
};
91102
static_assert(StackManipulationCallbackConcept<AssemblyCallbacks>);
92103

@@ -95,6 +106,7 @@ class CodeTransform
95106
public:
96107
static void run(
97108
AbstractAssembly& _assembly,
109+
ControlFlowGraphs& _controlFlowGraphs,
98110
ControlFlowGraphsLiveness const& _liveness,
99111
BuiltinContext& _builtinContext
100112
);
@@ -115,7 +127,10 @@ class CodeTransform
115127
CallSites const& _callSites,
116128
SSACFG const& _cfg,
117129
SSACFGStackLayout const& _stackLayout,
118-
ControlFlowGraphs::FunctionGraphID _graphID);
130+
spill::SpillSet const& _spillSet,
131+
ControlFlowGraphs::FunctionGraphID _graphID,
132+
spill::MemoryAddressing const& _addressing
133+
);
119134

120135
void operator()(SSACFG::BlockId _blockId);
121136
void operator()(InstId _instId, StackData const& _operationInputLayout);
@@ -127,17 +142,22 @@ class CodeTransform
127142

128143
void prepareBlockExitStack(StackData const& _target, PhiInverse const& _phiInverse);
129144

145+
/// If `_value` is spilled, shuffles it to the stack top and stores it into its memory slot
146+
void spillStore(InstId _value);
147+
130148
AbstractAssembly& m_assembly;
131149
BuiltinContext& m_builtinContext;
132150
ControlFlowGraphs const& m_controlFlow;
133151
FunctionLabels const& m_functionLabels;
134152
CallSites const& m_callSites;
135153
SSACFG const& m_cfg;
136154
SSACFGStackLayout const& m_stackLayout;
155+
spill::SpillSet const& m_spillSet;
137156
ControlFlowGraphs::FunctionGraphID const m_graphID;
138157

139158
std::vector<std::uint8_t> m_blockIsTransformed;
140159
std::vector<AbstractAssembly::LabelID> m_blockLabels;
160+
std::optional<spill::Emitter> m_spillEmitter{std::nullopt};
141161
AssemblyCallbacks m_assemblyCallbacks;
142162
StackData m_stackData;
143163
Stack<AssemblyCallbacks> m_stack;

libyul/backends/evm/ssa/SSACFGTypes.h

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,9 @@ template<typename R, typename T>
3737
concept InputRangeOf = ranges::input_range<R> && std::same_as<ranges::range_value_t<R>, T>;
3838

3939
class SSACFG;
40-
40+
class SSACFGStackLayout;
41+
class StackSlot;
42+
using StackData = std::vector<StackSlot>;
4143
using FunctionGraphID = std::uint32_t;
4244

4345
struct BlockId

0 commit comments

Comments
 (0)