Skip to content

Commit 1fb56db

Browse files
committed
[ownership] Implement movable guaranteed scopes in ossa.
This patch implements movable guaranteed scopes in ossa. This pattern is currently not generated anywhere in the compiler, but my hope is to begin emitting these in SemanticARCOpts. The idea is that these model true phi nodes and thus can be used to fuse multiple guaranteed scopes into one using br instructions. This is treated similarly to how owned instructions are forwarded through /all/ terminators. This will enable us to use the SILSSAUpdater with guaranteed arguments as well as enable the expression of sets of borrow scopes that minimally jointly-dominate a guaranteed argument. This will enable us to express +0 merge points like the following: ``` bb1: %0a = begin_borrow %0 : $Klass br bb3(%0a : $Klass) bb2: %1a = load_borrow %1 : $*Klass br bb3(%1a : $Klass) bb3(%2 : @guaranteed $Klass) ... end_borrow %2 : $Klass ``` I describe below what the semantics of guaranteed block arguments were previously, what they are now, and a little bit of interesting things from a semantic perspective around implicit sub-scope users. Before this patch in ossa, guaranteed block arguments had two different sets of semantics: 1. Given a checked_cast_br or a switch_enum, the guaranteed block argument was treated like a forwarding instruction. As such, the guaranteed argument's did not require an end_borrow and its uses were validated as part of the use list of the switch_enum/checked_cast_br operand's borrow introducer. It also was not classified as a BorrowScopeValueIntroducer since it was not introducing a new scope. 2. Given any other predecessor terminator, we treated the guaranteed argument as a complete sub-scope of its incoming values. Thus we required the guaranteed argument to have its lifetime eneded by an end_borrow and that all incoming values of the guaranteed argument to come from a borrow introducer whose set of jointly post-dominating end_borrows also jointly post-dominates the set of end_borrows associated with the guaranteed argument itself. Consider the following example: ``` bb0: %1 = begin_borrow %foo : $Foo // (1) %2 = begin_borrow %foo2 : $Foo2 // (2) cond_br ..., bb1, bb2 bb1: br bb3(%1 : $Foo) bb2: br bb3(%2 : $Foo) bb3(%3 : @guaranteed $Foo) ... end_borrow %3 : $Foo // (3) end_borrow %2 : $Foo // (4) end_borrow %1 : $Foo // (5) ... ``` Notice how due to SSA, (1) and (2) must dominate (4) and (5) and thus must dominate bb3, preventing the borrows from existing within bb1, bb2. This dominance property is actively harmful to expressivity in SIL since it means that guaranteed arguments can not be used to express (without contortion) sil code patterns where an argument is jointly-dominated by a minimal set of guaranteed incoming values. For instance, consider the following SIL example: ``` bb0: cond_br ..., bb1, bb2 bb1: %0 = load [copy] %globalAddr : $Foo br bb3(%0 : $Foo) bb2: %1 = copy_value %guaranteedFunctionArg : $Foo br bb3(%1 : $Foo): bb3(%2 : @owned $Foo): apply %useFoo(%2) destroy_value %2 : $Foo ``` As a quick proof: Assume the previous rules for guaranteed arguments. Then to promote the load [copy] -> load_borrow and the copy_value to a begin_borrow, we would need to place an end_borrow in bb3. But neither bb1 or bb2 dominates bb3, so we would violate SSA dominance rules. To enable SIL to express this pattern, we introduce a third rule for terminator in ossa that applies only to branch insts. All other branches that obeyed the previous rules (cond_br), still follow the old rule. This is not on purpose, I am just being incremental and changing things as I need to. Specifically, guaranteed arguments whose incoming values are defined by branch instructions now act as a move on guaranteed values. The intuition here is that these arguments are acting as true phis in an SSA sense and thus are just new names for the incoming values. This implies since it is just a new name (not a semantic change) that the guaranteed incoming value's guaranteed scopes should be fused into one scope. The natural way to model this is by treating branch insts as consuming guaranteed values. This then lets us express the example above without using copies as follows: ``` bb0: cond_br ..., bb1, bb2 bb1: %0 = load_borrow %globalAddr : $Foo br bb3(%0 : $Foo) // consumes %0 and acts as %0's end_borrow. bb2: // We need to introduce a new begin_borrow here since function // arguments are required to never be consumed. %1 = begin_borrow %guaranteedFunctionArg : $Foo br bb3(%1 : $Foo) // consumes %1 and acts as %1's end_borrow // %2 continues the guaranteed scope of %0, %1. This time fused with one name. bb3(%2 : @guaranteed $Foo): apply %useFoo(%2) // End the lifetime of %2 (which implicitly ends the lifetime of %0, %1). end_borrow %2 : $Foo ... ``` The main complication for users is that now when attempting to discover the set of implicit users on an owned or guaranteed value caused by their usage as an argument of a borrow introducer like begin_borrow. For those who are unaware, a begin_borrow places an implicit requirement on its parent value that the parent value is alive for the entire part of the CFG where this begin_borrow is live. Previously, one could just look for the end_borrows of the begin_borrow. Now one must additionally look for consuming branch insts. This is because the original value that is being borrowed from must be alive over the entire web of guaranteed values. That is the entire web of guaranteed values act as a liveness requirement on the begin_borrow's operand. The way this is implemented is given a use that we are validating, if the use is a BorrowScopeOperand (1), we see if the borrow scope operand consumes the given guaranteed scope and forwards it into a borrow scope introducer. If so, we add the list of consuming uses of the borrow scope introducer to the worklist to visit and then iterate. In order to avoid working with cycles, for now, the ownership verifier bans liveness requiring uses that have cycles in them. This still allows us to have loop carried guaranteed values. (1) A BorrowScopeOperand is a concept that represents an operand to a SIL instruction that begins a guaranteed scope of some sort. All BorrowScopeOperand are thus at a minimum able to compute a compile time the static region in which they implicitly use their operands. NOTE: We do not require the scope to be represented as a SILValue in the same function. We achieve some nice benefit by introducing this. Specifically: 1. We can optimize the pattern I mentioned above. This is a common pattern in many frameworks that want to return a default object if a computation fails (with the default object usually being some sort of global or static var). This will let us optimize that case when the global is a let global. 2. The SSA Updater can now be used with guaranteed values without needing to introduce extra copies. This will enable predictable mem opts to introduce less copies and for semantic arc opts to optimize the remaining copies that PMO exposes but does not insert itself. rdar://56720519
1 parent 42e8a96 commit 1fb56db

File tree

12 files changed

+1039
-121
lines changed

12 files changed

+1039
-121
lines changed

include/swift/SIL/OwnershipUtils.h

Lines changed: 125 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -199,22 +199,30 @@ bool isValueAddressOrTrivial(SILValue v);
199199
/// These operations forward both owned and guaranteed ownership.
200200
bool isOwnershipForwardingValueKind(SILNodeKind kind);
201201

202+
/// Is this an instruction that can forward both owned and guaranteed ownership
203+
/// kinds.
204+
bool isOwnershipForwardingInst(SILInstruction *i);
205+
206+
/// Is this an instruction that can forward guaranteed ownership.
207+
bool isGuaranteedForwardingInst(SILInstruction *i);
208+
202209
/// These operations forward guaranteed ownership, but don't necessarily forward
203210
/// owned values.
204211
bool isGuaranteedForwardingValueKind(SILNodeKind kind);
205212

213+
/// Is this a value that is the result of an operation that forwards owned
214+
/// ownership.
206215
bool isGuaranteedForwardingValue(SILValue value);
207216

208-
bool isOwnershipForwardingInst(SILInstruction *i);
209-
210-
bool isGuaranteedForwardingInst(SILInstruction *i);
217+
/// Is this a node kind that can forward owned ownership, but may not be able to
218+
/// forward guaranteed ownership.
219+
bool isOwnedForwardingValueKind(SILNodeKind kind);
211220

212221
struct BorrowScopeOperandKind {
213-
using UnderlyingKindTy = std::underlying_type<SILInstructionKind>::type;
214-
215-
enum Kind : UnderlyingKindTy {
216-
BeginBorrow = UnderlyingKindTy(SILInstructionKind::BeginBorrowInst),
217-
BeginApply = UnderlyingKindTy(SILInstructionKind::BeginApplyInst),
222+
enum Kind {
223+
BeginBorrow,
224+
BeginApply,
225+
Branch,
218226
};
219227

220228
Kind value;
@@ -232,6 +240,8 @@ struct BorrowScopeOperandKind {
232240
return BorrowScopeOperandKind(BeginBorrow);
233241
case SILInstructionKind::BeginApplyInst:
234242
return BorrowScopeOperandKind(BeginApply);
243+
case SILInstructionKind::BranchInst:
244+
return BorrowScopeOperandKind(Branch);
235245
}
236246
}
237247

@@ -242,9 +252,15 @@ struct BorrowScopeOperandKind {
242252
llvm::raw_ostream &operator<<(llvm::raw_ostream &os,
243253
BorrowScopeOperandKind kind);
244254

255+
struct BorrowScopeIntroducingValue;
256+
245257
/// An operand whose user instruction introduces a new borrow scope for the
246258
/// operand's value. The value of the operand must be considered as implicitly
247259
/// borrowed until the user's corresponding end scope instruction.
260+
///
261+
/// NOTE: We do not require that the guaranteed scope be represented by a
262+
/// guaranteed value in the same function: see begin_apply. In such cases, we
263+
/// require instead an end_* instruction to mark the end of the scope's region.
248264
struct BorrowScopeOperand {
249265
BorrowScopeOperandKind kind;
250266
Operand *op;
@@ -270,6 +286,66 @@ struct BorrowScopeOperand {
270286

271287
void visitEndScopeInstructions(function_ref<void(Operand *)> func) const;
272288

289+
/// Returns true if this borrow scope operand consumes guaranteed
290+
/// values and produces a new scope afterwards.
291+
bool consumesGuaranteedValues() const {
292+
switch (kind) {
293+
case BorrowScopeOperandKind::BeginBorrow:
294+
case BorrowScopeOperandKind::BeginApply:
295+
return false;
296+
case BorrowScopeOperandKind::Branch:
297+
return true;
298+
}
299+
llvm_unreachable("Covered switch isn't covered?!");
300+
}
301+
302+
/// Is this a borrow scope operand that can open new borrow scopes
303+
/// for owned values.
304+
bool canAcceptOwnedValues() const {
305+
switch (kind) {
306+
case BorrowScopeOperandKind::BeginBorrow:
307+
case BorrowScopeOperandKind::BeginApply:
308+
return true;
309+
case BorrowScopeOperandKind::Branch:
310+
return false;
311+
}
312+
llvm_unreachable("Covered switch isn't covered?!");
313+
}
314+
315+
/// Is the result of this instruction also a borrow introducer?
316+
///
317+
/// TODO: This needs a better name.
318+
bool areAnyUserResultsBorrowIntroducers() const {
319+
// TODO: Can we derive this by running a borrow introducer check ourselves?
320+
switch (kind) {
321+
case BorrowScopeOperandKind::BeginBorrow:
322+
case BorrowScopeOperandKind::Branch:
323+
return true;
324+
case BorrowScopeOperandKind::BeginApply:
325+
return false;
326+
}
327+
llvm_unreachable("Covered switch isn't covered?!");
328+
}
329+
330+
/// Visit all of the results of the operand's user instruction that are
331+
/// consuming uses.
332+
void visitUserResultConsumingUses(function_ref<void(Operand *)> visitor);
333+
334+
/// Visit all of the "results" of the user of this operand that are borrow
335+
/// scope introducers for the specific scope that this borrow scope operand
336+
/// summarizes.
337+
void visitBorrowIntroducingUserResults(
338+
function_ref<void(BorrowScopeIntroducingValue)> visitor);
339+
340+
/// Passes to visitor all of the consuming uses of this use's using
341+
/// instruction.
342+
///
343+
/// This enables one to walk the def-use chain of guaranteed phis for a single
344+
/// guaranteed scope by using a worklist and checking if any of the operands
345+
/// are BorrowScopeOperands.
346+
void visitConsumingUsesOfBorrowIntroducingUserResults(
347+
function_ref<void(Operand *)> visitor);
348+
273349
void print(llvm::raw_ostream &os) const;
274350
SWIFT_DEBUG_DUMP { print(llvm::dbgs()); }
275351

@@ -287,10 +363,11 @@ struct BorrowScopeIntroducingValueKind {
287363
using UnderlyingKindTy = std::underlying_type<ValueKind>::type;
288364

289365
/// Enum we use for exhaustive pattern matching over borrow scope introducers.
290-
enum Kind : UnderlyingKindTy {
291-
LoadBorrow = UnderlyingKindTy(ValueKind::LoadBorrowInst),
292-
BeginBorrow = UnderlyingKindTy(ValueKind::BeginBorrowInst),
293-
SILFunctionArgument = UnderlyingKindTy(ValueKind::SILFunctionArgument),
366+
enum Kind {
367+
LoadBorrow,
368+
BeginBorrow,
369+
SILFunctionArgument,
370+
Phi,
294371
};
295372

296373
static Optional<BorrowScopeIntroducingValueKind> get(ValueKind kind) {
@@ -303,6 +380,8 @@ struct BorrowScopeIntroducingValueKind {
303380
return BorrowScopeIntroducingValueKind(BeginBorrow);
304381
case ValueKind::SILFunctionArgument:
305382
return BorrowScopeIntroducingValueKind(SILFunctionArgument);
383+
case ValueKind::SILPhiArgument:
384+
return BorrowScopeIntroducingValueKind(Phi);
306385
}
307386
}
308387

@@ -323,6 +402,7 @@ struct BorrowScopeIntroducingValueKind {
323402
switch (value) {
324403
case BorrowScopeIntroducingValueKind::BeginBorrow:
325404
case BorrowScopeIntroducingValueKind::LoadBorrow:
405+
case BorrowScopeIntroducingValueKind::Phi:
326406
return true;
327407
case BorrowScopeIntroducingValueKind::SILFunctionArgument:
328408
return false;
@@ -365,9 +445,28 @@ struct BorrowScopeIntroducingValue {
365445
: kind(BorrowScopeIntroducingValueKind::SILFunctionArgument), value(arg) {
366446
assert(arg->getOwnershipKind() == ValueOwnershipKind::Guaranteed);
367447
}
448+
BorrowScopeIntroducingValue(SILPhiArgument *arg)
449+
: kind(BorrowScopeIntroducingValueKind::Phi), value(arg) {
450+
assert(llvm::all_of(arg->getParent()->getPredecessorBlocks(),
451+
[](SILBasicBlock *block) {
452+
return isa<BranchInst>(block->getTerminator());
453+
}) &&
454+
"Phi argument incoming values must come from branch insts!");
455+
assert(arg->isPhiArgument() && "Can only accept a true phi argument!");
456+
assert(arg->getOwnershipKind() == ValueOwnershipKind::Guaranteed);
457+
}
368458

369459
BorrowScopeIntroducingValue(SILValue v)
370460
: kind(*BorrowScopeIntroducingValueKind::get(v->getKind())), value(v) {
461+
// Validate that if we have a phi argument that all our predecessors have
462+
// branches as terminators.
463+
assert(!isa<SILPhiArgument>(v) ||
464+
(llvm::all_of(v->getParentBlock()->getPredecessorBlocks(),
465+
[](SILBasicBlock *block) {
466+
return isa<BranchInst>(block->getTerminator());
467+
}) &&
468+
"Phi argument incoming values must come from branch insts!"));
469+
371470
assert(v.getOwnershipKind() == ValueOwnershipKind::Guaranteed);
372471
}
373472

@@ -376,6 +475,15 @@ struct BorrowScopeIntroducingValue {
376475
auto kind = BorrowScopeIntroducingValueKind::get(value->getKind());
377476
if (!kind || value.getOwnershipKind() != ValueOwnershipKind::Guaranteed)
378477
return None;
478+
// If kind is phi and we were not passed something with all branch
479+
// predecessors, return None.
480+
if ((*kind) == BorrowScopeIntroducingValueKind::Phi &&
481+
llvm::any_of(value->getParentBlock()->getPredecessorBlocks(),
482+
[](SILBasicBlock *block) {
483+
return !isa<BranchInst>(block->getTerminator());
484+
}))
485+
return None;
486+
// Otherwise, create our value directly.
379487
return BorrowScopeIntroducingValue(*kind, value);
380488
}
381489

@@ -414,6 +522,11 @@ struct BorrowScopeIntroducingValue {
414522
SmallPtrSetImpl<SILBasicBlock *> &visitedBlocks,
415523
DeadEndBlocks &deadEndBlocks) const;
416524

525+
/// Given a local borrow scope introducer, visit all non-forwarding consuming
526+
/// users. This means that this looks through guaranteed block arguments.
527+
bool visitLocalScopeTransitiveEndingUses(
528+
function_ref<void(Operand *)> visitor) const;
529+
417530
void print(llvm::raw_ostream &os) const;
418531
SWIFT_DEBUG_DUMP { print(llvm::dbgs()); }
419532

lib/SIL/OperandOwnership.cpp

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -415,8 +415,19 @@ OperandOwnershipKindClassifier::checkTerminatorArgumentMatchesDestBB(
415415

416416
OperandOwnershipKindMap
417417
OperandOwnershipKindClassifier::visitBranchInst(BranchInst *bi) {
418-
return checkTerminatorArgumentMatchesDestBB(bi->getDestBB(),
419-
getOperandIndex());
418+
ValueOwnershipKind destBlockArgOwnershipKind =
419+
bi->getDestBB()->getArgument(getOperandIndex())->getOwnershipKind();
420+
421+
// If we have a guaranteed parameter, treat this as consuming.
422+
if (destBlockArgOwnershipKind == ValueOwnershipKind::Guaranteed) {
423+
return Map::compatibilityMap(destBlockArgOwnershipKind,
424+
UseLifetimeConstraint::MustBeInvalidated);
425+
}
426+
427+
// Otherwise, defer to defaults.
428+
auto lifetimeConstraint =
429+
destBlockArgOwnershipKind.getForwardingLifetimeConstraint();
430+
return Map::compatibilityMap(destBlockArgOwnershipKind, lifetimeConstraint);
420431
}
421432

422433
OperandOwnershipKindMap

0 commit comments

Comments
 (0)