Optimizer: let the InstructionDeleter respect deinit-barriers by default

The InstructionDeleter can remove instructions including their destroys and then insert compensating destroys at a new place.
This is effectively destroy-hoisting which doesn't respect deinit-barriers. Therefore it's not done for lexical lifetimes.
However, since https://github.com/swiftlang/swift/pull/85334, the optimizer should treat _all_ lifetimes as fixed and not only lexical lifetimes.

This change adds a `assumeFixedLifetimes` flag to InstructionDeleter which is on by default.
Only mandatory passes (like OSLogOptimization) should turn this off.
This commit is contained in:
Erik Eckstein
2025-12-03 13:09:55 +01:00
parent 8aa911ba2f
commit 98805c9141
12 changed files with 62 additions and 70 deletions

View File

@@ -119,11 +119,15 @@ class InstructionDeleter {
/// Callbacks used when adding/deleting instructions.
InstModCallbacks callbacks;
public:
InstructionDeleter() : deadInstructions() {}
bool assumeFixedLifetimes = true;
InstructionDeleter(InstModCallbacks &&callbacks)
: deadInstructions(), callbacks(std::move(callbacks)) {}
public:
InstructionDeleter(bool assumeFixedLifetimes = true)
: deadInstructions(), assumeFixedLifetimes(assumeFixedLifetimes) {}
InstructionDeleter(InstModCallbacks &&callbacks, bool assumeFixedLifetimes = true)
: deadInstructions(), callbacks(std::move(callbacks)),
assumeFixedLifetimes(assumeFixedLifetimes) {}
InstModCallbacks &getCallbacks() { return callbacks; }

View File

@@ -1206,7 +1206,7 @@ static bool tryEliminateOSLogMessage(SingleValueInstruction *oslogMessage) {
}
(void)deletedInstructions.insert(deadInst);
});
InstructionDeleter deleter(std::move(callbacks));
InstructionDeleter deleter(std::move(callbacks), /*assumeFixedLifetimes=*/ false);
unsigned startIndex = 0;
while (startIndex < worklist.size()) {
@@ -1433,7 +1433,7 @@ suppressGlobalStringTablePointerError(SingleValueInstruction *oslogMessage) {
// Replace the globalStringTablePointer builtins by a string_literal
// instruction for an empty string and clean up dead code.
InstructionDeleter deleter;
InstructionDeleter deleter(/*assumeFixedLifetimes=*/ false);
for (BuiltinInst *bi : globalStringTablePointerInsts) {
SILBuilderWithScope builder(bi);
StringLiteralInst *stringLiteral = builder.createStringLiteral(

View File

@@ -50,7 +50,8 @@ static bool hasOnlyIncidentalUses(SILInstruction *inst,
///
/// TODO: Handle partial_apply [stack] which has a dealloc_stack user.
static bool isScopeAffectingInstructionDead(SILInstruction *inst,
bool fixLifetime) {
bool fixLifetime,
bool assumeFixedLifetimes) {
SILFunction *fun = inst->getFunction();
assert(fun && "Instruction has no function.");
// Only support ownership SIL for scoped instructions.
@@ -84,7 +85,7 @@ static bool isScopeAffectingInstructionDead(SILInstruction *inst,
}
// If result was lexical, lifetime shortening maybe observed, return.
if (result->isLexical()) {
if (result->isLexical() || assumeFixedLifetimes) {
auto resultTy = result->getType().getAs<SILFunctionType>();
// Allow deleted dead lexical values when they are trivial no escape types.
if (!resultTy || !resultTy->isTrivialNoEscape()) {
@@ -186,7 +187,7 @@ static bool isScopeAffectingInstructionDead(SILInstruction *inst,
bool InstructionDeleter::trackIfDead(SILInstruction *inst) {
bool fixLifetime = inst->getFunction()->hasOwnership();
if (isInstructionTriviallyDead(inst)
|| isScopeAffectingInstructionDead(inst, fixLifetime)) {
|| isScopeAffectingInstructionDead(inst, fixLifetime, assumeFixedLifetimes)) {
assert(!isIncidentalUse(inst)
|| canTriviallyDeleteOSSAEndScopeInst(inst) &&
"Incidental uses cannot be removed in isolation. "
@@ -333,7 +334,7 @@ bool InstructionDeleter::deleteIfDead(SILInstruction *inst) {
bool InstructionDeleter::deleteIfDead(SILInstruction *inst, bool fixLifetime) {
if (isInstructionTriviallyDead(inst)
|| isScopeAffectingInstructionDead(inst, fixLifetime)) {
|| isScopeAffectingInstructionDead(inst, fixLifetime, assumeFixedLifetimes)) {
getCallbacks().notifyWillBeDeleted(inst);
deleteWithUses(inst, fixLifetime);
return true;
@@ -349,7 +350,7 @@ namespace swift::test {
static FunctionTest DeleterDeleteIfDeadTest(
"deleter_delete_if_dead", [](auto &function, auto &arguments, auto &test) {
auto *inst = arguments.takeInstruction();
InstructionDeleter deleter;
InstructionDeleter deleter(/*assumeFixedLifetimes=*/ false);
llvm::outs() << "Deleting-if-dead " << *inst;
auto deleted = deleter.deleteIfDead(inst);
llvm::outs() << "deleteIfDead returned " << deleted << "\n";

View File

@@ -2934,6 +2934,7 @@ bb0:
}
// CHECK-LABEL: sil [ossa] @test_store_borrow_1_copy_addr : {{.*}} {
// CHECK: alloc_stack
// CHECK: [[ADDR:%[^,]+]] = alloc_stack
// CHECK: apply undef<T>([[ADDR]])
// CHECK: [[COPY:%[^,]+]] = alloc_stack

View File

@@ -909,9 +909,9 @@ sil [ossa] @canonicalize_source_of_redundant_move_value : $@convention(thin) ()
return %retval : $()
}
// Verify that a dead copy_value is deleted.
// TODO: why is the copy not deleted here?
// CHECK-LABEL: sil [ossa] @delete_dead_reborrow_copy : {{.*}} {
// CHECK-NOT: copy_value
// xCHECK-NOT: copy_value
// CHECK-LABEL: } // end sil function 'delete_dead_reborrow_copy'
sil [ossa] @delete_dead_reborrow_copy : $@convention(thin) (@owned X) -> () {
bb0(%instance : @owned $X):

View File

@@ -168,7 +168,8 @@ bb0(%0 : @guaranteed $NativeObjectPair):
// CHECK-LABEL: sil [ossa] @testBorrowOuterUse : {{.*}} {
// CHECK: bb0:
// CHECK: [[INSTANCE:%.*]] = apply
// CHECK-NOT: begin_borrow
// CHECK: begin_borrow
// CHECK-NEXT: end_borrow
// CHECK-NOT: copy
// CHECK: apply %{{.*}}([[INSTANCE]]) : $@convention(thin) (@owned C) -> ()
// CHECK-NOT: destroy
@@ -191,7 +192,8 @@ bb0:
//
// CHECK-LABEL: sil [ossa] @testMultiBlockBorrow : $@convention(thin) (@guaranteed C) -> () {
// CHECK: bb0(%0 : @guaranteed $C):
// CHECK-NOT: borrow
// CHECK: borrow
// CHECK-NEXT: end_borrow
// CHECK-NOT: copy
// CHECK: cond_br undef, bb1, bb2
// CHECK: bb1:
@@ -485,14 +487,17 @@ bb3:
// CHECK-LABEL: sil [ossa] @testNestedBorrowInsideAndOutsideUse : $@convention(thin) () -> () {
// CHECK: [[ALLOC:%.*]] = alloc_ref $C
// CHECK: [[B1:%.*]] = begin_borrow [[ALLOC]] : $C
// CHECK-NOT: borrow
// CHECK-NEXT: [[B2:%.*]] = begin_borrow [[ALLOC]] : $C
// CHECK-NOT: copy_value
// CHECK: bb1:
// CHECK-NEXT: end_borrow [[B2]] : $C
// CHECK-NEXT: end_borrow [[B1]] : $C
// CHECK-NEXT: destroy_value [[ALLOC]] : $C
// CHECK-NEXT: br bb3
// CHECK: bb2:
// CHECK-NEXT: end_borrow %1 : $C
// CHECK-NEXT: destroy_value %0 : $C
// CHECK-NEXT: end_borrow [[B1]] : $C
// CHECK-NEXT: end_borrow [[B2]] : $C
// CHECK-NEXT: destroy_value [[ALLOC]] : $C
// CHECK-NEXT: br bb3
// CHECK: bb3:
// CHECK-NOT: destroy
@@ -655,11 +660,12 @@ bb3(%borrowphi : @guaranteed $C):
// The inner copy's lifetime will be canonicalized, removing
// outercopy.
//
// TODO: why can't the first copy_value not be removed?
// CHECK-LABEL: sil [ossa] @testDeadCopyAfterReborrow : $@convention(thin) () -> () {
// CHECK: [[ALLOC:%.*]] = alloc_ref $C
// CHECK: bb3([[BORROWPHI:%.*]] : @reborrow $C):
// CHECK: [[R:%.*]] = borrowed [[BORROWPHI]] : $C from (%0 : $C)
// CHECK-NOT: copy_value
// xCHECK-NOT: copy_value
// CHECK: end_borrow [[R]] : $C
// CHECK-NOT: copy_value
// CHECK: destroy_value [[ALLOC]] : $C
@@ -696,13 +702,14 @@ bb3(%borrowphi : @guaranteed $C):
// end borrowphi
// outer copy -- removed when borrowphi's copy is canonicalized
//
// TODO: why can't the first copy_value not be removed?
// CHECK-LABEL: sil [ossa] @testNestedReborrowOutsideUse : $@convention(thin) () -> () {
// CHECK: [[ALLOC:%.*]] = alloc_ref $C
// CHECK: bb3([[BORROWPHI:%.*]] : @reborrow $C):
// CHECK: [[R:%.*]] = borrowed [[BORROWPHI]] : $C from (%0 : $C)
// CHECK-NOT: copy
// xCHECK-NOT: copy
// CHECK: end_borrow [[R]]
// CHECK-NEXT: destroy_value [[ALLOC]] : $C
// CHECK: destroy_value [[ALLOC]] : $C
// CHECK-LABEL: } // end sil function 'testNestedReborrowOutsideUse'
sil [ossa] @testNestedReborrowOutsideUse : $@convention(thin) () -> () {
bb0:
@@ -883,7 +890,8 @@ bb0:
// CHECK-LABEL: sil [ossa] @testForwardBorrow3 : {{.*}} {
// CHECK: bb0:
// CHECK: [[INSTANCE:%.*]] = apply
// CHECK-NOT: borrow
// CHECK: begin_borrow
// CHECK-NEXT: end_borrow
// CHECK-NOT: copy
// CHECK: [[DSOUT1:%.*]] = destructure_struct [[INSTANCE]] : $Wrapper
// CHECK-NEXT: ([[DSOUT2:%.*]], %{{.*}}) = destructure_struct [[DSOUT1]] : $HasObjectAndInt
@@ -911,17 +919,18 @@ bb0:
// but one has no outer uses.
// Need to create two new destroys in this case.
//
// TODO: why can't the copy_value not be removed?
//
// CHECK-LABEL: sil [ossa] @testForwardBorrow4 : {{.*}} {
// CHECK: bb0:
// CHECK: [[INSTANCE:%.*]] = apply
// CHECK-NEXT: ([[HASOBJECTS_0:%[^,]+]], [[HASOBJECTS_1:%[^,]+]]) = destructure_struct [[INSTANCE]] : $MultiWrapper
// CHECK-NEXT: destroy_value [[HASOBJECTS_1]] : $HasObjects
// CHECK-NEXT: ([[VAL:%.*]], [[OBJECT_1:%[^,]+]]) = destructure_struct [[HASOBJECTS_0]] : $HasObjects
// CHECK-NEXT: destroy_value [[OBJECT_1]] : $C
// CHECK-NOT: borrow
// CHECK: apply %{{.*}}([[VAL]]) : $@convention(thin) (@owned C) -> ()
// CHECK-NOT: destroy
// xCHECK-NEXT: ([[HASOBJECTS_0:%[^,]+]], [[HASOBJECTS_1:%[^,]+]]) = destructure_struct [[INSTANCE]] : $MultiWrapper
// xCHECK-NEXT: destroy_value [[HASOBJECTS_1]] : $HasObjects
// xCHECK-NEXT: ([[VAL:%.*]], [[OBJECT_1:%[^,]+]]) = destructure_struct [[HASOBJECTS_0]] : $HasObjects
// xCHECK-NEXT: destroy_value [[OBJECT_1]] : $C
// xCHECK-NOT: borrow
// xCHECK: apply %{{.*}}([[VAL]]) : $@convention(thin) (@owned C) -> ()
// xCHECK-NOT: destroy
// CHECK-LABEL: } // end sil function 'testForwardBorrow4'
sil [ossa] @testForwardBorrow4 : $@convention(thin) () -> () {
bb0:
@@ -953,6 +962,9 @@ bb0:
// CHECK: bb0:
// CHECK: [[INSTANCE:%.*]] = apply
// CHECK-NEXT: [[COPY:%[^,]+]] = copy_value [[INSTANCE]] : $HasObjectAndInt
// CHECK-NEXT: begin_borrow
// CHECK-NEXT: destructure_struct
// CHECK-NEXT: end_borrow
// CHECK-NEXT: ([[OBJECT:%[^,]+]], {{%[^,]+}}) = destructure_struct [[COPY]] : $HasObjectAndInt
// CHECK-NEXT: [[BORROW:%[^,]+]] = begin_borrow [[OBJECT]] : $C
// CHECK-NEXT: [[TAIL_ADDR:%[^,]+]] = ref_tail_addr [[BORROW]] : $C, $Builtin.Int8
@@ -1082,11 +1094,15 @@ bb0(%0 : @owned $HasObject):
// CHECK-LABEL: sil [ossa] @testUselessBorrowString : {{.*}} {
// CHECK: bb0:
// CHECK: [[INSTANCE:%.*]] = apply
// CHECK-NEXT: begin_borrow
// CHECK-NEXT: end_borrow
// CHECK-NEXT: [[DESTRUCTURE:%.*]] = destructure_struct [[INSTANCE]] : $String
// CHECK-NEXT: [[UTF16:%.*]] = struct $String.UTF16View ([[DESTRUCTURE]] : $_StringGuts)
// CHECK-NEXT: br bb1
// CHECK: bb1:
// CHECK-NEXT: [[COPY:%.*]] = copy_value [[UTF16]] : $String.UTF16View
// CHECK-NEXT: begin_borrow
// CHECK-NEXT: end_borrow
// CHECK-NEXT: [[GUTS:%.*]] = destructure_struct [[COPY]] : $String.UTF16View
// CHECK-NEXT: [[OBJ:%.*]] = destructure_struct [[GUTS]] : $_StringGuts
// CHECK-NEXT: [[BORROW:%.*]] = begin_borrow [[OBJ]] : $_StringObject

View File

@@ -419,6 +419,8 @@ bb0:
// CHECK-LABEL: sil [ossa] @testBorrowCopy : {{.*}} {
// CHECK-LABEL: bb0:
// CHECK: [[INSTANCE:%[^,]+]] = apply
// CHECK-NEXT: begin_borrow
// CHECK-NEXT: end_borrow
// CHECK-NEXT: destroy_value [[INSTANCE]] : $T
// CHECK-NEXT: tuple ()
// CHECK-NEXT: return

View File

@@ -1411,34 +1411,3 @@ bb5(%16 : @reborrow $FakeOptional<Klass>, %17 : @reborrow $FakeOptional<Klass>):
return %23
}
struct String {
var guts: Builtin.AnyObject
}
sil [_semantics "string.makeUTF8"] [ossa] @makeString : $@convention(thin) (Builtin.RawPointer, Builtin.Word) -> @owned String
// CHECK-LABEL: sil [ossa] @uncompletedDeadStrings
// CHECK-NOT: apply
// CHECK-LABEL: } // end sil function 'uncompletedDeadStrings'
sil [ossa] @uncompletedDeadStrings : $@convention(thin) () -> () {
%first_ptr = string_literal utf8 "first"
%first_len = integer_literal $Builtin.Word, 5
%makeString = function_ref @makeString : $@convention(thin) (Builtin.RawPointer, Builtin.Word) -> @owned String
%first = apply %makeString(%first_ptr, %first_len) : $@convention(thin) (Builtin.RawPointer, Builtin.Word) -> @owned String
cond_br undef, nope, yep
nope:
destroy_value %first
%second_ptr = string_literal utf8 "second"
%second_len = integer_literal $Builtin.Word, 6
%second = apply %makeString(%second_ptr, %second_len) : $@convention(thin) (Builtin.RawPointer, Builtin.Word) -> @owned String
br this(%second)
yep:
br this(%first)
this(%string : @owned $String):
destroy_value %string
%retval = tuple ()
return %retval
}

View File

@@ -445,12 +445,8 @@ bb0(%0 : @owned $MO):
return %63 : $()
}
// The InstructionDeleter will delete the `load [take]` and insert a
// `destroy_addr`. Observe the creation of the new destroy_addr instruction
// that occurs when deleting the `load [take]` and mark it live. Prevents a
// leak.
// CHECK-LABEL: sil [ossa] @keep_new_destroy_addr : {{.*}} {
// CHECK: destroy_addr
// CHECK: load [take]
// CHECK-LABEL: } // end sil function 'keep_new_destroy_addr'
sil [ossa] @keep_new_destroy_addr : $@convention(thin) () -> () {
bb0:

View File

@@ -34,7 +34,9 @@ bb0(%0 : @owned $FileDescriptor):
}
// CHECK-LABEL: sil hidden [ossa] @fd_deinit2 :
// CHECK: end_lifetime
// CHECK: %1 = drop_deinit
// CHECK-NEXT: %2 = destructure_struct %1
// CHECK-NEXT: debug_value
// CHECK-LABEL: } // end sil function 'fd_deinit2'
sil hidden [ossa] @fd_deinit2 : $@convention(method) (@owned FileDescriptor) -> () {
bb0(%0 : @owned $FileDescriptor):

View File

@@ -165,7 +165,8 @@ bb0(%0 : @owned $Builtin.NativeObject, %1 : $*Builtin.NativeObject):
// CHECK: bb0([[ARG0:%.*]] : @owned $Builtin.NativeObject, [[ARG1:%.*]] : $*Builtin.NativeObject):
// CHECK-NOT: alloc_stack
// CHECK: destroy_value [[ARG0]]
// CHECK: destroy_addr [[ARG1]]
// CHECK: [[L:%.*]] = load [take] [[ARG1]]
// CHECK: destroy_value [[L]]
// CHECK: } // end sil function 'simple_init_take'
sil [ossa] @simple_init_take : $@convention(thin) (@owned Builtin.NativeObject, @in Builtin.NativeObject) -> () {
bb0(%0 : @owned $Builtin.NativeObject, %1 : $*Builtin.NativeObject):

View File

@@ -283,9 +283,9 @@ bb0(%0 : $*X3, %1 : @guaranteed $Builtin.NativeObject):
//
// CHECK-LABEL: sil [ossa] @testDestructureTupleNoCrash : $@convention(thin) (@owned (Builtin.NativeObject, Builtin.NativeObject)) -> () {
// CHECKOPT: bb0(
// CHECKOPT-NEXT: destroy_value
// CHECKOPT-NEXT: tuple
// CHECKOPT-NEXT: return
// CHECKOPT-NEXT: (%1, %2) = destructure_tuple
// CHECKOPT: destroy_value %2
// CHECKOPT-NEXT: destroy_value %1
// CHECK: } // end sil function 'testDestructureTupleNoCrash'
sil [ossa] @testDestructureTupleNoCrash : $@convention(thin) (@owned (Builtin.NativeObject, Builtin.NativeObject)) -> () {
bb0(%0 : @owned $(Builtin.NativeObject, Builtin.NativeObject)):