Skip to content

Commit b614c8f

Browse files
committed
[Wasm] Spill live ref/byref values to pinned stack slots at calls so the GC won't move them
1 parent ecaa1c5 commit b614c8f

11 files changed

Lines changed: 160 additions & 0 deletions

File tree

src/coreclr/jit/codegen.h

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -217,6 +217,7 @@ class CodeGen final : public CodeGenInterface
217217
ArrayStack<WasmInterval*>* wasmControlFlowStack = nullptr;
218218
unsigned wasmCursor = 0;
219219
unsigned wasmExtraControlFlowDepth = 0;
220+
unsigned wasmSpillRefIndex = 0;
220221
unsigned findTargetDepth(BasicBlock* target);
221222
void WasmProduceReg(GenTree* node);
222223
regNumber GetMultiUseOperandReg(GenTree* operand);

src/coreclr/jit/codegenlinear.cpp

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -459,6 +459,9 @@ void CodeGen::genCodeForBlock(BasicBlock* block)
459459
#endif
460460

461461
#ifdef TARGET_WASM
462+
// Reset spill counter at block boundaries.
463+
wasmSpillRefIndex = 0;
464+
462465
// genHomeRegisterParams can generate arbitrary amounts of code on Wasm, so
463466
// we have moved it out of the prolog to the first basic block in order to
464467
// work around the restriction that the prolog can only be one insGroup.

src/coreclr/jit/codegenwasm.cpp

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -811,6 +811,7 @@ void CodeGen::genCodeForTreeNode(GenTree* treeNode)
811811
break;
812812

813813
case GT_CALL:
814+
wasmSpillRefIndex = 0;
814815
genCall(treeNode->AsCall());
815816
break;
816817

@@ -864,6 +865,36 @@ void CodeGen::genCodeForTreeNode(GenTree* treeNode)
864865
GetEmitter()->emitIns(INS_unreachable);
865866
break;
866867

868+
case GT_WASM_SPILL_REF:
869+
{
870+
const unsigned splashZoneVar = m_compiler->m_wasmSpillSlots->at(0);
871+
noway_assert(wasmSpillRefIndex + 1 < m_compiler->m_wasmSpillSlots->size());
872+
const unsigned spillTargetVar = m_compiler->m_wasmSpillSlots->at(wasmSpillRefIndex + 1);
873+
unsigned splashZoneLclIndex;
874+
bool FPBased;
875+
876+
{
877+
LclVarDsc* varDsc = m_compiler->lvaGetDesc(splashZoneVar);
878+
assert(genIsValidReg(varDsc->GetRegNum()));
879+
splashZoneLclIndex = WasmRegToIndex(varDsc->GetRegNum());
880+
881+
GetEmitter()->emitIns_I(INS_local_tee, EA_PTRSIZE, splashZoneLclIndex);
882+
}
883+
884+
GetEmitter()->emitIns_I(INS_local_get, EA_PTRSIZE, GetFramePointerRegIndex());
885+
m_compiler->lvaFrameAddress(spillTargetVar, &FPBased);
886+
GetEmitter()->emitIns_S(INS_I_const, EA_PTRSIZE, spillTargetVar, 0);
887+
GetEmitter()->emitIns(INS_I_add);
888+
889+
GetEmitter()->emitIns_I(INS_local_get, EA_PTRSIZE, splashZoneLclIndex);
890+
891+
instruction ins = ins_Store(TYP_BYREF);
892+
GetEmitter()->emitIns_I(ins, EA_PTRSIZE, 0);
893+
894+
wasmSpillRefIndex++;
895+
break;
896+
}
897+
867898
case GT_CATCH_ARG:
868899
genCatchArg(treeNode);
869900
break;

src/coreclr/jit/compiler.cpp

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5053,6 +5053,11 @@ void Compiler::compCompile(void** methodCodePtr, uint32_t* methodCodeSize, JitFl
50535053
// keep the Virtual IP updated.
50545054
//
50555055
DoPhase(this, PHASE_WASM_VIRTUAL_IP, &Compiler::fgWasmVirtualIP);
5056+
5057+
// Ensure that any refs or byrefs live at call sites are spilled
5058+
// to pinned stack slots so the objects aren't moved.
5059+
//
5060+
DoPhase(this, PHASE_WASM_SPILL_REFS, &Compiler::WasmSpillRefs);
50565061
#endif
50575062

50585063
FinalizeEH();

src/coreclr/jit/compiler.h

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4244,6 +4244,7 @@ class Compiler
42444244
unsigned lvaWasmVirtualIP = BAD_VAR_NUM; // Wasm virtual IP slot
42454245
unsigned lvaWasmFunctionIndex = BAD_VAR_NUM; // Wasm function index slot
42464246
unsigned lvaWasmResumeIP = BAD_VAR_NUM; // Wasm catch resumption IP slot
4247+
jitstd::vector<unsigned>* m_wasmSpillSlots = nullptr;
42474248
#endif // defined(TARGET_WASM)
42484249

42494250
unsigned lvaInlinedPInvokeFrameVar = BAD_VAR_NUM; // variable representing the InlinedCallFrame
@@ -6757,6 +6758,7 @@ class Compiler
67576758
PhaseStatus fgWasmControlFlow();
67586759
PhaseStatus fgWasmTransformSccs();
67596760
PhaseStatus fgWasmVirtualIP();
6761+
PhaseStatus WasmSpillRefs();
67606762
#ifdef DEBUG
67616763
void fgDumpWasmControlFlow();
67626764
void fgDumpWasmControlFlowDot();

src/coreclr/jit/compmemkind.h

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -73,6 +73,7 @@ CompMemKindMacro(RangeCheckCloning)
7373
CompMemKindMacro(WasmSccTransform)
7474
CompMemKindMacro(WasmCfgLowering)
7575
CompMemKindMacro(WasmEH)
76+
CompMemKindMacro(WasmSpillRefs)
7677
//clang-format on
7778

7879
#undef CompMemKindMacro

src/coreclr/jit/compphases.h

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -130,6 +130,7 @@ CompPhaseNameMacro(PHASE_DFS_BLOCKS_WASM, "Wasm remove unreachable bl
130130
CompPhaseNameMacro(PHASE_WASM_EH_FLOW, "Wasm eh control flow", false, -1, false)
131131
CompPhaseNameMacro(PHASE_WASM_TRANSFORM_SCCS, "Wasm transform sccs", false, -1, false)
132132
CompPhaseNameMacro(PHASE_WASM_CONTROL_FLOW, "Wasm control flow", false, -1, false)
133+
CompPhaseNameMacro(PHASE_WASM_SPILL_REFS, "Wasm spill refs", false, -1, false)
133134
CompPhaseNameMacro(PHASE_WASM_VIRTUAL_IP, "Wasm virtual IP", false, -1, false)
134135

135136
CompPhaseNameMacro(PHASE_ASYNC, "Transform async", false, -1, true)

src/coreclr/jit/fgwasm.cpp

Lines changed: 106 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1649,6 +1649,112 @@ PhaseStatus Compiler::fgWasmControlFlow()
16491649
return PhaseStatus::MODIFIED_EVERYTHING;
16501650
}
16511651

1652+
PhaseStatus Compiler::WasmSpillRefs()
1653+
{
1654+
bool anyChanges = false;
1655+
1656+
size_t highWaterMark = 0;
1657+
jitstd::vector<GenTree*> defs(getAllocator(CMK_WasmSpillRefs));
1658+
1659+
for (BasicBlock* const block : Blocks())
1660+
{
1661+
// LIR edges cannot span blocks, so we can safely clear the list of live values per-block
1662+
defs.clear();
1663+
1664+
for (GenTree* tree : LIR::AsRange(block))
1665+
{
1666+
if (tree->IsCall())
1667+
{
1668+
highWaterMark = std::max(highWaterMark, defs.size());
1669+
1670+
if (defs.size())
1671+
{
1672+
JITDUMP("Spilling %d live ref(s) for call\n", defs.size());
1673+
DISPNODE(tree);
1674+
for (GenTree* def : defs)
1675+
{
1676+
JITDUMP(" ");
1677+
DISPNODE(def);
1678+
GenTreeUnOp* spill = gtNewOperNode(GT_WASM_SPILL_REF, def->TypeGet(), def);
1679+
LIR::Use use;
1680+
noway_assert(LIR::AsRange(block).TryGetUse(def, &use));
1681+
use.ReplaceWith(spill);
1682+
LIR::AsRange(block).InsertAfter(def, spill);
1683+
anyChanges = true;
1684+
}
1685+
1686+
defs.clear();
1687+
}
1688+
}
1689+
1690+
// FIXME: Should this happen before the spilling of the live defs list?
1691+
// I think the answer is no, because live defs being passed as arguments to the current call
1692+
// are not guaranteed to ever end up in memory where the GC can see them unless we spill
1693+
// them. If we can somehow guarantee that all callees will spill their ref parameters
1694+
// immediately, we could do this before the block above.
1695+
// Remove used nodes from defs list, they're no longer meaningfully 'live'.
1696+
tree->VisitOperands([&defs](GenTree* op) {
1697+
if (!op->IsValue())
1698+
return GenTree::VisitResult::Continue;
1699+
if (!op->TypeIs(TYP_REF, TYP_BYREF))
1700+
return GenTree::VisitResult::Continue;
1701+
1702+
for (size_t i = defs.size(); i > 0; i--)
1703+
{
1704+
if (op == defs[i - 1])
1705+
{
1706+
defs[i - 1] = defs[defs.size() - 1];
1707+
defs.erase(defs.begin() + (defs.size() - 1), defs.end());
1708+
break;
1709+
}
1710+
}
1711+
1712+
return GenTree::VisitResult::Continue;
1713+
});
1714+
1715+
if (tree->IsValue() && tree->TypeIs(TYP_REF, TYP_BYREF) && !tree->OperIs(GT_WASM_SPILL_REF))
1716+
{
1717+
// TODO: Can we skip this for GT_LCL_VAR when it lives in memory? Or is it possible
1718+
// that the LCL_VAR has been modified since it was loaded onto the Wasm stack?
1719+
defs.push_back(tree);
1720+
}
1721+
}
1722+
}
1723+
1724+
JITDUMP("High water mark for refs was %d\n", highWaterMark);
1725+
if (highWaterMark == 0)
1726+
return PhaseStatus::MODIFIED_NOTHING;
1727+
1728+
m_wasmSpillSlots = new (this, CMK_WasmSpillRefs) jitstd::vector<unsigned>(highWaterMark + 1, 0, getAllocator(CMK_WasmSpillRefs));
1729+
1730+
// Allocate a temporary wasm local to use as a scratch slot during spills
1731+
{
1732+
const unsigned varNum = lvaGrabTemp(false DEBUGARG("WasmSpillRefs splash zone"));
1733+
LclVarDsc* const varDsc = lvaGetDesc(varNum);
1734+
// HACK: Make this TYP_I_IMPL because if we make it a REF or BYREF that may block enregistration
1735+
varDsc->lvType = TYP_I_IMPL;
1736+
varDsc->lvHasExplicitInit = true;
1737+
varDsc->lvImplicitlyReferenced = true;
1738+
// If we don't make this var tracked, regalloc will crash when allocating a register for it
1739+
varDsc->lvTracked = true;
1740+
m_wasmSpillSlots->at(0) = varNum;
1741+
}
1742+
1743+
// Allocate N temporary refs to act as GC-visible storage for all spills that occur during execution
1744+
for (size_t i = 0; i < highWaterMark; i++)
1745+
{
1746+
const unsigned varNum = lvaGrabTemp(false DEBUGARG("WasmSpillRefs spill slot"));
1747+
LclVarDsc* const varDsc = lvaGetDesc(varNum);
1748+
varDsc->lvType = TYP_BYREF;
1749+
varDsc->lvPinned = true;
1750+
varDsc->lvImplicitlyReferenced = true;
1751+
lvaSetVarDoNotEnregister(varNum, DoNotEnregisterReason::WasmGCVisibility);
1752+
m_wasmSpillSlots->at(i + 1) = varNum;
1753+
}
1754+
1755+
return PhaseStatus::MODIFIED_EVERYTHING;
1756+
}
1757+
16521758
#ifdef DEBUG
16531759

16541760
//------------------------------------------------------------------------

src/coreclr/jit/gentree.cpp

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11767,6 +11767,9 @@ GenTreeUseEdgeIterator::GenTreeUseEdgeIterator(GenTree* node)
1176711767
case GT_RETURN_SUSPEND:
1176811768
case GT_PATCHPOINT_FORCED:
1176911769
case GT_NONLOCAL_JMP:
11770+
#ifdef TARGET_WASM
11771+
case GT_WASM_SPILL_REF:
11772+
#endif
1177011773
m_edge = &m_node->AsUnOp()->gtOp1;
1177111774
assert(*m_edge != nullptr);
1177211775
m_advance = &GenTreeUseEdgeIterator::Terminate;

src/coreclr/jit/gtlist.h

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -357,6 +357,7 @@ GTNODE(SWIFT_ERROR_RET , GenTreeOp ,0,1,GTK_BINOP|GTK_NOVALUE) // Retu
357357

358358
GTNODE(WASM_JEXCEPT , GenTree ,0,0,GTK_LEAF|GTK_NOVALUE|DBK_NOTHIR) // Special jump for Wasm exception handling
359359
GTNODE(WASM_THROW_REF , GenTree ,0,0,GTK_LEAF|GTK_NOVALUE|DBK_NOTHIR) // Wasm rethrow host exception (exception is an implicit operand)
360+
GTNODE(WASM_SPILL_REF , GenTreeOp ,0,0,GTK_UNOP|DBK_NOTHIR)
360361

361362
//-----------------------------------------------------------------------------
362363
// Nodes used by Lower to generate a closer CPU representation of other nodes

0 commit comments

Comments
 (0)