diff --git a/include/swift/SIL/OSSALifetimeCompletion.h b/include/swift/SIL/OSSALifetimeCompletion.h index 9098fdd34b29b..cc33d41be545c 100644 --- a/include/swift/SIL/OSSALifetimeCompletion.h +++ b/include/swift/SIL/OSSALifetimeCompletion.h @@ -87,7 +87,7 @@ class OSSALifetimeCompletion { static void visitUnreachableLifetimeEnds( SILValue value, const SSAPrunedLiveness &liveness, - llvm::function_ref visit); + llvm::function_ref visit); protected: bool analyzeAndUpdateLifetime(SILValue value, bool forceBoundaryCompletion); diff --git a/include/swift/SIL/SILBasicBlock.h b/include/swift/SIL/SILBasicBlock.h index 8fb8f083ab42a..b16309ea484cb 100644 --- a/include/swift/SIL/SILBasicBlock.h +++ b/include/swift/SIL/SILBasicBlock.h @@ -518,13 +518,13 @@ public SwiftObjectHeader { #ifndef NDEBUG /// Print the ID of the block, bbN. - void dumpID() const; + void dumpID(bool newline = true) const; /// Print the ID of the block with \p OS, bbN. - void printID(llvm::raw_ostream &OS) const; + void printID(llvm::raw_ostream &OS, bool newline = true) const; /// Print the ID of the block with \p Ctx, bbN. - void printID(SILPrintContext &Ctx) const; + void printID(SILPrintContext &Ctx, bool newline = true) const; #endif /// getSublistAccess() - returns pointer to member of instruction list diff --git a/include/swift/SILOptimizer/Utils/CanonicalizeOSSALifetime.h b/include/swift/SILOptimizer/Utils/CanonicalizeOSSALifetime.h index 2563e4829354f..be89565244781 100644 --- a/include/swift/SILOptimizer/Utils/CanonicalizeOSSALifetime.h +++ b/include/swift/SILOptimizer/Utils/CanonicalizeOSSALifetime.h @@ -136,6 +136,10 @@ class CanonicalOSSAConsumeInfo final { /// Map blocks on the lifetime boundary to the last consuming instruction. llvm::SmallDenseMap finalBlockConsumes; + /// The instructions on the availability boundary of the dead-end region where + /// this value is not consumed. + SmallPtrSet unreachableLifetimeEnds; + public: void clear() { finalBlockConsumes.clear(); } @@ -161,6 +165,14 @@ class CanonicalOSSAConsumeInfo final { return false; } + void recordUnreachableLifetimeEnd(SILInstruction *inst) { + unreachableLifetimeEnds.insert(inst); + } + + bool isUnreachableLifetimeEnd(SILInstruction *inst) { + return unreachableLifetimeEnds.contains(inst); + } + CanonicalOSSAConsumeInfo() {} CanonicalOSSAConsumeInfo(CanonicalOSSAConsumeInfo const &) = delete; CanonicalOSSAConsumeInfo & @@ -235,9 +247,12 @@ class CanonicalizeOSSALifetime final { SILValue currentDef; /// Original points in the CFG where the current value's lifetime is consumed - /// or destroyed. For guaranteed values it remains empty. A backward walk from - /// these blocks must discover all uses on paths that lead to a return or - /// throw. + /// or destroyed. Each block either contains a consuming instruction (e.g. + /// `destroy_value`) or is on the availability boundary of the value in a + /// dead-end region (e.g. `unreachable`). + /// + /// For guaranteed values it remains empty. A backward walk from these blocks + /// must discover all uses on paths that lead to a return or throw. /// /// These blocks are not necessarily in the pruned live blocks since /// pruned liveness does not consider destroy_values. @@ -406,9 +421,18 @@ class CanonicalizeOSSALifetime final { void recordDebugValue(DebugValueInst *dvi) { debugValues.insert(dvi); } void recordConsumingUse(Operand *use) { recordConsumingUser(use->getUser()); } + /// Record that the value is consumed at `user`. + /// + /// Either `user` is a consuming use (e.g. `destroy_value`) or it is the + /// terminator of a block on the availability boundary of the value in a + /// dead-end region (e.g. `unreachable`). void recordConsumingUser(SILInstruction *user) { consumingBlocks.insert(user->getParent()); } + void recordUnreachableLifetimeEnd(SILInstruction *user) { + recordConsumingUser(user); + consumes.recordUnreachableLifetimeEnd(user); + } bool computeCanonicalLiveness(); bool endsAccessOverlappingPrunedBoundary(SILInstruction *inst); diff --git a/lib/SIL/IR/SILPrinter.cpp b/lib/SIL/IR/SILPrinter.cpp index 578b6bfb85f75..6636478e42673 100644 --- a/lib/SIL/IR/SILPrinter.cpp +++ b/lib/SIL/IR/SILPrinter.cpp @@ -853,8 +853,11 @@ class SILPrinter : public SILInstructionVisitor { } #ifndef NDEBUG - void printID(const SILBasicBlock *BB) { - *this << Ctx.getID(BB) << "\n"; + void printID(const SILBasicBlock *BB, bool newline) { + *this << Ctx.getID(BB); + if (newline) { + *this << "\n"; + } } #endif @@ -3108,17 +3111,17 @@ void SILBasicBlock::print(SILPrintContext &Ctx) const { } #ifndef NDEBUG -void SILBasicBlock::dumpID() const { - printID(llvm::errs()); +void SILBasicBlock::dumpID(bool newline) const { + printID(llvm::errs(), newline); } -void SILBasicBlock::printID(llvm::raw_ostream &OS) const { +void SILBasicBlock::printID(llvm::raw_ostream &OS, bool newline) const { SILPrintContext Ctx(OS); - printID(Ctx); + printID(Ctx, newline); } -void SILBasicBlock::printID(SILPrintContext &Ctx) const { - SILPrinter(Ctx).printID(this); +void SILBasicBlock::printID(SILPrintContext &Ctx, bool newline) const { + SILPrinter(Ctx).printID(this, newline); } #endif diff --git a/lib/SIL/Utils/OSSALifetimeCompletion.cpp b/lib/SIL/Utils/OSSALifetimeCompletion.cpp index 94df9612284ab..1eafcca87caa2 100644 --- a/lib/SIL/Utils/OSSALifetimeCompletion.cpp +++ b/lib/SIL/Utils/OSSALifetimeCompletion.cpp @@ -51,8 +51,10 @@ #include "swift/SIL/OSSALifetimeCompletion.h" #include "swift/SIL/SILBuilder.h" +#include "swift/SIL/SILFunction.h" #include "swift/SIL/SILInstruction.h" #include "swift/SIL/Test.h" +#include "llvm/ADT/STLExtras.h" using namespace swift; @@ -99,40 +101,217 @@ static bool endLifetimeAtBoundary(SILValue value, return changed; } -void OSSALifetimeCompletion::visitUnreachableLifetimeEnds( - SILValue value, const SSAPrunedLiveness &liveness, - llvm::function_ref visit) { +namespace { +/// Implements OSSALifetimeCompletion::visitUnreachableLifetimeEnds. Finds +/// positions as near as possible to unreachables at which `value`'s lifetime +/// is available. +/// +/// Finding these positions is a three step process: +/// 1) computeRegion: Forward CFG walk from non-lifetime-ending boundary to find +/// the dead-end region in which the value might be available. +/// 2) propagateAvailability: Forward iterative dataflow within the region to +/// determine which blocks the value is available in. +/// 3) visitAvailabilityBoundary: Visits the final blocks in the region where +/// the value is available--these are the blocks +/// without successors or with at least one +/// unavailable successor. +class VisitUnreachableLifetimeEnds { + /// The value whose dead-end block lifetime ends are to be visited. + SILValue value; + + /// The non-lifetime-ending boundary of `value`. + BasicBlockSet starts; + /// The region between (inclusive) the `starts` and the unreachable blocks. + BasicBlockSetVector region; + +public: + VisitUnreachableLifetimeEnds(SILValue value) + : value(value), starts(value->getFunction()), + region(value->getFunction()) {} + + /// Region discovery. + /// + /// Forward CFG walk from non-lifetime-ending boundary to unreachable + /// instructions. + void computeRegion(const SSAPrunedLiveness &liveness); + + struct Result; + + /// Iterative dataflow to determine availability for each block in `region`. + void propagateAvailablity(Result &result); + + /// Visit the terminators of blocks on the boundary of availability. + void + visitAvailabilityBoundary(Result const &result, + llvm::function_ref visit); + + struct State { + enum class Value : uint8_t { + Unavailable = 0, + Available, + Unknown, + }; + Value value; + + State(Value value) : value(value){}; + operator Value() const { return value; } + State meet(State const other) const { + return *this < other ? *this : other; + } + + static State Unavailable() { return {Value::Unavailable}; } + static State Available() { return {Value::Available}; } + static State Unknown() { return {Value::Unknown}; } + }; + + struct Result { + BasicBlockBitfield states; + + Result(SILFunction *function) : states(function, 2) {} + + State getState(SILBasicBlock *block) const { + return {(State::Value)states.get(block)}; + } + + void setState(SILBasicBlock *block, State newState) { + states.set(block, (unsigned)newState.value); + } + + /// Propagate predecessors' state into `block`. + /// + /// states[block] ∧= state[predecessor_1] ∧ ... ∧ state[predecessor_n] + bool updateState(SILBasicBlock *block) { + auto oldState = getState(block); + auto state = oldState; + for (auto *predecessor : block->getPredecessorBlocks()) { + state = state.meet(getState(predecessor)); + } + setState(block, state); + return state != oldState; + } + }; +}; + +void VisitUnreachableLifetimeEnds::computeRegion( + const SSAPrunedLiveness &liveness) { + // Find the non-lifetime-ending boundary of `value`. PrunedLivenessBoundary boundary; liveness.computeBoundary(boundary); - BasicBlockWorklist deadEndBlocks(value->getFunction()); for (SILInstruction *lastUser : boundary.lastUsers) { if (liveness.isInterestingUser(lastUser) != PrunedLiveness::LifetimeEndingUse) { - deadEndBlocks.push(lastUser->getParent()); + region.insert(lastUser->getParent()); + starts.insert(lastUser->getParent()); } } for (SILBasicBlock *edge : boundary.boundaryEdges) { - deadEndBlocks.push(edge); + region.insert(edge); + starts.insert(edge); } for (SILNode *deadDef : boundary.deadDefs) { - deadEndBlocks.push(deadDef->getParentBlock()); + region.insert(deadDef->getParentBlock()); + starts.insert(deadDef->getParentBlock()); + } + + // Forward walk to find the region in which `value` might be available. + BasicBlockWorklist regionWorklist(value->getFunction()); + // Start the forward walk from the non-lifetime-ending boundary. + for (auto *start : region) { + regionWorklist.push(start); } - // Forward CFG walk from the non-lifetime-ending boundary to the unreachable - // instructions. - while (auto *block = deadEndBlocks.pop()) { + while (auto *block = regionWorklist.pop()) { if (block->succ_empty()) { // This assert will fail unless there are already lifetime-ending // instruction on all paths to normal function exits. - auto *unreachable = cast(block->getTerminator()); - visit(unreachable); + assert(isa(block->getTerminator())); } for (auto *successor : block->getSuccessorBlocks()) { - deadEndBlocks.pushIfNotVisited(successor); + regionWorklist.pushIfNotVisited(successor); + region.insert(successor); } } } +void VisitUnreachableLifetimeEnds::propagateAvailablity(Result &result) { + // Initialize per-block state. + // - all blocks outside of the region are ::Unavailable (automatically + // initialized) + // - non-initial in-region blocks are Unknown + // - start blocks are ::Available + for (auto *block : region) { + if (starts.contains(block)) + result.setState(block, State::Available()); + else + result.setState(block, State::Unknown()); + } + + BasicBlockWorklist worklist(value->getFunction()); + + // Initialize worklist with all participating blocks. + // + // Only perform dataflow in the non-initial region. Every initial block is + // by definition ::Available. + for (auto *block : region) { + if (starts.contains(block)) + continue; + worklist.push(block); + } + + // Iterate over blocks which are successors of blocks whose state changed. + while (auto *block = worklist.popAndForget()) { + // Only propagate availability in non-initial, in-region blocks. + if (!region.contains(block) || starts.contains(block)) + continue; + auto changed = result.updateState(block); + if (!changed) { + continue; + } + // The state has changed. Propagate the new state into successors. + for (auto *successor : block->getSuccessorBlocks()) { + worklist.pushIfNotVisited(successor); + } + } +} + +void VisitUnreachableLifetimeEnds::visitAvailabilityBoundary( + Result const &result, llvm::function_ref visit) { + for (auto *block : region) { + auto available = result.getState(block) == State::Available(); + if (!available) { + continue; + } + auto hasUnreachableSuccessor = [&]() { + // Use a lambda to avoid checking if possible. + return llvm::any_of(block->getSuccessorBlocks(), [&result](auto *block) { + return result.getState(block) == State::Unavailable(); + }); + }; + if (!block->succ_empty() && !hasUnreachableSuccessor()) { + continue; + } + assert(hasUnreachableSuccessor() || + isa(block->getTerminator())); + visit(block->getTerminator()); + } +} +} // end anonymous namespace + +void OSSALifetimeCompletion::visitUnreachableLifetimeEnds( + SILValue value, const SSAPrunedLiveness &liveness, + llvm::function_ref visit) { + + VisitUnreachableLifetimeEnds visitor(value); + + visitor.computeRegion(liveness); + + VisitUnreachableLifetimeEnds::Result result(value->getFunction()); + + visitor.propagateAvailablity(result); + + visitor.visitAvailabilityBoundary(result, visit); +} + static bool endLifetimeAtUnreachableBlocks(SILValue value, const SSAPrunedLiveness &liveness) { bool changed = false; @@ -274,4 +453,3 @@ bool UnreachableLifetimeCompletion::completeLifetimes() { } return changed; } - diff --git a/lib/SILOptimizer/Utils/CanonicalizeOSSALifetime.cpp b/lib/SILOptimizer/Utils/CanonicalizeOSSALifetime.cpp index 2f6f5f31fdb47..531f8dd0d9c2c 100644 --- a/lib/SILOptimizer/Utils/CanonicalizeOSSALifetime.cpp +++ b/lib/SILOptimizer/Utils/CanonicalizeOSSALifetime.cpp @@ -261,7 +261,7 @@ void CanonicalizeOSSALifetime::extendLivenessToDeinitBarriers() { OSSALifetimeCompletion::visitUnreachableLifetimeEnds( getCurrentDef(), completeLiveness, [&](auto *unreachable) { - recordConsumingUser(unreachable); + recordUnreachableLifetimeEnd(unreachable); if (auto *previous = unreachable->getPreviousInstruction()) { if (liveness->isInterestingUser(previous) == PrunedLiveness::IsInterestingUser::NonUser) { @@ -923,8 +923,11 @@ static void insertDestroyBeforeInstruction(SILInstruction *nextInstruction, InstModCallbacks &callbacks) { // OSSALifetimeCompletion: This conditional clause can be deleted with // complete lifetimes. - if (isa(nextInstruction)) { - // Don't create a destroy_value if the next instruction is an unreachable. + if (consumes.isUnreachableLifetimeEnd(nextInstruction)) { + // Don't create a destroy_value if the next instruction is an unreachable + // (or a terminator on the availability boundary of the dead-end region + // starting from the non-lifetime-ending boundary of `currentDef`). + // // If there was a destroy here already, it would be reused. Avoids // creating an explicit destroy of a value which might have an unclosed // borrow scope. Doing so would result in diff --git a/test/SILOptimizer/mem2reg_lifetime.sil b/test/SILOptimizer/mem2reg_lifetime.sil index de47163b0f08e..5a905824290db 100644 --- a/test/SILOptimizer/mem2reg_lifetime.sil +++ b/test/SILOptimizer/mem2reg_lifetime.sil @@ -1858,8 +1858,7 @@ right: // CHECK: {{bb[^,]+}}([[INSTANCE:%[^,]+]] : @owned $C): // CHECK: cond_br undef, [[BB1:bb[0-9]+]], [[BB4:bb[0-9]+]] // CHECK: [[BB1]]: -// CHECK: [[COPY:%[^,]+]] = copy_value [[INSTANCE]] -// CHECK: [[LIFETIME_OWNED:%[^,]+]] = move_value [lexical] [[COPY]] +// CHECK: [[LIFETIME_OWNED:%[^,]+]] = move_value [lexical] [[INSTANCE]] // CHECK: cond_br undef, [[BB2:bb[0-9]+]], [[BB3:bb[0-9]+]] // CHECK: [[BB2]]: // CHECK: destroy_value [[LIFETIME_OWNED]] diff --git a/test/SILOptimizer/ossa_lifetime_completion.sil b/test/SILOptimizer/ossa_lifetime_completion.sil index 744965f1d923f..4a16670859391 100644 --- a/test/SILOptimizer/ossa_lifetime_completion.sil +++ b/test/SILOptimizer/ossa_lifetime_completion.sil @@ -212,3 +212,189 @@ bb0(%0 : @guaranteed $C): return %12 : $() } +// Insert destroy on availability boundary of `value` within the region after +// the non-lifetime-ending boundary of `value`. Namely, in to_die_11. +// CHECK-LABEL: sil [ossa] @availability_boundary_1 : {{.*}} { +// CHECK: [[VALUE:%[^,]+]] = move_value [lexical] +// CHECK: br [[CONDITION_1:bb[0-9]+]] +// CHECK: [[CONDITION_1]]: +// CHECK: cond_br undef, [[CONDITION_2:bb[0-9]+]], [[TO_DIE_1:bb[0-9]+]] +// CHECK: [[CONDITION_2]]: +// CHECK: destroy_value [[VALUE]] +// CHECK: cond_br undef, [[EXIT:bb[0-9]+]], [[TO_DIE_2:bb[0-9]+]] +// CHECK: [[TO_DIE_1]]: +// CHECK: br [[TO_DIE_11:bb[0-9]+]] +// CHECK: [[TO_DIE_11]]: +// CHECK: destroy_value [[VALUE]] +// CHECK: br [[DIE:bb[0-9]+]] +// CHECK: [[TO_DIE_2]]: +// CHECK: br [[DIE]] +// CHECK: [[DIE]]: +// CHECK: unreachable +// CHECK: [[EXIT]]: +// CHECK-LABEL: } // end sil function 'availability_boundary_1' +sil [ossa] @availability_boundary_1 : $@convention(thin) () -> () { +entry: + %value = apply undef() : $@convention(thin) () -> @owned C + %lexical = move_value [lexical] %value : $C // required (for lexicality) + debug_value [trace] %lexical : $C + test_specification "ossa-lifetime-completion @trace[0]" + br condition_1 + +condition_1: + cond_br undef, condition_2, to_die_1 + +condition_2: + destroy_value %lexical : $C + cond_br undef, exit, to_die_2 + +to_die_1: + br to_die_11 + +to_die_11: + // End lifetime here. + br die + +to_die_2: + br die + +die: + unreachable + +exit: + %retval = tuple () + return %retval : $() +} + +// CHECK-LABEL: sil [ossa] @availability_boundary_2_after_loop : {{.*}} { +// CHECK: [[REGISTER_1:%[^,]+]] = move_value [lexical] +// CHECK: br [[CONDITION_1:bb[0-9]+]] +// CHECK: [[CONDITION_1]]: +// CHECK: cond_br undef, [[CONDITION_2:bb[0-9]+]], [[PREHEADER:bb[0-9]+]] +// CHECK: [[CONDITION_2]]: +// CHECK: destroy_value [[REGISTER_1]] +// CHECK: cond_br undef, [[EXIT:bb[0-9]+]], [[TO_DIE_2:bb[0-9]+]] +// CHECK: [[PREHEADER]]: +// CHECK: br [[HEADER:bb[0-9]+]] +// CHECK: [[HEADER]]: +// CHECK: br [[LATCH:bb[0-9]+]] +// CHECK: [[LATCH]]: +// CHECK: cond_br undef, [[BACKEDGE:bb[0-9]+]], [[TO_DIE_1:bb[0-9]+]] +// CHECK: [[BACKEDGE]]: +// CHECK: br [[HEADER]] +// CHECK: [[TO_DIE_1]]: +// CHECK: destroy_value [[REGISTER_1]] +// CHECK: br [[DIE:bb[0-9]+]] +// CHECK: [[TO_DIE_2]]: +// CHECK: br [[DIE]] +// CHECK: [[DIE]]: +// CHECK: unreachable +// CHECK: [[EXIT]]: +// CHECK-LABEL: } // end sil function 'availability_boundary_2_after_loop' +sil [ossa] @availability_boundary_2_after_loop : $@convention(thin) () -> () { +entry: + %value = apply undef() : $@convention(thin) () -> @owned C + %lexical = move_value [lexical] %value : $C // required (for lexicality) + debug_value [trace] %lexical : $C + test_specification "ossa-lifetime-completion @trace[0]" + br condition_1 + +condition_1: + cond_br undef, condition_2, preheader + +condition_2: + destroy_value %lexical : $C + cond_br undef, exit, to_die_2 + +preheader: + br header + +header: + br latch + +latch: + cond_br undef, backedge, to_die_1 + +backedge: + br header + +to_die_1: + br die + +to_die_2: + br die + +die: + unreachable + +exit: + %retval = tuple () + return %retval : $() +} + +// CHECK-LABEL: sil [ossa] @availability_boundary_3_after_loop : {{.*}} { +// CHECK: [[VALUE:%[^,]+]] = move_value [lexical] +// CHECK: br [[CONDITION_1:bb[0-9]+]] +// CHECK: [[CONDITION_1]]: +// CHECK: cond_br undef, [[CONDITION_2:bb[0-9]+]], [[PREHEADER:bb[0-9]+]] +// CHECK: [[CONDITION_2]]: +// CHECK: destroy_value [[VALUE]] +// CHECK: cond_br undef, [[EXIT:bb[0-9]+]], [[TO_BODY:bb[0-9]+]] +// CHECK: [[TO_BODY]]: +// CHECK: br [[BODY:bb[0-9]+]] +// CHECK: [[PREHEADER]]: +// CHECK: destroy_value [[VALUE]] +// CHECK: br [[HEADER:bb[0-9]+]] +// CHECK: [[HEADER]]: +// CHECK: br [[BODY]] +// CHECK: [[BODY]]: +// CHECK: br [[LATCH:bb[0-9]+]] +// CHECK: [[LATCH]]: +// CHECK: cond_br undef, [[BACKEDGE:bb[0-9]+]], [[DIE:bb[0-9]+]] +// CHECK: [[BACKEDGE]]: +// CHECK: br [[HEADER]] +// CHECK: [[DIE]]: +// CHECK: unreachable +// CHECK: [[EXIT]]: +// CHECK-LABEL: } // end sil function 'availability_boundary_3_after_loop' +sil [ossa] @availability_boundary_3_after_loop : $@convention(thin) () -> () { +entry: + %value = apply undef() : $@convention(thin) () -> @owned C + %lexical = move_value [lexical] %value : $C // required (for lexicality) + debug_value [trace] %lexical : $C + test_specification "ossa-lifetime-completion @trace[0]" + br condition_1 + +condition_1: + cond_br undef, condition_2, preheader + +condition_2: + destroy_value %lexical : $C + cond_br undef, exit, to_body2 + +to_body2: + br body + +preheader: + // End lifetime here. + br header + +header: + br body + +body: + br latch + +latch: + cond_br undef, backedge, die + +backedge: + br header + +die: + unreachable + +exit: + %retval = tuple () + return %retval : $() +}