From bdabc2145a1cf712fc505d697aa206bbbbd3302c Mon Sep 17 00:00:00 2001 From: Andrew Trick Date: Thu, 14 Mar 2024 20:48:44 -0700 Subject: [PATCH] Add computeKnownLiveness utility To fix LifetimeDependenceScopeFixup in the presense of pointer escapes. --- .../Optimizer/Utilities/AddressUtils.swift | 4 +- .../Optimizer/Utilities/BorrowUtils.swift | 9 +- .../Utilities/OwnershipLiveness.swift | 175 ++++++++++++------ test/SILOptimizer/ownership_liveness_unit.sil | 1 - 4 files changed, 123 insertions(+), 66 deletions(-) diff --git a/SwiftCompilerSources/Sources/Optimizer/Utilities/AddressUtils.swift b/SwiftCompilerSources/Sources/Optimizer/Utilities/AddressUtils.swift index 8841ea42adf..60b3566ca9b 100644 --- a/SwiftCompilerSources/Sources/Optimizer/Utilities/AddressUtils.swift +++ b/SwiftCompilerSources/Sources/Optimizer/Utilities/AddressUtils.swift @@ -502,7 +502,7 @@ extension AddressOwnershipLiveRange { /// /// For address values, use AccessBase.computeOwnershipRange. /// - /// FIXME: This should use computeLinearLiveness rather than computeInteriorLiveness as soon as lifetime completion + /// FIXME: This should use computeLinearLiveness rather than computeKnownLiveness as soon as lifetime completion /// runs immediately after SILGen. private static func computeValueLiveRange(of value: Value, _ context: FunctionPassContext) -> AddressOwnershipLiveRange? { @@ -511,7 +511,7 @@ extension AddressOwnershipLiveRange { // This is unexpected for a value with derived addresses. return nil case .owned: - return .owned(value, computeInteriorLiveness(for: value, context)) + return .owned(value, computeKnownLiveness(for: value, context)) case .guaranteed: return .borrow(computeBorrowLiveRange(for: value, context)) } diff --git a/SwiftCompilerSources/Sources/Optimizer/Utilities/BorrowUtils.swift b/SwiftCompilerSources/Sources/Optimizer/Utilities/BorrowUtils.swift index 086cc1cad72..4d60c6212e7 100644 --- a/SwiftCompilerSources/Sources/Optimizer/Utilities/BorrowUtils.swift +++ b/SwiftCompilerSources/Sources/Optimizer/Utilities/BorrowUtils.swift @@ -405,8 +405,9 @@ func gatherBorrowIntroducers(for value: Value, } /// Compute the live range for the borrow scopes of a guaranteed value. This returns a separate instruction range for -/// each of the value's borrow introducers. Unioning those ranges would be incorrect. We typically want their -/// intersection. +/// each of the value's borrow introducers. +/// +/// TODO: This should return a single multiply-defined instruction range. func computeBorrowLiveRange(for value: Value, _ context: FunctionPassContext) -> SingleInlineArray<(BeginBorrowValue, InstructionRange)> { assert(value.ownership == .guaranteed) @@ -418,10 +419,10 @@ func computeBorrowLiveRange(for value: Value, _ context: FunctionPassContext) // If introducers is empty, then the dependence is on a trivial value, so // there is no ownership range. while let beginBorrow = introducers.pop() { - /// FIXME: Remove calls to computeInteriorLiveness as soon as lifetime completion runs immediately after + /// FIXME: Remove calls to computeKnownLiveness() as soon as lifetime completion runs immediately after /// SILGen. Instead, this should compute linear liveness for borrowed value by switching over BeginBorrowValue, just /// like LifetimeDependenc.Scope.computeRange(). - ranges.push((beginBorrow, computeInteriorLiveness(for: beginBorrow.value, context))) + ranges.push((beginBorrow, computeKnownLiveness(for: beginBorrow.value, context))) } return ranges } diff --git a/SwiftCompilerSources/Sources/Optimizer/Utilities/OwnershipLiveness.swift b/SwiftCompilerSources/Sources/Optimizer/Utilities/OwnershipLiveness.swift index 6f5192b1f0c..72c72c7c64a 100644 --- a/SwiftCompilerSources/Sources/Optimizer/Utilities/OwnershipLiveness.swift +++ b/SwiftCompilerSources/Sources/Optimizer/Utilities/OwnershipLiveness.swift @@ -23,6 +23,14 @@ import SIL +private let verbose = false + +private func log(_ message: @autoclosure () -> String) { + if verbose { + print("### \(message())") + } +} + /// Compute liveness and return a range, which the caller must deinitialize. /// /// `definingValue` must introduce an OSSA lifetime. It may be either @@ -66,15 +74,13 @@ typealias InnerScopeHandler = (Value) -> WalkResult /// Compute liveness and return a range, which the caller must deinitialize. /// -/// An OSSA lifetime begins with a single "defining" value, which must -/// be owned, or must begin a borrow scope. A complete OSSA lifetime -/// has a linear lifetime, meaning that it has a lifetime-ending use -/// on all paths. Interior liveness computes liveness without assuming -/// the lifetime is complete. To do this, it must find all "use -/// points" and prove that the defining value is never propagated -/// beyond those points. This is used to initially complete OSSA -/// lifetimes and fix them after transformations that's don't preserve -/// OSSA. +/// An OSSA lifetime begins with a single "defining" value, which must be owned, or must begin a borrow scope. A +/// complete OSSA lifetime has a linear lifetime, meaning that it has a lifetime-ending use on all paths. Interior +/// liveness computes liveness without assuming the lifetime is complete. To do this, it must find all "use points" and +/// prove that the defining value is never propagated beyond those points. This is used to initially complete OSSA +/// lifetimes and fix them after transformations that's don't preserve OSSA. +/// +/// The caller must check that `definingValue` has no pointer escape before calling this. /// /// Invariants: /// @@ -83,39 +89,100 @@ typealias InnerScopeHandler = (Value) -> WalkResult /// - Liveness does not extend beyond lifetime-ending operations /// (a.k.a. affine lifetimes). /// -/// - All inner scopes are complete. (Use `innerScopeHandler` to -/// complete them or bail-out). -func computeInteriorLiveness(for definingValue: Value, - _ context: FunctionPassContext, - innerScopeHandler: InnerScopeHandler? = nil) -> InstructionRange { - - assert(definingValue.ownership == .owned - || BeginBorrowValue(definingValue) != nil, - "value must define an OSSA lifetime") - - var range = InstructionRange(for: definingValue, context) - - var visitor = InteriorUseWalker(definingValue: definingValue, context) { - range.insert($0.instruction) - return .continueWalk - } - defer { visitor.deinitialize() } - visitor.innerScopeHandler = innerScopeHandler - let success = visitor.visitUses() - switch visitor.pointerStatus { - case .nonEscaping: +/// - All inner scopes are complete. (Use `innerScopeHandler` to complete them or bail-out). +func computeInteriorLiveness(for definingValue: Value, _ context: FunctionPassContext, + innerScopeHandler: InnerScopeHandler? = nil) -> InstructionRange { + let result = InteriorLivenessResult.compute(for: definingValue, ignoreEscape: false, context) + switch result.pointerStatus { + case .nonescaping: break - case let .escaping(operand): + case let .escaping(operands): fatalError(""" check findPointerEscape() before computing interior liveness. - Pointer escape: \(operand.instruction) + Pointer escape: \(operands[0].instruction) """) case let .unknown(operand): fatalError("Unrecognized SIL address user \(operand.instruction)") } - assert(success == .continueWalk, "our visitor never fails") - assert(visitor.unenclosedPhis.isEmpty, "missing adjacent phis") - return range + return result.range +} + +/// Compute known liveness and return a range, which the caller must deinitialize. +/// +/// This computes a minimal liveness, ignoring pointer escaping uses. +func computeKnownLiveness(for definingValue: Value, _ context: FunctionPassContext) -> InstructionRange { + return InteriorLivenessResult.compute(for: definingValue, ignoreEscape: true, context).range +} + +/// If any interior pointer may escape, then record the first instance here. If 'ignoseEscape' is true, this +/// immediately aborts the walk, so further instances are unavailable. +/// +/// .escaping may either be a non-address operand with +/// .pointerEscape ownership, or and address operand that escapes +/// the address (address_to_pointer). +/// +/// .unknown is an address operand whose user is unrecognized. +enum InteriorPointerStatus: CustomDebugStringConvertible { + case nonescaping + case escaping(SingleInlineArray) + case unknown(Operand) + + mutating func setEscaping(operand: Operand) { + switch self { + case .nonescaping: + self = .escaping(SingleInlineArray(element: operand)) + case let .escaping(oldOperands): + var newOperands = SingleInlineArray() + newOperands.append(contentsOf: oldOperands) + newOperands.append(operand) + self = .escaping(newOperands) + case .unknown: + break + } + } + + var debugDescription: String { + switch self { + case .nonescaping: + return "No pointer escape" + case let .escaping(operands): + return "Pointer escapes: " + operands.map({ "\($0)" }).joined(separator: "\n ") + case let .unknown(operand): + return "Unknown use: \(operand)" + } + } +} + +struct InteriorLivenessResult: CustomDebugStringConvertible { + let success: WalkResult + let range: InstructionRange + let pointerStatus: InteriorPointerStatus + + static func compute(for definingValue: Value, ignoreEscape: Bool = false, + _ context: FunctionPassContext, + innerScopeHandler: InnerScopeHandler? = nil) -> InteriorLivenessResult { + + assert(definingValue.ownership == .owned || BeginBorrowValue(definingValue) != nil, + "value must define an OSSA lifetime") + + var range = InstructionRange(for: definingValue, context) + + var visitor = InteriorUseWalker(definingValue: definingValue, ignoreEscape: ignoreEscape, context) { + range.insert($0.instruction) + return .continueWalk + } + defer { visitor.deinitialize() } + visitor.innerScopeHandler = innerScopeHandler + let success = visitor.visitUses() + assert(visitor.unenclosedPhis.isEmpty, "missing adjacent phis") + let result = InteriorLivenessResult(success: success, range: range, pointerStatus: visitor.pointerStatus) + log("Interior liveness for: \(definingValue)\n\(result)") + return result + } + + var debugDescription: String { + "\(success)\n\(range)\n\(pointerStatus)" + } } /// Classify ownership uses. This reduces operand ownership to a @@ -455,6 +522,7 @@ struct InteriorUseWalker { var context: Context { functionContext } let definingValue: Value + let ignoreEscape: Bool let useVisitor: (Operand) -> WalkResult var innerScopeHandler: InnerScopeHandler? = nil @@ -470,21 +538,7 @@ struct InteriorUseWalker { var function: Function { definingValue.parentFunction } - /// If any interior pointer may escape, then record the first instance - /// here. This immediately aborts the walk, so further instances are - /// unavailable. - /// - /// .escaping may either be a non-address operand with - /// .pointerEscape ownership, or and address operand that escapes - /// the address (address_to_pointer). - /// - /// .unknown is an address operand whose user is unrecognized. - enum InteriorPointerStatus { - case nonEscaping - case escaping(Operand) - case unknown(Operand) - } - var pointerStatus: InteriorPointerStatus = .nonEscaping + var pointerStatus: InteriorPointerStatus = .nonescaping private var visited: ValueSet @@ -492,11 +546,12 @@ struct InteriorUseWalker { visited.deinitialize() } - init(definingValue: Value, _ context: FunctionPassContext, + init(definingValue: Value, ignoreEscape: Bool, _ context: FunctionPassContext, visitor: @escaping (Operand) -> WalkResult) { assert(!definingValue.type.isAddress, "address values have no ownership") self.functionContext = context self.definingValue = definingValue + self.ignoreEscape = ignoreEscape self.useVisitor = visitor self.visited = ValueSet(context) } @@ -598,8 +653,8 @@ extension InteriorUseWalker: OwnershipUseVisitor { if useVisitor(operand) == .abortWalk { return .abortWalk } - pointerStatus = .escaping(operand) - return .abortWalk + pointerStatus.setEscaping(operand: operand) + return ignoreEscape ? .continueWalk : .abortWalk } // Call the innerScopeHandler before visiting the scope-ending uses. @@ -695,8 +750,8 @@ extension InteriorUseWalker: AddressUseVisitor { } mutating func escapingAddressUse(of operand: Operand) -> WalkResult { - pointerStatus = .escaping(operand) - return .abortWalk + pointerStatus.setEscaping(operand: operand) + return ignoreEscape ? .continueWalk : .abortWalk } mutating func unknownAddressUse(of operand: Operand) -> WalkResult { @@ -894,7 +949,7 @@ let interiorLivenessTest = FunctionTest("interior_liveness_swift") { var range = InstructionRange(for: value, context) defer { range.deinitialize() } - var visitor = InteriorUseWalker(definingValue: value, context) { + var visitor = InteriorUseWalker(definingValue: value, ignoreEscape: true, context) { range.insert($0.instruction) return .continueWalk } @@ -903,10 +958,12 @@ let interiorLivenessTest = FunctionTest("interior_liveness_swift") { let success = visitor.visitUses() switch visitor.pointerStatus { - case .nonEscaping: + case .nonescaping: break - case let .escaping(operand): - print("Pointer escape: \(operand.instruction)") + case let .escaping(operands): + for operand in operands { + print("Pointer escape: \(operand.instruction)") + } case let .unknown(operand): print("Unrecognized SIL address user \(operand.instruction)") } diff --git a/test/SILOptimizer/ownership_liveness_unit.sil b/test/SILOptimizer/ownership_liveness_unit.sil index 911cd48f4a9..03cd0f67f28 100644 --- a/test/SILOptimizer/ownership_liveness_unit.sil +++ b/test/SILOptimizer/ownership_liveness_unit.sil @@ -254,7 +254,6 @@ exit(%reborrow : @guaranteed $C, %phi : @guaranteed $D): // CHECK-LABEL: testInteriorRefElementEscape: interior_liveness_swift with: %0 // CHECK: Interior liveness: %0 = argument of bb0 : $C // CHECK-NEXT: Pointer escape: %{{.*}} = address_to_pointer %{{.*}} : $*C to $Builtin.RawPointer -// CHECK-NEXT: Incomplete liveness // CHECK-NEXT: begin: %{{.*}} = unchecked_ref_cast %0 : $C to $D // CHECK-NEXT: ends: %{{.*}} = address_to_pointer %{{.*}} : $*C to $Builtin.RawPointer // CHECK-NEXT: exits: