Files
swift-mirror/lib/SILOptimizer/Utils/InstructionDeleter.cpp
Erik Eckstein 98805c9141 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.
2025-12-03 15:53:56 +01:00

473 lines
18 KiB
C++

//===--- InstructionDeleter.cpp - InstructionDeleter utility --------------===//
//
// This source file is part of the Swift.org open source project
//
// Copyright (c) 2014 - 2021 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
//
//===----------------------------------------------------------------------===//
#include "swift/SILOptimizer/Utils/InstructionDeleter.h"
#include "swift/Basic/Assertions.h"
#include "swift/SIL/SILFunction.h"
#include "swift/SIL/Test.h"
#include "swift/SILOptimizer/Utils/ConstExpr.h"
#include "swift/SILOptimizer/Utils/DebugOptUtils.h"
#include "swift/SILOptimizer/Utils/InstOptUtils.h"
using namespace swift;
static bool hasOnlyIncidentalUses(SILInstruction *inst,
bool preserveDebugInfo = false) {
for (SILValue result : inst->getResults()) {
for (Operand *use : result->getUses()) {
SILInstruction *user = use->getUser();
if (!isIncidentalUse(user))
return false;
if (preserveDebugInfo && user->isDebugInstruction())
return false;
}
}
return true;
}
/// A scope-affecting instruction is an instruction which may end the scope of
/// its operand or may produce scoped results that require cleaning up. E.g.
/// begin_borrow, begin_access, copy_value, a call that produces a owned value
/// are scoped instructions. The scope of the results of the first two
/// instructions end with an end_borrow/access instruction, while those of the
/// latter two end with a consuming operation like destroy_value instruction.
/// These instruction may also end the scope of its operand e.g. a call could
/// consume owned arguments thereby ending its scope. Dead-code eliminating a
/// scope-affecting instruction requires fixing the lifetime of the non-trivial
/// operands of the instruction and requires cleaning up the end-of-scope uses
/// of non-trivial results.
///
/// \param inst instruction that checked for liveness.
///
/// TODO: Handle partial_apply [stack] which has a dealloc_stack user.
static bool isScopeAffectingInstructionDead(SILInstruction *inst,
bool fixLifetime,
bool assumeFixedLifetimes) {
SILFunction *fun = inst->getFunction();
assert(fun && "Instruction has no function.");
// Only support ownership SIL for scoped instructions.
if (!fun->hasOwnership()) {
return false;
}
// If the instruction has any use other than end of scope use or destroy_value
// use, bail out.
if (!hasOnlyEndOfScopeOrEndOfLifetimeUses(inst)) {
return false;
}
// Don't delete dead drop_deinit instruction. They are a marker to eliminate
// user-defined deinit and we do not want to lose it.
if (isa<DropDeinitInst>(inst)) {
return false;
}
for (auto result : inst->getResults()) {
// If inst has any owned move-only value as a result, deleting it may
// shorten that value's lifetime which is illegal according to language
// rules.
//
// In particular, this check is needed before returning true when
// getSingleValueCopyOrCast returns true. That function returns true for
// move_value instructions. And `move_value %moveOnlyValue` must not be
// deleted.
if (result->getType().isMoveOnly() &&
result->getOwnershipKind() == OwnershipKind::Owned) {
return false;
}
// If result was lexical, lifetime shortening maybe observed, return.
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()) {
return false;
}
}
}
// If inst is a copy or beginning of scope, inst is dead, since we know that
// it is used only in a destroy_value or end-of-scope instruction.
if (getSingleValueCopyOrCast(inst))
return true;
switch (inst->getKind()) {
case SILInstructionKind::AllocStackInst: {
// An alloc_stack only used by dealloc_stack is dead.
return
fun->getEffectiveOptimizationMode() > OptimizationMode::NoOptimization
|| !cast<AllocStackInst>(inst)->getVarInfo();
}
case SILInstructionKind::LoadBorrowInst: {
// A load_borrow only used in an end_borrow is dead.
return true;
}
case SILInstructionKind::LoadInst: {
LoadOwnershipQualifier loadOwnershipQual =
cast<LoadInst>(inst)->getOwnershipQualifier();
// If the load creates a copy, it is dead, since we know that if at all it
// is used, it is only in a destroy_value instruction.
return (loadOwnershipQual == LoadOwnershipQualifier::Take ||
loadOwnershipQual == LoadOwnershipQualifier::Copy ||
loadOwnershipQual == LoadOwnershipQualifier::Trivial);
}
case SILInstructionKind::PartialApplyInst: {
bool onlyTrivialArgs = true;
for (auto &arg : cast<PartialApplyInst>(inst)->getArgumentOperands()) {
auto argTy = arg.get()->getType();
// Non-stack partial apply captures that are passed by address are always
// captured at +1 by the closure contrext, regardless of the calling
// convention.
//
// TODO: When on-stack partial applies are also handled, then their +0
// address arguments can be ignored.
//
// FIXME: Even with fixLifetimes enabled, InstructionDeleter does not know
// how to cleanup arguments captured by address. This should be as simple
// as inserting a destroy_addr. But the analagous code in
// tryDeleteDeadClosure() and keepArgsOfPartialApplyAlive() mysteriously
// creates new alloc_stack's and invalidates stack nesting. So we
// conservatively bail-out until we understand why that hack exists.
if (argTy.isAddress())
return false;
onlyTrivialArgs &= argTy.isTrivial(*fun);
}
// Partial applies that are only used in destroys cannot have any effect on
// the program state, provided the values they capture are explicitly
// destroyed, which only happens when fixLifetime is true.
return onlyTrivialArgs || fixLifetime;
}
case SILInstructionKind::StructInst:
case SILInstructionKind::EnumInst:
case SILInstructionKind::TupleInst:
case SILInstructionKind::ConvertFunctionInst:
case SILInstructionKind::DestructureStructInst:
case SILInstructionKind::DestructureTupleInst: {
// All these ownership forwarding instructions that are only used in
// destroys are dead provided the values they consume are destroyed
// explicitly.
return true;
}
case SILInstructionKind::ApplyInst: {
// The following property holds for constant-evaluable functions that do
// not take arguments of generic type:
// 1. they do not create objects having deinitializers with global
// side effects, as they can only create objects consisting of trivial
// values, (non-generic) arrays and strings.
// 2. they do not use global variables or call arbitrary functions with
// side effects.
// The above two properties imply that a value returned by a constant
// evaluable function does not have a deinitializer with global side
// effects. Therefore, the deinitializer can be sinked.
//
// A generic, read-only constant evaluable call only reads and/or
// destroys its (non-generic) parameters. It therefore cannot have any
// side effects (note that parameters being non-generic have value
// semantics). Therefore, the constant evaluable call can be removed
// provided the parameter lifetimes are handled correctly, which is taken
// care of by the function: \c deleteInstruction.
FullApplySite applySite(cast<ApplyInst>(inst));
return isReadOnlyConstantEvaluableCall(applySite);
}
default: {
return false;
}
}
}
bool InstructionDeleter::trackIfDead(SILInstruction *inst) {
bool fixLifetime = inst->getFunction()->hasOwnership();
if (isInstructionTriviallyDead(inst)
|| isScopeAffectingInstructionDead(inst, fixLifetime, assumeFixedLifetimes)) {
assert(!isIncidentalUse(inst)
|| canTriviallyDeleteOSSAEndScopeInst(inst) &&
"Incidental uses cannot be removed in isolation. "
"They would be removed iff the operand is dead");
getCallbacks().notifyWillBeDeleted(inst);
deadInstructions.insert(inst);
return true;
}
return false;
}
void InstructionDeleter::forceTrackAsDead(SILInstruction *inst) {
bool preserveDebugInfo = inst->getFunction()->getEffectiveOptimizationMode()
<= OptimizationMode::NoOptimization;
assert(hasOnlyIncidentalUses(inst, preserveDebugInfo));
getCallbacks().notifyWillBeDeleted(inst);
deadInstructions.insert(inst);
}
/// Force-delete \p inst and all its uses.
///
/// \p fixLifetimes causes new destroys to be inserted after dropping
/// operands.
///
/// \p forceDeleteUsers allows deleting an instruction with non-incidental,
/// non-destroy uses, such as a store.
///
/// Does not call callbacks.notifyWillBeDeleted for \p inst. But does
/// for any other instructions that become dead as a result.
///
/// Carefully orchestrated steps for deleting an instruction with its uses:
///
/// Recursively gather the instruction's uses into the toDeleteInsts set and
/// dropping the operand for each use traversed.
///
/// For the remaining operands, insert destroys for consuming operands and track
/// newly dead operand definitions.
///
/// Finally, erase the instruction.
void InstructionDeleter::deleteWithUses(SILInstruction *inst, bool fixLifetimes,
bool forceDeleteUsers) {
// Cannot fix operand lifetimes in non-ownership SIL.
assert(!fixLifetimes || inst->getFunction()->hasOwnership());
// Recursively visit all uses while growing toDeleteInsts in def-use order and
// dropping dead operands.
SmallVector<SILInstruction *, 4> toDeleteInsts;
SmallVector<Operand *, 4> toDropUses;
toDeleteInsts.push_back(inst);
swift::salvageDebugInfo(inst);
for (unsigned idx = 0; idx < toDeleteInsts.size(); ++idx) {
for (SILValue result : toDeleteInsts[idx]->getResults()) {
// Temporary use vector to avoid iterator invalidation.
auto uses = llvm::to_vector<4>(result->getUses());
for (Operand *use : uses) {
SILInstruction *user = use->getUser();
assert(forceDeleteUsers || isIncidentalUse(user)
|| isa<DestroyValueInst>(user) || isa<DeallocStackInst>(user));
assert(!isa<BranchInst>(user) && "can't delete phis");
toDeleteInsts.push_back(user);
toDropUses.push_back(use);
swift::salvageDebugInfo(user);
}
}
}
// Drop all after salvage debug info has been run.
for (auto *use : toDropUses)
use->drop();
// Process the remaining operands. Insert destroys for consuming
// operands. Track newly dead operand values. Instructions with multiple dead
// operands may occur in toDeleteInsts multiple times.
for (auto *inst : toDeleteInsts) {
if (inst->isDeleted())
continue;
for (Operand &operand : inst->getAllOperands()) {
SILValue operandValue = operand.get();
// Check for dead operands, which are dropped above.
if (!operandValue)
continue;
if (fixLifetimes) {
LoadInst *li = nullptr;
if (operand.isConsuming()) {
if (isa<DropDeinitInst>(operandValue)) {
SILBuilderWithScope builder(inst);
auto *eli = builder.createEndLifetime(inst->getLoc(), operandValue);
getCallbacks().createdNewInst(eli);
} else {
SILBuilderWithScope builder(inst);
auto *dvi =
builder.createDestroyValue(inst->getLoc(), operandValue);
getCallbacks().createdNewInst(dvi);
}
} else if ((li = dyn_cast<LoadInst>(inst)) &&
li->getOwnershipQualifier() ==
LoadOwnershipQualifier::Take) {
SILBuilderWithScope builder(inst);
auto *dai = builder.createDestroyAddr(inst->getLoc(), operandValue);
getCallbacks().createdNewInst(dai);
}
}
auto *operDef = operandValue->getDefiningInstruction();
operand.drop();
if (operDef) {
trackIfDead(operDef);
}
}
inst->dropNonOperandReferences();
deadInstructions.remove(inst);
getCallbacks().deleteInst(inst, false /*notify when deleting*/);
}
}
void InstructionDeleter::cleanupDeadInstructions() {
while (!deadInstructions.empty()) {
SmallVector<SILInstruction *, 8> currentDeadInsts(deadInstructions.begin(),
deadInstructions.end());
// Though deadInstructions is cleared here, calls to deleteInstruction may
// append to deadInstructions. So we need to iterate until this it is empty.
deadInstructions.clear();
for (SILInstruction *deadInst : currentDeadInsts) {
if (deadInst->isDeleted())
continue;
// deadInst will not have been deleted in the previous iterations,
// because, by definition, deleteInstruction will only delete an earlier
// instruction and its incidental/destroy uses. The former cannot be
// deadInst as deadInstructions is a set vector, and the latter cannot be
// in deadInstructions as they are incidental uses which are never added
// to deadInstructions.
deleteWithUses(deadInst,
/*fixLifetimes*/ deadInst->getFunction()->hasOwnership());
}
}
}
bool InstructionDeleter::deleteIfDead(SILInstruction *inst) {
bool fixLifetime = inst->getFunction()->hasOwnership();
return deleteIfDead(inst, fixLifetime);
}
bool InstructionDeleter::deleteIfDead(SILInstruction *inst, bool fixLifetime) {
if (isInstructionTriviallyDead(inst)
|| isScopeAffectingInstructionDead(inst, fixLifetime, assumeFixedLifetimes)) {
getCallbacks().notifyWillBeDeleted(inst);
deleteWithUses(inst, fixLifetime);
return true;
}
return false;
}
namespace swift::test {
// Arguments:
// - instruction: the instruction to delete
// Dumps:
// - the function
static FunctionTest DeleterDeleteIfDeadTest(
"deleter_delete_if_dead", [](auto &function, auto &arguments, auto &test) {
auto *inst = arguments.takeInstruction();
InstructionDeleter deleter(/*assumeFixedLifetimes=*/ false);
llvm::outs() << "Deleting-if-dead " << *inst;
auto deleted = deleter.deleteIfDead(inst);
llvm::outs() << "deleteIfDead returned " << deleted << "\n";
function.print(llvm::outs());
});
// Arguments:
// - instruction: the instruction to delete
// Dumps:
// - the function
static FunctionTest DeleterTrackIfDeadTest(
"deleter_track_if_dead", [](auto &function, auto &arguments, auto &test) {
auto *inst = arguments.takeInstruction();
InstructionDeleter deleter;
llvm::outs() << "Tracking " << *inst;
deleter.trackIfDead(inst);
deleter.cleanupDeadInstructions();
function.print(llvm::outs());
});
} // namespace swift::test
void InstructionDeleter::forceDeleteAndFixLifetimes(SILInstruction *inst) {
SILFunction *fun = inst->getFunction();
bool preserveDebugInfo =
fun->getEffectiveOptimizationMode() <= OptimizationMode::NoOptimization;
assert(hasOnlyIncidentalUses(inst, preserveDebugInfo));
deleteWithUses(inst, /*fixLifetimes*/ fun->hasOwnership());
}
void InstructionDeleter::forceDelete(SILInstruction *inst) {
bool preserveDebugInfo = inst->getFunction()->getEffectiveOptimizationMode()
<= OptimizationMode::NoOptimization;
assert(hasOnlyIncidentalUses(inst, preserveDebugInfo));
deleteWithUses(inst, /*fixLifetimes*/ false);
}
void InstructionDeleter::recursivelyDeleteUsersIfDead(SILInstruction *inst) {
SmallVector<SILInstruction *, 8> users;
for (SILValue result : inst->getResults())
for (Operand *use : result->getUses())
users.push_back(use->getUser());
for (SILInstruction *user : users)
recursivelyDeleteUsersIfDead(user);
deleteIfDead(inst);
}
void InstructionDeleter::recursivelyForceDeleteUsersAndFixLifetimes(
SILInstruction *inst) {
for (SILValue result : inst->getResults()) {
while (!result->use_empty()) {
SILInstruction *user = result->use_begin()->getUser();
recursivelyForceDeleteUsersAndFixLifetimes(user);
}
}
if (isIncidentalUse(inst) || isa<DestroyValueInst>(inst)) {
forceDelete(inst);
return;
}
forceDeleteAndFixLifetimes(inst);
}
void swift::eliminateDeadInstruction(SILInstruction *inst,
InstModCallbacks callbacks) {
InstructionDeleter deleter(std::move(callbacks));
deleter.trackIfDead(inst);
deleter.cleanupDeadInstructions();
}
void swift::recursivelyDeleteTriviallyDeadInstructions(
ArrayRef<SILInstruction *> ia, bool force, InstModCallbacks callbacks) {
// Delete these instruction and others that become dead after it's deleted.
llvm::SmallPtrSet<SILInstruction *, 8> deadInsts;
for (auto *inst : ia) {
// If the instruction is not dead and force is false, do nothing.
if (force || isInstructionTriviallyDead(inst))
deadInsts.insert(inst);
}
llvm::SmallPtrSet<SILInstruction *, 8> nextInsts;
while (!deadInsts.empty()) {
for (auto inst : deadInsts) {
// Call the callback before we mutate the to be deleted instruction in any
// way.
callbacks.notifyWillBeDeleted(inst);
// Check if any of the operands will become dead as well.
MutableArrayRef<Operand> operands = inst->getAllOperands();
for (Operand &operand : operands) {
SILValue operandVal = operand.get();
if (!operandVal)
continue;
// Remove the reference from the instruction being deleted to this
// operand.
operand.drop();
// If the operand is an instruction that is only used by the instruction
// being deleted, delete it.
if (auto *operandValInst = operandVal->getDefiningInstruction())
if (!deadInsts.count(operandValInst) &&
isInstructionTriviallyDead(operandValInst))
nextInsts.insert(operandValInst);
}
// If we have a function ref inst, we need to especially drop its function
// argument so that it gets a proper ref decrement.
if (auto *fri = dyn_cast<FunctionRefBaseInst>(inst))
fri->dropReferencedFunction();
}
for (auto inst : deadInsts) {
// This will remove this instruction and all its uses.
eraseFromParentWithDebugInsts(inst, callbacks);
}
nextInsts.swap(deadInsts);
nextInsts.clear();
}
}