|
| 1 | +//===--- DestroyHoisting.swift ---------------------------------------------==// |
| 2 | +// |
| 3 | +// This source file is part of the Swift.org open source project |
| 4 | +// |
| 5 | +// Copyright (c) 2014 - 2024 Apple Inc. and the Swift project authors |
| 6 | +// Licensed under Apache License v2.0 with Runtime Library Exception |
| 7 | +// |
| 8 | +// See https://swift.org/LICENSE.txt for license information |
| 9 | +// See https://swift.org/CONTRIBUTORS.txt for the list of Swift project authors |
| 10 | +// |
| 11 | +//===----------------------------------------------------------------------===// |
| 12 | + |
| 13 | +import SIL |
| 14 | + |
| 15 | +/// Hoists `destroy_value` instructions without shrinking an object's lifetime. |
| 16 | +/// This is done if it can be proved that another copy of a value (either in an SSA value or in memory) keeps |
| 17 | +/// the referenced object(s) alive until the original position of the `destroy_value`. |
| 18 | +/// |
| 19 | +/// ``` |
| 20 | +/// %1 = copy_value %0 |
| 21 | +/// ... |
| 22 | +/// last_use_of %0 |
| 23 | +/// // other instructions |
| 24 | +/// destroy_value %0 // %1 is still alive here |
| 25 | +/// ``` |
| 26 | +/// -> |
| 27 | +/// ``` |
| 28 | +/// %1 = copy_value %0 |
| 29 | +/// ... |
| 30 | +/// last_use_of %0 |
| 31 | +/// destroy_value %0 |
| 32 | +/// // other instructions |
| 33 | +/// ``` |
| 34 | +/// |
| 35 | +/// This also works if a copy of the value is kept alive in memory: |
| 36 | +/// |
| 37 | +/// ``` |
| 38 | +/// %1 = copy_value %0 |
| 39 | +/// store %1 to [assign] %a |
| 40 | +/// ... |
| 41 | +/// last_use_of %0 |
| 42 | +/// // other instructions |
| 43 | +/// destroy_value %0 // memory location %a is not modified since the store |
| 44 | +/// ``` |
| 45 | +/// -> |
| 46 | +/// ``` |
| 47 | +/// %1 = copy_value %0 |
| 48 | +/// store %0 to [assign] %a |
| 49 | +/// ... |
| 50 | +/// last_use_of %0 |
| 51 | +/// destroy_value %0 |
| 52 | +/// // other instructions |
| 53 | +/// ``` |
| 54 | +/// |
| 55 | +/// The benefit of this optimization is that it can enable copy-propagation by moving |
| 56 | +/// destroys above deinit barries and access scopes. |
| 57 | +/// |
| 58 | +let destroyHoisting = FunctionPass(name: "destroy-hoisting") { |
| 59 | + (function: Function, context: FunctionPassContext) in |
| 60 | + |
| 61 | + if !function.hasOwnership { |
| 62 | + return |
| 63 | + } |
| 64 | + |
| 65 | + for block in function.blocks { |
| 66 | + for arg in block.arguments { |
| 67 | + optimize(value: arg, context) |
| 68 | + if !context.continueWithNextSubpassRun() { |
| 69 | + return |
| 70 | + } |
| 71 | + } |
| 72 | + for inst in block.instructions { |
| 73 | + for result in inst.results { |
| 74 | + optimize(value: result, context) |
| 75 | + if !context.continueWithNextSubpassRun(for: inst) { |
| 76 | + return |
| 77 | + } |
| 78 | + } |
| 79 | + } |
| 80 | + } |
| 81 | +} |
| 82 | + |
| 83 | +private func optimize(value: Value, _ context: FunctionPassContext) { |
| 84 | + guard value.ownership == .owned, |
| 85 | + // Avoid all the analysis effort if there are no destroys to hoist. |
| 86 | + !value.uses.filterUsers(ofType: DestroyValueInst.self).isEmpty |
| 87 | + else { |
| 88 | + return |
| 89 | + } |
| 90 | + |
| 91 | + var hoistableDestroys = selectHoistableDestroys(of: value, context) |
| 92 | + defer { hoistableDestroys.deinitialize() } |
| 93 | + |
| 94 | + var minimalLiverange = InstructionRange(withLiverangeOf: value, ignoring: hoistableDestroys, context) |
| 95 | + defer { minimalLiverange.deinitialize() } |
| 96 | + |
| 97 | + hoistDestroys(of: value, toEndOf: minimalLiverange, restrictingTo: &hoistableDestroys, context) |
| 98 | +} |
| 99 | + |
| 100 | +private func selectHoistableDestroys(of value: Value, _ context: FunctionPassContext) -> InstructionSet { |
| 101 | + // Also includes liveranges of copied values and values stored to memory. |
| 102 | + var forwardExtendedLiverange = InstructionRange(withForwardExtendedLiverangeOf: value, context) |
| 103 | + defer { forwardExtendedLiverange.deinitialize() } |
| 104 | + |
| 105 | + let deadEndBlocks = context.deadEndBlocks |
| 106 | + var hoistableDestroys = InstructionSet(context) |
| 107 | + |
| 108 | + for use in value.uses { |
| 109 | + if let destroy = use.instruction as? DestroyValueInst, |
| 110 | + // We can hoist all destroys for which another copy of the value is alive at the destroy. |
| 111 | + forwardExtendedLiverange.contains(destroy), |
| 112 | + // TODO: once we have complete OSSA lifetimes we don't need to handle dead-end blocks. |
| 113 | + !deadEndBlocks.isDeadEnd(destroy.parentBlock) |
| 114 | + { |
| 115 | + hoistableDestroys.insert(destroy) |
| 116 | + } |
| 117 | + } |
| 118 | + return hoistableDestroys |
| 119 | +} |
| 120 | + |
| 121 | +private func hoistDestroys(of value: Value, |
| 122 | + toEndOf minimalLiverange: InstructionRange, |
| 123 | + restrictingTo hoistableDestroys: inout InstructionSet, |
| 124 | + _ context: FunctionPassContext) |
| 125 | +{ |
| 126 | + createNewDestroys(for: value, atEndPointsOf: minimalLiverange, reusing: &hoistableDestroys, context) |
| 127 | + |
| 128 | + createNewDestroys(for: value, atExitPointsOf: minimalLiverange, reusing: &hoistableDestroys, context) |
| 129 | + |
| 130 | + removeDestroys(of: value, restrictingTo: hoistableDestroys, context) |
| 131 | +} |
| 132 | + |
| 133 | +private func createNewDestroys( |
| 134 | + for value: Value, |
| 135 | + atEndPointsOf liverange: InstructionRange, |
| 136 | + reusing hoistableDestroys: inout InstructionSet, |
| 137 | + _ context: FunctionPassContext |
| 138 | +) { |
| 139 | + let deadEndBlocks = context.deadEndBlocks |
| 140 | + |
| 141 | + for endInst in liverange.ends { |
| 142 | + if !endInst.endsLifetime(of: value) { |
| 143 | + Builder.insert(after: endInst, context) { builder in |
| 144 | + builder.createDestroy(of: value, reusing: &hoistableDestroys, notIn: deadEndBlocks) |
| 145 | + } |
| 146 | + } |
| 147 | + } |
| 148 | +} |
| 149 | + |
| 150 | +private func createNewDestroys( |
| 151 | + for value: Value, |
| 152 | + atExitPointsOf liverange: InstructionRange, |
| 153 | + reusing hoistableDestroys: inout InstructionSet, |
| 154 | + _ context: FunctionPassContext |
| 155 | +) { |
| 156 | + let deadEndBlocks = context.deadEndBlocks |
| 157 | + |
| 158 | + for exitBlock in liverange.exitBlocks { |
| 159 | + let builder = Builder(atBeginOf: exitBlock, context) |
| 160 | + builder.createDestroy(of: value, reusing: &hoistableDestroys, notIn: deadEndBlocks) |
| 161 | + } |
| 162 | +} |
| 163 | + |
| 164 | +private func removeDestroys( |
| 165 | + of value: Value, |
| 166 | + restrictingTo hoistableDestroys: InstructionSet, |
| 167 | + _ context: FunctionPassContext |
| 168 | +) { |
| 169 | + for use in value.uses { |
| 170 | + if let destroy = use.instruction as? DestroyValueInst, |
| 171 | + hoistableDestroys.contains(destroy) |
| 172 | + { |
| 173 | + context.erase(instruction: destroy) |
| 174 | + } |
| 175 | + } |
| 176 | +} |
| 177 | + |
| 178 | +private extension InstructionRange { |
| 179 | + |
| 180 | + init(withLiverangeOf initialDef: Value, ignoring ignoreDestroys: InstructionSet, _ context: FunctionPassContext) |
| 181 | + { |
| 182 | + var liverange = InstructionRange(for: initialDef, context) |
| 183 | + var visitor = InteriorUseWalker(definingValue: initialDef, ignoreEscape: true, visitInnerUses: false, context) { |
| 184 | + if !ignoreDestroys.contains($0.instruction) { |
| 185 | + liverange.insert($0.instruction) |
| 186 | + } |
| 187 | + return .continueWalk |
| 188 | + } |
| 189 | + defer { visitor.deinitialize() } |
| 190 | + |
| 191 | + _ = visitor.visitUses() |
| 192 | + self = liverange |
| 193 | + } |
| 194 | + |
| 195 | + // In addition to the forward-extended liverange, also follows copy_value's transitively. |
| 196 | + init(withForwardExtendedLiverangeOf initialDef: Value, _ context: FunctionPassContext) { |
| 197 | + self.init(for: initialDef, context) |
| 198 | + |
| 199 | + var worklist = ValueWorklist(context) |
| 200 | + defer { worklist.deinitialize() } |
| 201 | + |
| 202 | + worklist.pushIfNotVisited(initialDef) |
| 203 | + while let value = worklist.pop() { |
| 204 | + assert(value.ownership == .owned) |
| 205 | + |
| 206 | + for use in value.uses { |
| 207 | + let user = use.instruction |
| 208 | + if !use.endsLifetime { |
| 209 | + if let copy = user as? CopyValueInst { |
| 210 | + worklist.pushIfNotVisited(copy) |
| 211 | + } |
| 212 | + continue |
| 213 | + } |
| 214 | + |
| 215 | + switch user { |
| 216 | + case let store as StoreInst: |
| 217 | + extendLiverangeInMemory(of: initialDef, with: store, context) |
| 218 | + |
| 219 | + case let termInst as TermInst & ForwardingInstruction: |
| 220 | + worklist.pushIfNotVisited(contentsOf: termInst.forwardedResults.lazy.filter({ $0.ownership != .none })) |
| 221 | + |
| 222 | + case is ForwardingInstruction, is MoveValueInst: |
| 223 | + if let result = user.results.lazy.filter({ $0.ownership != .none }).singleElement { |
| 224 | + worklist.pushIfNotVisited(result) |
| 225 | + } |
| 226 | + |
| 227 | + default: |
| 228 | + // We cannot extend a lexical liverange with a non-lexical liverange, because afterwards the |
| 229 | + // non-lexical liverange could be shrunk over a deinit barrier which would let the original |
| 230 | + // lexical liverange to be shrunk, too. |
| 231 | + if !initialDef.isInLexicalLiverange(context) || value.isInLexicalLiverange(context) { |
| 232 | + self.insert(user) |
| 233 | + } |
| 234 | + } |
| 235 | + } |
| 236 | + } |
| 237 | + } |
| 238 | + |
| 239 | + private mutating func extendLiverangeInMemory( |
| 240 | + of initialDef: Value, |
| 241 | + with store: StoreInst, |
| 242 | + _ context: FunctionPassContext |
| 243 | + ) { |
| 244 | + let domTree = context.dominatorTree |
| 245 | + |
| 246 | + if initialDef.destroyUsers(dominatedBy: store.parentBlock, domTree).isEmpty { |
| 247 | + return |
| 248 | + } |
| 249 | + |
| 250 | + // We have to take care of lexical lifetimes. See comment above. |
| 251 | + if initialDef.isInLexicalLiverange(context) && |
| 252 | + !store.destination.accessBase.isInLexicalOrGlobalLiverange(context) |
| 253 | + { |
| 254 | + return |
| 255 | + } |
| 256 | + |
| 257 | + if isTakeOrDestroy(ofAddress: store.destination, after: store, beforeDestroysOf: initialDef, context) { |
| 258 | + return |
| 259 | + } |
| 260 | + |
| 261 | + self.insert(contentsOf: initialDef.destroyUsers(dominatedBy: store.parentBlock, domTree).map { $0.next! }) |
| 262 | + } |
| 263 | +} |
| 264 | + |
| 265 | +private func isTakeOrDestroy( |
| 266 | + ofAddress address: Value, |
| 267 | + after store: StoreInst, |
| 268 | + beforeDestroysOf initialDef: Value, |
| 269 | + _ context: FunctionPassContext |
| 270 | +) -> Bool { |
| 271 | + let aliasAnalysis = context.aliasAnalysis |
| 272 | + let domTree = context.dominatorTree |
| 273 | + var worklist = InstructionWorklist(context) |
| 274 | + defer { worklist.deinitialize() } |
| 275 | + |
| 276 | + worklist.pushIfNotVisited(store.next!) |
| 277 | + while let inst = worklist.pop() { |
| 278 | + if inst.endsLifetime(of: initialDef) { |
| 279 | + continue |
| 280 | + } |
| 281 | + if inst.mayTakeOrDestroy(address: address, aliasAnalysis) { |
| 282 | + return true |
| 283 | + } |
| 284 | + if let next = inst.next { |
| 285 | + worklist.pushIfNotVisited(next) |
| 286 | + } else { |
| 287 | + for succ in inst.parentBlock.successors where store.parentBlock.dominates(succ, domTree) { |
| 288 | + worklist.pushIfNotVisited(succ.instructions.first!) |
| 289 | + } |
| 290 | + } |
| 291 | + } |
| 292 | + return false |
| 293 | +} |
| 294 | + |
| 295 | +private extension Builder { |
| 296 | + func createDestroy(of value: Value, |
| 297 | + reusing hoistableDestroys: inout InstructionSet, |
| 298 | + notIn deadEndBlocks: DeadEndBlocksAnalysis) { |
| 299 | + guard case .before(let insertionPoint) = insertionPoint else { |
| 300 | + fatalError("unexpected kind of insertion point") |
| 301 | + } |
| 302 | + if deadEndBlocks.isDeadEnd(insertionPoint.parentBlock) { |
| 303 | + return |
| 304 | + } |
| 305 | + if hoistableDestroys.contains(insertionPoint) { |
| 306 | + hoistableDestroys.erase(insertionPoint) |
| 307 | + } else { |
| 308 | + createDestroyValue(operand: value) |
| 309 | + } |
| 310 | + } |
| 311 | +} |
| 312 | + |
| 313 | +private extension Value { |
| 314 | + func destroyUsers(dominatedBy domBlock: BasicBlock, _ domTree: DominatorTree) -> |
| 315 | + LazyMapSequence<LazyFilterSequence<LazyMapSequence<UseList, DestroyValueInst?>>, DestroyValueInst> { |
| 316 | + return uses.lazy.compactMap { use in |
| 317 | + if let destroy = use.instruction as? DestroyValueInst, |
| 318 | + domBlock.dominates(destroy.parentBlock, domTree) |
| 319 | + { |
| 320 | + return destroy |
| 321 | + } |
| 322 | + return nil |
| 323 | + } |
| 324 | + } |
| 325 | +} |
| 326 | + |
| 327 | +private extension Instruction { |
| 328 | + func endsLifetime(of value: Value) -> Bool { |
| 329 | + return operands.contains { $0.value == value && $0.endsLifetime } |
| 330 | + } |
| 331 | + |
| 332 | + func mayTakeOrDestroy(address: Value, _ aliasAnalysis: AliasAnalysis) -> Bool { |
| 333 | + switch self { |
| 334 | + case is BeginAccessInst, is EndAccessInst, is EndBorrowInst: |
| 335 | + return false |
| 336 | + default: |
| 337 | + return mayWrite(toAddress: address, aliasAnalysis) |
| 338 | + } |
| 339 | + } |
| 340 | +} |
| 341 | + |
| 342 | +private extension AccessBase { |
| 343 | + func isInLexicalOrGlobalLiverange(_ context: FunctionPassContext) -> Bool { |
| 344 | + switch self { |
| 345 | + case .box(let pbi): return pbi.box.isInLexicalLiverange(context) |
| 346 | + case .class(let rea): return rea.instance.isInLexicalLiverange(context) |
| 347 | + case .tail(let rta): return rta.instance.isInLexicalLiverange(context) |
| 348 | + case .stack(let asi): return asi.isLexical |
| 349 | + case .global: return true |
| 350 | + case .argument(let arg): |
| 351 | + switch arg.convention { |
| 352 | + case .indirectIn, .indirectInGuaranteed, .indirectInout, .indirectInoutAliasable: |
| 353 | + return arg.isLexical |
| 354 | + default: |
| 355 | + return false |
| 356 | + } |
| 357 | + case .yield, .storeBorrow, .pointer, .index, .unidentified: |
| 358 | + return false |
| 359 | + } |
| 360 | + } |
| 361 | +} |
0 commit comments