Files
swift-mirror/SwiftCompilerSources/Sources/Optimizer/FunctionPasses/StackPromotion.swift
Nate Chandler 74528eef8c [NFC] OptUtils: Promote isInLoop to internal.
It will be used by both AllocBoxToStack in addition to StackPromotion
and for the same reason.
2025-08-27 17:03:47 -07:00

333 lines
13 KiB
Swift

//===--- StackPromotion.swift - Stack promotion optimization --------------===//
//
// This source file is part of the Swift.org open source project
//
// Copyright (c) 2014 - 2022 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 AST
import SIL
/// Promotes heap allocated objects to the stack.
///
/// It handles `alloc_ref` and `alloc_ref_dynamic` instructions of native swift
/// classes: if promoted, the `[stack]` attribute is set in the allocation
/// instruction and a `dealloc_stack_ref` is inserted at the end of the object's
/// lifetime.
/// The main criteria for stack promotion is that the allocated object must not
/// escape its function.
///
/// Example:
/// %k = alloc_ref $Klass
/// // .. some uses of %k
/// destroy_value %k // The end of %k's lifetime
///
/// is transformed to:
///
/// %k = alloc_ref [stack] $Klass
/// // .. some uses of %k
/// destroy_value %k
/// dealloc_stack_ref %k
///
/// The destroy/release of the promoted object remains in the SIL, but is effectively
/// a no-op, because a stack promoted object is initialized with an "immortal"
/// reference count.
/// Later optimizations can clean that up.
let stackPromotion = FunctionPass(name: "stack-promotion") {
(function: Function, context: FunctionPassContext) in
let deadEndBlocks = context.deadEndBlocks
var needFixStackNesting = false
for inst in function.instructions {
if let allocRef = inst as? AllocRefInstBase {
if !context.continueWithNextSubpassRun(for: allocRef) {
break
}
if tryPromoteAlloc(allocRef, deadEndBlocks, context) {
needFixStackNesting = true
}
}
}
if needFixStackNesting {
// Make sure that all stack allocating instructions are nested correctly.
context.fixStackNesting(in: function)
}
}
// Returns true if the allocation is promoted.
private func tryPromoteAlloc(_ allocRef: AllocRefInstBase,
_ deadEndBlocks: DeadEndBlocksAnalysis,
_ context: FunctionPassContext) -> Bool {
if allocRef.isObjC || allocRef.canAllocOnStack {
return false
}
// Usually resilient classes cannot be promoted anyway, because their initializers are
// not visible and let the object appear to escape.
if allocRef.type.nominal!.isResilient(in: allocRef.parentFunction) {
return false
}
if let dtor = (allocRef.type.nominal as? ClassDecl)?.destructor {
if dtor.isIsolated {
// Classes (including actors) with isolated deinit can escape implicitly.
//
// We could optimize this further and allow promotion if we can prove that
// deinit will take fast path (i.e. it will not schedule a job).
// But for now, let's keep things simple and disable promotion conservatively.
return false
}
}
// The most important check: does the object escape the current function?
if allocRef.isEscaping(context) {
return false
}
if deadEndBlocks.isDeadEnd(allocRef.parentBlock) {
// Allocations inside a code region which ends up in a no-return block may missing their
// final release. Therefore we extend their lifetime indefinitely, e.g.
//
// %k = alloc_ref $Klass
// ...
// unreachable // The end of %k's lifetime
//
// There is one exception: if it's in a loop (within the dead-end region) we must not
// extend its lifetime. In this case we can be sure that its final release is not
// missing, because otherwise the object would be leaking. For example:
//
// bb1:
// %k = alloc_ref $Klass
// ... // %k's lifetime must end somewhere here
// cond_br %c, bb1, bb2
// bb2:
// unreachable
//
// Therefore, if the allocation is inside a loop, we can treat it like allocations in
// non dead-end regions.
if !isInLoop(block: allocRef.parentBlock, context) {
allocRef.setIsStackAllocatable(context)
return true
}
}
// Try to find the top most dominator block which dominates all use points.
// * This block can be located "earlier" than the actual allocation block, in case the
// promoted object is stored into an "outer" object, e.g.
//
// bb0: // outerDominatingBlock _
// %o = alloc_ref $Outer |
// ... |
// bb1: // allocation block _ |
// %k = alloc_ref $Klass | | "outer"
// %f = ref_element_addr %o, #Outer.f | "inner" | liverange
// store %k to %f | liverange |
// ... | |
// destroy_value %o _| _|
//
// * Finding the `outerDominatingBlock` is not guaranteed to work.
// In this example, the top most dominator block is `bb0`, but `bb0` has no
// use points in the outer liverange. We'll get `bb3` as outerDominatingBlock.
// This is no problem because 1. it's an unusual case and 2. the `outerBlockRange`
// is invalid in this case and we'll bail later.
//
// bb0: // real top most dominating block
// cond_br %c, bb1, bb2
// bb1:
// %o1 = alloc_ref $Outer
// br bb3(%o1)
// bb2:
// %o2 = alloc_ref $Outer
// br bb3(%o1)
// bb3(%o): // resulting outerDominatingBlock: wrong!
// %k = alloc_ref $Klass
// %f = ref_element_addr %o, #Outer.f
// store %k to %f
// destroy_value %o
//
let domTree = context.dominatorTree
let outerDominatingBlock = getDominatingBlockOfAllUsePoints(context: context, allocRef, domTree: domTree)
// The "inner" liverange contains all use points which are dominated by the allocation block.
// Note that this `visit` cannot fail because otherwise our initial `isEscaping` check would have failed already.
var innerRange = allocRef.visit(using: ComputeInnerLiverange(of: allocRef, domTree, context), context)!
defer { innerRange.deinitialize() }
// The "outer" liverange contains all use points.
// Same here: this `visit` cannot fail.
var outerBlockRange = allocRef.visit(using: ComputeOuterBlockrange(dominatedBy: outerDominatingBlock, context), context)!
defer { outerBlockRange.deinitialize() }
assert(innerRange.blockRange.isValid, "inner range should be valid because we did a dominance check")
if !outerBlockRange.isValid {
// This happens if we fail to find a correct outerDominatingBlock.
return false
}
// Check if there is a control flow edge from the inner to the outer liverange, which
// would mean that the promoted object can escape to the outer liverange.
// This can e.g. be the case if the inner liverange does not post dominate the outer range:
// _
// %o = alloc_ref $Outer |
// cond_br %c, bb1, bb2 |
// bb1: _ |
// %k = alloc_ref $Klass | | outer
// %f = ref_element_addr %o, #Outer.f | inner | range
// store %k to %f | range |
// br bb2 // branch from inner to outer _| |
// bb2: |
// destroy_value %o _|
//
// Or if there is a loop with a back-edge from the inner to the outer range:
// _
// %o = alloc_ref $Outer |
// br bb1 |
// bb1: _ |
// %k = alloc_ref $Klass | | outer
// %f = ref_element_addr %o, #Outer.f | inner | range
// store %k to %f | range |
// cond_br %c, bb1, bb2 // inner -> outer _| |
// bb2: |
// destroy_value %o _|
//
if innerRange.blockRange.hasControlFlowEdge(to: outerBlockRange) {
return false
}
// There shouldn't be any critical exit edges from the liverange, because that would mean
// that the promoted allocation is leaking.
// Just to be on the safe side, do a check and bail if we find critical exit edges: we
// cannot insert instructions on critical edges.
if innerRange.blockRange.containsCriticalExitEdges(deadEndBlocks: deadEndBlocks) {
return false
}
// Do the transformation!
// Insert `dealloc_stack_ref` instructions at the exit- and end-points of the inner liverange.
for exitInst in innerRange.exits {
if !deadEndBlocks.isDeadEnd(exitInst.parentBlock) {
let builder = Builder(before: exitInst, context)
builder.createDeallocStackRef(allocRef)
}
}
for endInst in innerRange.ends {
Builder.insert(after: endInst, location: allocRef.location, context) {
(builder) in builder.createDeallocStackRef(allocRef)
}
}
allocRef.setIsStackAllocatable(context)
return true
}
private func getDominatingBlockOfAllUsePoints(context: FunctionPassContext,
_ value: SingleValueInstruction,
domTree: DominatorTree) -> BasicBlock {
struct FindDominatingBlock : EscapeVisitorWithResult {
var result: BasicBlock
let domTree: DominatorTree
mutating func visitUse(operand: Operand, path: EscapePath) -> UseResult {
let defBlock = operand.value.parentBlock
if defBlock.dominates(result, domTree) {
result = defBlock
}
return .continueWalk
}
}
return value.visit(using: FindDominatingBlock(result: value.parentBlock, domTree: domTree), context)!
}
private struct ComputeInnerLiverange : EscapeVisitorWithResult {
var result: InstructionRange
let domTree: DominatorTree
init(of instruction: Instruction, _ domTree: DominatorTree, _ context: FunctionPassContext) {
result = InstructionRange(begin: instruction, context)
self.domTree = domTree
}
mutating func visitUse(operand: Operand, path: EscapePath) -> UseResult {
let user = operand.instruction
let beginBlockOfRange = result.blockRange.begin
if beginBlockOfRange.dominates(user.parentBlock, domTree) {
result.insert(user)
}
return .continueWalk
}
}
private struct ComputeOuterBlockrange : EscapeVisitorWithResult {
var result: BasicBlockRange
init(dominatedBy: BasicBlock, _ context: FunctionPassContext) {
result = BasicBlockRange(begin: dominatedBy, context)
}
mutating func visitUse(operand: Operand, path: EscapePath) -> UseResult {
let user = operand.instruction
result.insert(user.parentBlock)
let value = operand.value
let operandsDefinitionBlock = value.parentBlock
// Also insert the operand's definition. Otherwise we would miss allocation
// instructions (for which the `visitUse` closure is not called).
result.insert(operandsDefinitionBlock)
// We need to explicitly add predecessor blocks of phis because they
// are not necessarily visited during the down-walk in `isEscaping()`.
// This is important for the special case where there is a back-edge from the
// inner range to the inner rage's begin-block:
//
// bb0: // <- need to be in the outer range
// br bb1(%some_init_val)
// bb1(%arg):
// %k = alloc_ref $Klass // innerInstRange.begin
// cond_br bb2, bb1(%k) // back-edge to bb1 == innerInstRange.blockRange.begin
//
if let phi = Phi(value) {
result.insert(contentsOf: phi.predecessors)
}
return .continueWalk
}
}
private extension BasicBlockRange {
/// Returns true if there is a direct edge connecting this range with the `otherRange`.
func hasControlFlowEdge(to otherRange: BasicBlockRange) -> Bool {
func isOnlyInOtherRange(_ block: BasicBlock) -> Bool {
return !inclusiveRangeContains(block) && otherRange.inclusiveRangeContains(block)
}
for lifeBlock in inclusiveRange {
assert(otherRange.inclusiveRangeContains(lifeBlock), "range must be a subset of other range")
for succ in lifeBlock.successors {
if isOnlyInOtherRange(succ) && succ != otherRange.begin {
return true
}
// The entry of the begin-block is conceptually not part of the range. We can check if
// it's part of the `otherRange` by checking the begin-block's predecessors.
if succ == begin && begin.predecessors.contains(where: { isOnlyInOtherRange($0) }) {
return true
}
}
}
return false
}
func containsCriticalExitEdges(deadEndBlocks: DeadEndBlocksAnalysis) -> Bool {
exits.contains { !deadEndBlocks.isDeadEnd($0) && !$0.hasSinglePredecessor }
}
}