Files
swift-mirror/SwiftCompilerSources/Sources/Optimizer/FunctionPasses/StackPromotion.swift
Erik Eckstein 393d1a1488 SIL Builder: rename insert(at:) -> insert(before:)
It matches with `insert(after:)` and the intent should be clearer now
2023-01-16 15:11:34 +01:00

296 lines
12 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 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 deadEndBlocks.isDeadEnd(allocRef.parentBlock) {
// Don't stack promote any allocation inside a code region which ends up
// in a no-return block. Such allocations may missing their final release.
// We would insert the deallocation too early, which may result in a
// use-after-free problem.
continue
}
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.
function.fixStackNesting(context)
}
}
// Returns true if the allocation is promoted.
private func tryPromoteAlloc(_ allocRef: AllocRefInstBase,
_ deadEndBlocks: DeadEndBlocksAnalysis,
_ context: FunctionPassContext) -> Bool {
if allocRef.isObjC || allocRef.canAllocOnStack {
return false
}
// The most important check: does the object escape the current function?
if allocRef.isEscaping(context) {
return false
}
// 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" | liferange
// store %k to %f | liferange |
// ... | |
// 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 liferange. 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" liferange 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: ComputeInnerLiferange(of: allocRef, domTree, context), context)!
defer { innerRange.deinitialize() }
// The "outer" liferange 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 liferange, which
// would mean that the promoted object can escape to the outer liferange.
// This can e.g. be the case if the inner liferange 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 liferange, 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 liferange.
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 ComputeInnerLiferange : 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 phi-arguments becaues they
// are not necesesarily 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 value is BlockArgument {
result.insert(contentsOf: operandsDefinitionBlock.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) && block != otherRange.begin
}
for lifeBlock in inclusiveRange {
assert(otherRange.inclusiveRangeContains(lifeBlock), "range must be a subset of other range")
for succ in lifeBlock.successors {
if isOnlyInOtherRange(succ) {
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 }
}
}