Merge pull request #85814 from eeckstein/no-destroy-hoisting-in-dce

Optimizer: don't hoist destroys in DeadCodeElimination and the InstructionDeleter
This commit is contained in:
eeckstein
2025-12-04 14:22:14 +01:00
committed by GitHub
19 changed files with 735 additions and 192 deletions

View File

@@ -14,8 +14,147 @@ import SIL
extension DestroyValueInst : OnoneSimplifiable, SILCombineSimplifiable {
func simplify(_ context: SimplifyContext) {
// If the value has `.none` ownership, the destroy is a no-op. Note that a value can have `.none`
// ownership even if it's type is not trivial, e.g.
//
// ```
// %1 = enum $NonTrivialEnum, #NonTrivialEnum.trivialCase!enumelt // ownership: none
// %2 = destroy_value %1
// ```
//
if destroyedValue.ownership == .none {
context.erase(instruction: self)
return
}
tryRemoveForwardingOperandInstruction(context)
}
/// Attempt to optimize by forwarding the destroy to operands of forwarding instructions.
///
/// ```
/// %3 = struct $S (%1, %2)
/// destroy_value %3 // the only use of %3
/// ```
/// ->
/// ```
/// destroy_value %1
/// destroy_value %2
/// ```
///
/// The benefit of this transformation is that the forwarding instruction can be removed.
///
private func tryRemoveForwardingOperandInstruction(_ context: SimplifyContext) {
guard context.preserveDebugInfo ? destroyedValue.uses.isSingleUse
: destroyedValue.uses.ignoreDebugUses.isSingleUse
else {
return
}
let destroyedInst: Instruction
switch destroyedValue {
case is StructInst,
is EnumInst:
if destroyedValue.type.nominal!.valueTypeDestructor != nil {
// Moving the destroy to a non-copyable struct/enum's operands would drop the deinit call!
return
}
destroyedInst = destroyedValue as! SingleValueInstruction
// Handle various "forwarding" instructions that simply pass through values
// without performing operations that would affect destruction semantics.
//
// We are intentionally _not_ handling `unchecked_enum_data`, because that would not necessarily be
// a simplification, because destroying the whole enum is more effort than to destroy an enum payload.
// We are also not handling `destructure_struct` and `destructure_tuple`. That would end up in
// an infinite simplification loop in MandatoryPerformanceOptimizations because there we "split" such
// destroys again when de-virtualizing deinits of non-copyable types.
//
case is TupleInst,
is RefToBridgeObjectInst,
is ConvertFunctionInst,
is ThinToThickFunctionInst,
is UpcastInst,
is UncheckedRefCastInst,
is UnconditionalCheckedCastInst,
is BridgeObjectToRefInst,
is InitExistentialRefInst,
is OpenExistentialRefInst:
destroyedInst = destroyedValue as! SingleValueInstruction
case let arg as Argument:
tryRemovePhiArgument(arg, context)
return
default:
return
}
let builder = Builder(before: self, context)
for op in destroyedInst.definedOperands where op.value.ownership == .owned {
builder.createDestroyValue(operand: op.value)
}
// Users include `debug_value` instructions and this `destroy_value`
context.erase(instructionIncludingAllUsers: destroyedInst)
}
/// Handles the optimization of `destroy_value` instructions for phi arguments.
/// This is a more complex case where the destroyed value comes from different predecessors
/// via a phi argument. The optimization moves the `destroy_value` to each predecessor block.
///
/// ```
/// bb1:
/// br bb3(%0)
/// bb2:
/// br bb3(%1)
/// bb3(%3 : @owned T):
/// ... // no deinit-barriers
/// destroy_value %3 // the only use of %3
/// ```
/// ->
/// ```
/// bb1:
/// destroy_value %0
/// br bb3
/// bb2:
/// destroy_value %1
/// br bb3
/// bb3:
/// ...
/// ```
///
private func tryRemovePhiArgument(_ arg: Argument, _ context: SimplifyContext) {
guard let phi = Phi(arg),
arg.parentBlock == parentBlock,
!isDeinitBarrierInBlock(before: self, context)
else {
return
}
for incomingOp in phi.incomingOperands {
let oldBranch = incomingOp.instruction as! BranchInst
let builder = Builder(before: oldBranch, context)
builder.createDestroyValue(operand: incomingOp.value)
builder.createBranch(to: parentBlock, arguments: oldBranch.arguments(excluding: incomingOp))
context.erase(instruction: oldBranch)
}
// Users of `arg` include `debug_value` instructions and this `destroy_value`
context.erase(instructions: arg.uses.users)
arg.parentBlock.eraseArgument(at: arg.index, context)
}
}
private func isDeinitBarrierInBlock(before instruction: Instruction, _ context: SimplifyContext) -> Bool {
return ReverseInstructionList(first: instruction.previous).contains(where: {
$0.isDeinitBarrier(context.calleeAnalysis)
})
}
private extension BranchInst {
func arguments(excluding excludeOp: Operand) -> [Value] {
return Array(operands.filter{ $0 != excludeOp }.values)
}
}