mirror of
https://github.com/apple/swift.git
synced 2025-12-14 20:36:38 +01:00
* remove `filterUsers(ofType:)`, because it's a duplication of `users(ofType:)`
* rename `filterUses(ofType:)` -> `filter(usersOfType:)`
* rename `ignoreUses(ofType:)` -> `ignore(usersOfType:)`
* rename `getSingleUser` -> `singleUser`
* implement `singleUse` with `Sequence.singleElement`
* implement `ignoreDebugUses` with `ignore(usersOfType:)`
This is a follow-up of eb1d5f484c.
382 lines
12 KiB
Swift
382 lines
12 KiB
Swift
//===--- DestroyHoisting.swift ---------------------------------------------==//
|
|
//
|
|
// This source file is part of the Swift.org open source project
|
|
//
|
|
// Copyright (c) 2014 - 2024 Apple Inc. and the Swift project authors
|
|
// Licensed under Apache License v2.0 with Runtime Library Exception
|
|
//
|
|
// See https://swift.org/LICENSE.txt for license information
|
|
// See https://swift.org/CONTRIBUTORS.txt for the list of Swift project authors
|
|
//
|
|
//===----------------------------------------------------------------------===//
|
|
|
|
import SIL
|
|
|
|
/// Hoists `destroy_value` instructions without shrinking an object's lifetime.
|
|
/// This is done if it can be proved that another copy of a value (either in an SSA value or in memory) keeps
|
|
/// the referenced object(s) alive until the original position of the `destroy_value`.
|
|
///
|
|
/// ```
|
|
/// %1 = copy_value %0
|
|
/// ...
|
|
/// last_use_of %0
|
|
/// // other instructions
|
|
/// destroy_value %0 // %1 is still alive here
|
|
/// ```
|
|
/// ->
|
|
/// ```
|
|
/// %1 = copy_value %0
|
|
/// ...
|
|
/// last_use_of %0
|
|
/// destroy_value %0
|
|
/// // other instructions
|
|
/// ```
|
|
///
|
|
/// This also works if a copy of the value is kept alive in memory:
|
|
///
|
|
/// ```
|
|
/// %1 = copy_value %0
|
|
/// store %1 to [assign] %a
|
|
/// ...
|
|
/// last_use_of %0
|
|
/// // other instructions
|
|
/// destroy_value %0 // memory location %a is not modified since the store
|
|
/// ```
|
|
/// ->
|
|
/// ```
|
|
/// %1 = copy_value %0
|
|
/// store %0 to [assign] %a
|
|
/// ...
|
|
/// last_use_of %0
|
|
/// destroy_value %0
|
|
/// // other instructions
|
|
/// ```
|
|
///
|
|
/// The benefit of this optimization is that it can enable copy-propagation by moving
|
|
/// destroys above deinit barries and access scopes.
|
|
///
|
|
let destroyHoisting = FunctionPass(name: "destroy-hoisting") {
|
|
(function: Function, context: FunctionPassContext) in
|
|
|
|
if !function.hasOwnership {
|
|
return
|
|
}
|
|
|
|
for block in function.blocks {
|
|
for arg in block.arguments {
|
|
optimize(value: arg, context)
|
|
if !context.continueWithNextSubpassRun() {
|
|
return
|
|
}
|
|
}
|
|
for inst in block.instructions {
|
|
for result in inst.results {
|
|
optimize(value: result, context)
|
|
if !context.continueWithNextSubpassRun(for: inst) {
|
|
return
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
private func optimize(value: Value, _ context: FunctionPassContext) {
|
|
guard value.ownership == .owned,
|
|
// Avoid all the analysis effort if there are no destroys to hoist.
|
|
!value.uses.filter(usersOfType: DestroyValueInst.self).isEmpty
|
|
else {
|
|
return
|
|
}
|
|
|
|
var (foundDestroys, hoistableDestroys) = selectHoistableDestroys(of: value, context)
|
|
defer { hoistableDestroys.deinitialize() }
|
|
|
|
guard foundDestroys else {
|
|
return
|
|
}
|
|
|
|
guard var minimalLiverange = InstructionRange(withLiverangeOf: value, ignoring: hoistableDestroys, context) else {
|
|
return
|
|
}
|
|
defer { minimalLiverange.deinitialize() }
|
|
|
|
hoistDestroys(of: value, toEndOf: minimalLiverange, restrictingTo: &hoistableDestroys, context)
|
|
}
|
|
|
|
private func selectHoistableDestroys(of value: Value, _ context: FunctionPassContext) -> (Bool, InstructionSet) {
|
|
// Also includes liveranges of copied values and values stored to memory.
|
|
var forwardExtendedLiverange = InstructionRange(withForwardExtendedLiverangeOf: value, context)
|
|
defer { forwardExtendedLiverange.deinitialize() }
|
|
|
|
let deadEndBlocks = context.deadEndBlocks
|
|
var foundDestroys = false
|
|
var hoistableDestroys = InstructionSet(context)
|
|
|
|
for use in value.uses {
|
|
if let destroy = use.instruction as? DestroyValueInst,
|
|
// We can hoist all destroys for which another copy of the value is alive at the destroy.
|
|
forwardExtendedLiverange.contains(destroy),
|
|
// TODO: once we have complete OSSA lifetimes we don't need to handle dead-end blocks.
|
|
!deadEndBlocks.isDeadEnd(destroy.parentBlock)
|
|
{
|
|
foundDestroys = true
|
|
hoistableDestroys.insert(destroy)
|
|
}
|
|
}
|
|
return (foundDestroys, hoistableDestroys)
|
|
}
|
|
|
|
private func hoistDestroys(of value: Value,
|
|
toEndOf minimalLiverange: InstructionRange,
|
|
restrictingTo hoistableDestroys: inout InstructionSet,
|
|
_ context: FunctionPassContext)
|
|
{
|
|
createNewDestroys(for: value, atEndPointsOf: minimalLiverange, reusing: &hoistableDestroys, context)
|
|
|
|
createNewDestroys(for: value, atExitPointsOf: minimalLiverange, reusing: &hoistableDestroys, context)
|
|
|
|
removeDestroys(of: value, restrictingTo: hoistableDestroys, context)
|
|
}
|
|
|
|
private func createNewDestroys(
|
|
for value: Value,
|
|
atEndPointsOf liverange: InstructionRange,
|
|
reusing hoistableDestroys: inout InstructionSet,
|
|
_ context: FunctionPassContext
|
|
) {
|
|
let deadEndBlocks = context.deadEndBlocks
|
|
|
|
for endInst in liverange.ends {
|
|
if !endInst.endsLifetime(of: value) {
|
|
Builder.insert(after: endInst, context) { builder in
|
|
builder.createDestroy(of: value, reusing: &hoistableDestroys, notIn: deadEndBlocks)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
private func createNewDestroys(
|
|
for value: Value,
|
|
atExitPointsOf liverange: InstructionRange,
|
|
reusing hoistableDestroys: inout InstructionSet,
|
|
_ context: FunctionPassContext
|
|
) {
|
|
let deadEndBlocks = context.deadEndBlocks
|
|
|
|
for exitBlock in liverange.exitBlocks {
|
|
let builder = Builder(atBeginOf: exitBlock, context)
|
|
builder.createDestroy(of: value, reusing: &hoistableDestroys, notIn: deadEndBlocks)
|
|
}
|
|
}
|
|
|
|
private func removeDestroys(
|
|
of value: Value,
|
|
restrictingTo hoistableDestroys: InstructionSet,
|
|
_ context: FunctionPassContext
|
|
) {
|
|
for use in value.uses {
|
|
if let destroy = use.instruction as? DestroyValueInst,
|
|
hoistableDestroys.contains(destroy)
|
|
{
|
|
context.erase(instruction: destroy)
|
|
}
|
|
}
|
|
}
|
|
|
|
private extension InstructionRange {
|
|
|
|
init?(withLiverangeOf initialDef: Value, ignoring ignoreDestroys: InstructionSet, _ context: FunctionPassContext)
|
|
{
|
|
var liverange = InstructionRange(for: initialDef, context)
|
|
var visitor = InteriorUseWalker(definingValue: initialDef, ignoreEscape: false, visitInnerUses: true, context) {
|
|
if !ignoreDestroys.contains($0.instruction) {
|
|
liverange.insert($0.instruction)
|
|
}
|
|
return .continueWalk
|
|
}
|
|
defer { visitor.deinitialize() }
|
|
|
|
// This is important to visit begin_borrows which don't have an end_borrow in dead-end blocks.
|
|
// TODO: we can remove this once we have complete lifetimes.
|
|
visitor.innerScopeHandler = {
|
|
if let inst = $0.definingInstruction {
|
|
liverange.insert(inst)
|
|
}
|
|
return .continueWalk
|
|
}
|
|
|
|
guard visitor.visitUses() == .continueWalk else {
|
|
liverange.deinitialize()
|
|
return nil
|
|
}
|
|
self = liverange
|
|
}
|
|
|
|
// In addition to the forward-extended liverange, also follows copy_value's transitively.
|
|
init(withForwardExtendedLiverangeOf initialDef: Value, _ context: FunctionPassContext) {
|
|
self.init(for: initialDef, context)
|
|
|
|
var worklist = ValueWorklist(context)
|
|
defer { worklist.deinitialize() }
|
|
|
|
worklist.pushIfNotVisited(initialDef)
|
|
while let value = worklist.pop() {
|
|
assert(value.ownership == .owned)
|
|
|
|
for use in value.uses {
|
|
let user = use.instruction
|
|
if !use.endsLifetime {
|
|
if let copy = user as? CopyValueInst {
|
|
worklist.pushIfNotVisited(copy)
|
|
}
|
|
continue
|
|
}
|
|
|
|
switch user {
|
|
case let store as StoreInst:
|
|
extendLiverangeInMemory(of: initialDef, with: store, context)
|
|
|
|
case let termInst as TermInst & ForwardingInstruction:
|
|
worklist.pushIfNotVisited(contentsOf: termInst.forwardedResults.lazy.filter({ $0.ownership != .none }))
|
|
|
|
case is ForwardingInstruction, is MoveValueInst:
|
|
if let result = user.results.lazy.filter({ $0.ownership != .none }).singleElement {
|
|
worklist.pushIfNotVisited(result)
|
|
}
|
|
|
|
default:
|
|
// We cannot extend a lexical liverange with a non-lexical liverange, because afterwards the
|
|
// non-lexical liverange could be shrunk over a deinit barrier which would let the original
|
|
// lexical liverange to be shrunk, too.
|
|
if !initialDef.isInLexicalLiverange(context) || value.isInLexicalLiverange(context) {
|
|
self.insert(user)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
private mutating func extendLiverangeInMemory(
|
|
of initialDef: Value,
|
|
with store: StoreInst,
|
|
_ context: FunctionPassContext
|
|
) {
|
|
let domTree = context.dominatorTree
|
|
|
|
if initialDef.destroyUsers(dominatedBy: store.parentBlock, domTree).isEmpty {
|
|
return
|
|
}
|
|
|
|
// We have to take care of lexical lifetimes. See comment above.
|
|
if initialDef.isInLexicalLiverange(context) &&
|
|
!store.destination.accessBase.isInLexicalOrGlobalLiverange(context)
|
|
{
|
|
return
|
|
}
|
|
|
|
if isTakeOrDestroy(ofAddress: store.destination, after: store, beforeDestroysOf: initialDef, context) {
|
|
return
|
|
}
|
|
|
|
self.insert(contentsOf: initialDef.destroyUsers(dominatedBy: store.parentBlock, domTree).map { $0.next! })
|
|
}
|
|
}
|
|
|
|
private func isTakeOrDestroy(
|
|
ofAddress address: Value,
|
|
after store: StoreInst,
|
|
beforeDestroysOf initialDef: Value,
|
|
_ context: FunctionPassContext
|
|
) -> Bool {
|
|
let aliasAnalysis = context.aliasAnalysis
|
|
let domTree = context.dominatorTree
|
|
var worklist = InstructionWorklist(context)
|
|
defer { worklist.deinitialize() }
|
|
|
|
worklist.pushIfNotVisited(store.next!)
|
|
while let inst = worklist.pop() {
|
|
if inst.endsLifetime(of: initialDef) {
|
|
continue
|
|
}
|
|
if inst.mayTakeOrDestroy(address: address, aliasAnalysis) {
|
|
return true
|
|
}
|
|
if let next = inst.next {
|
|
worklist.pushIfNotVisited(next)
|
|
} else {
|
|
for succ in inst.parentBlock.successors where store.parentBlock.dominates(succ, domTree) {
|
|
worklist.pushIfNotVisited(succ.instructions.first!)
|
|
}
|
|
}
|
|
}
|
|
return false
|
|
}
|
|
|
|
private extension Builder {
|
|
func createDestroy(of value: Value,
|
|
reusing hoistableDestroys: inout InstructionSet,
|
|
notIn deadEndBlocks: DeadEndBlocksAnalysis) {
|
|
guard case .before(let insertionPoint) = insertionPoint else {
|
|
fatalError("unexpected kind of insertion point")
|
|
}
|
|
if deadEndBlocks.isDeadEnd(insertionPoint.parentBlock) {
|
|
return
|
|
}
|
|
if hoistableDestroys.contains(insertionPoint) {
|
|
hoistableDestroys.erase(insertionPoint)
|
|
} else {
|
|
createDestroyValue(operand: value)
|
|
}
|
|
}
|
|
}
|
|
|
|
private extension Value {
|
|
func destroyUsers(dominatedBy domBlock: BasicBlock, _ domTree: DominatorTree) ->
|
|
LazyMapSequence<LazyFilterSequence<LazyMapSequence<UseList, DestroyValueInst?>>, DestroyValueInst> {
|
|
return uses.lazy.compactMap { use in
|
|
if let destroy = use.instruction as? DestroyValueInst,
|
|
domBlock.dominates(destroy.parentBlock, domTree)
|
|
{
|
|
return destroy
|
|
}
|
|
return nil
|
|
}
|
|
}
|
|
}
|
|
|
|
private extension Instruction {
|
|
func endsLifetime(of value: Value) -> Bool {
|
|
return operands.contains { $0.value == value && $0.endsLifetime }
|
|
}
|
|
|
|
func mayTakeOrDestroy(address: Value, _ aliasAnalysis: AliasAnalysis) -> Bool {
|
|
switch self {
|
|
case is BeginAccessInst, is EndAccessInst, is EndBorrowInst:
|
|
return false
|
|
default:
|
|
return mayWrite(toAddress: address, aliasAnalysis)
|
|
}
|
|
}
|
|
}
|
|
|
|
private extension AccessBase {
|
|
func isInLexicalOrGlobalLiverange(_ context: FunctionPassContext) -> Bool {
|
|
switch self {
|
|
case .box(let pbi): return pbi.box.isInLexicalLiverange(context)
|
|
case .class(let rea): return rea.instance.isInLexicalLiverange(context)
|
|
case .tail(let rta): return rta.instance.isInLexicalLiverange(context)
|
|
case .stack(let asi): return asi.isLexical
|
|
case .global: return true
|
|
case .argument(let arg):
|
|
switch arg.convention {
|
|
case .indirectIn, .indirectInGuaranteed, .indirectInout, .indirectInoutAliasable:
|
|
return arg.isLexical
|
|
default:
|
|
return false
|
|
}
|
|
case .yield, .storeBorrow, .pointer, .index, .unidentified:
|
|
return false
|
|
}
|
|
}
|
|
}
|