-
Notifications
You must be signed in to change notification settings - Fork 5.5k
[Wasm RyuJIT] Spill live ref/byref values to pinned stack slots at calls #129059
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
efd6a44
02375bb
9a2f3da
1e66891
5dd8f82
c90720a
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -1671,6 +1671,137 @@ PhaseStatus Compiler::fgWasmControlFlow() | |
| return PhaseStatus::MODIFIED_EVERYTHING; | ||
| } | ||
|
|
||
| PhaseStatus Compiler::WasmSpillRefs() | ||
| { | ||
| bool anyChanges = false; | ||
|
|
||
| size_t highWaterMark = 0; | ||
| jitstd::vector<GenTree*> defs(getAllocator(CMK_WasmSpillRefs)); | ||
|
|
||
| for (BasicBlock* const block : Blocks()) | ||
| { | ||
| // LIR edges cannot span blocks, so we can safely clear the list of live values per-block | ||
| defs.clear(); | ||
|
|
||
| for (GenTree* tree : LIR::AsRange(block)) | ||
| { | ||
| if (tree->IsCall()) | ||
| { | ||
| highWaterMark = std::max(highWaterMark, defs.size()); | ||
|
|
||
| // For any ref/byref values live at the point of a call, spill them into pinned slots | ||
| // on the stack where the GC can see them so it won't move them. | ||
| if (defs.size()) | ||
| { | ||
| anyChanges = true; | ||
|
|
||
| if (!m_wasmSpillSlots) | ||
| { | ||
| m_wasmSpillSlots = new (this, CMK_WasmSpillRefs) jitstd::vector<unsigned>(getAllocator(CMK_WasmSpillRefs)); | ||
| } | ||
|
|
||
| unsigned spillSlotIndex = 0; | ||
| JITDUMP("Spilling %zu live ref(s) for call\n", defs.size()); | ||
| DISPNODE(tree); | ||
| for (GenTree* def : defs) | ||
| { | ||
| JITDUMP(" "); | ||
| DISPNODE(def); | ||
|
|
||
| unsigned spillSlot; | ||
| if (spillSlotIndex < m_wasmSpillSlots->size()) | ||
| { | ||
| spillSlot = m_wasmSpillSlots->at(spillSlotIndex); | ||
| } | ||
| else | ||
| { | ||
| spillSlot = lvaGrabTemp(false DEBUGARG("WasmSpillRefs spill slot")); | ||
| LclVarDsc* const varDsc = lvaGetDesc(spillSlot); | ||
| varDsc->lvType = TYP_BYREF; | ||
| varDsc->lvPinned = true; | ||
| varDsc->lvImplicitlyReferenced = true; | ||
| varDsc->lvMustInit = true; | ||
|
Comment on lines
+1719
to
+1723
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Isn't it fine to store a REF in a pinned BYREF? Or no?
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Storing is fine, it's the reloading that is a bit iffy, we have to trust the thing stored there was really a ref. In your case we know this but the IR doesn't. I would probably just keep two pools for now like copilot suggests. Possibly reporting refs as refs is a bit cheaper for GC. |
||
| lvaSetVarDoNotEnregister(spillSlot, DoNotEnregisterReason::WasmGCVisibility); | ||
| m_wasmSpillSlots->push_back(spillSlot); | ||
| } | ||
| spillSlotIndex++; | ||
|
|
||
| GenTreeLclVar *spill = gtNewStoreLclVarNode(spillSlot, def); | ||
| GenTreeLclVar *reload = gtNewLclVarNode(spillSlot); | ||
| LIR::Use use; | ||
| noway_assert(LIR::AsRange(block).TryGetUse(def, &use)); | ||
| use.ReplaceWith(reload); | ||
| LIR::AsRange(block).InsertAfter(def, spill); | ||
| LIR::AsRange(block).InsertAfter(spill, reload); | ||
| if (def->gtLIRFlags & LIR::Flags::MultiplyUsed) | ||
| { | ||
| JITDUMP("Transferring multiply-used flag from [%06u] to [%06u] for spill\n", Compiler::dspTreeID(def), Compiler::dspTreeID(reload)); | ||
| def->gtLIRFlags &= ~LIR::Flags::MultiplyUsed; | ||
| reload->gtLIRFlags |= LIR::Flags::MultiplyUsed; | ||
| } | ||
|
Comment on lines
+1729
to
+1741
|
||
| anyChanges = true; | ||
| } | ||
|
|
||
| defs.clear(); | ||
| } | ||
| } | ||
|
|
||
| // FIXME: Should this happen before the spilling of the live defs list? | ||
| // I think the answer is no, because live defs being passed as arguments to the current call | ||
| // are not guaranteed to ever end up in memory where the GC can see them unless we spill | ||
| // them. If we can somehow guarantee that all callees will spill their ref parameters | ||
| // immediately, we could do this before the block above. | ||
|
|
||
| // Remove used nodes from defs list, they're no longer meaningfully 'live'. | ||
| tree->VisitOperands([&defs](GenTree* op) { | ||
| if (!op->IsValue()) | ||
| return GenTree::VisitResult::Continue; | ||
| if (!op->TypeIs(TYP_REF, TYP_BYREF)) | ||
| return GenTree::VisitResult::Continue; | ||
|
|
||
| for (size_t i = defs.size(); i > 0; i--) | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Probably worth commenting what this is doing (removing active defs once we find their use, and keeping the defs collection compact). |
||
| { | ||
| if (op == defs[i - 1]) | ||
| { | ||
| defs[i - 1] = defs[defs.size() - 1]; | ||
| defs.pop_back(); | ||
| break; | ||
| } | ||
| } | ||
|
|
||
| return GenTree::VisitResult::Continue; | ||
| }); | ||
|
|
||
| // We only care about used values, and invariant nodes can't produce movable GC refs, so skip | ||
| // nodes appropriately | ||
| if (!tree->IsValue() || tree->IsUnusedValue() || tree->IsInvariant()) | ||
| { | ||
| continue; | ||
| } | ||
|
|
||
| // If a value is just a GT_LCL_VAR that isn't address-exposed, by construction we ensure that | ||
| // it won't be mutated between its def (here) and its use (the call that would produce a spill) | ||
| // and we won't need to spill it. | ||
| if (tree->OperIs(GT_LCL_VAR)) | ||
| { | ||
| LclVarDsc* dsc = lvaGetDesc(tree->AsLclVarCommon()); | ||
| if (!dsc->IsAddressExposed()) | ||
| continue; | ||
| } | ||
|
|
||
| // We have a ref sourced from something like a call result or an indirection that hasn't been | ||
| // spilled yet, so record it for potential spilling at the next call. | ||
| if (tree->TypeIs(TYP_REF, TYP_BYREF)) | ||
| { | ||
| defs.push_back(tree); | ||
| } | ||
| } | ||
| } | ||
|
|
||
| JITDUMP("High water mark for refs was %zu\n", highWaterMark); | ||
| return anyChanges ? PhaseStatus::MODIFIED_EVERYTHING : PhaseStatus::MODIFIED_NOTHING; | ||
| } | ||
|
|
||
| #ifdef DEBUG | ||
|
|
||
| //------------------------------------------------------------------------ | ||
|
|
||
Uh oh!
There was an error while loading. Please reload this page.