Files
swift-mirror/lib/SILOptimizer/SemanticARC/OwnedToGuaranteedPhiOpt.cpp
Nate Chandler c893c74bb4 [SemanticARCOpts] Fix miscompile at dead-ends.
With incomplete lifetimes, a value may have an "incomplete lifetime"--it
may not be destroyed on paths into dead-end regions.  When a value lacks
a destroy on a path into a dead-end region, it is effectively destroyed
at its availability boundary (e.g. an `unreachable` instruction).
Indeed, lifetime completion amounts to making such implicit destroys
explicit (with correct nesting).

`SemanticARCOpts`' `performGuaranteedCopyValueOptimization` attempts to
eliminate a `copy_value` of a guaranteed value.  To do so, it computes
the live range (`OwnershipLiveRange`) of the copy_value.  Among other
things, it checks that all destroys of the copy_value are within the
scope of all guaranteed bases of the copied value.  If any destroys are
_not_ within any of those scopes, the copy cannot be eliminated (because
the copy would have been originally live beyond the range which it would
be live in after copy elimination).

A value with an incomplete lifetime is implicitly destroyed at its
availability boundary.  So those implicit destroys along the
availability boundary must _also_ be within the scopes of those
guaranteed bases.

Previously, implicit destroys along availability boundaries were not
considered.  This resulted in invalid shortening of lifetimes--before
the transformation a value would be unconsumed up to its availability
boundary; after the transformation it would be consumed somewhere before
that.  For example:

```
sil [ossa] @f : $@convention(thin) (@owned Outer) -> () {
entry(%o : $*Outer):
  %o_borrow = begin_borrow %o : $Outer
  %i = struct_extract %o_borrow : $Outer, #Outer.inner
  %i_copy = copy_value %i : $Inner
  end_borrow %o_borrow : $Outer
  destroy_value %o : $Outer
  apply @g(%i_copy) : $@convention(thin) (@guaranteed Inner) -> ()
  unreachable // %i_copy implicitly destroyed
}
```

Here, `%i_copy` is implicitly destroyed at `unreachable` but `%i` is
consumed at the scope end of `%o_borrow`.  That means that an attempt to
RAUW `%i_copy` with `%i` would be invalid because uses of `%i_copy` may
be outside the live range of `%i`.

(Note that this happens not to occur in the above example because
there's a bailout in the case that there are no destroys, but perturbing
the example to include a cond_br on one branch of which the value is
destroyed results in the miscompile described above.)

Here, this is fixed by adding the instructions on the availability
boundary at which the value is implicitly destroyed to the live range.
Do this by adding the instructions on the availability boundary of each
def from which the live range is generated to the live range.

One wrinkle is that the OwnershipLiveRange utility is used in one place
by a utility when SIL is in an invalid state (phi uses have been
replaced with undef so values leaks on those paths).  Account for this
by manually fixing up the liveness instance passed to the lifetime
completion utility.

rdar://157291161
2025-08-08 18:06:09 -07:00

279 lines
12 KiB
C++

//===--- OwnedToGuaranteedPhiOpt.cpp --------------------------------------===//
//
// This source file is part of the Swift.org open source project
//
// Copyright (c) 2014 - 2020 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
//
//===----------------------------------------------------------------------===//
///
/// \file
///
/// Late optimization that eliminates webs of owned phi nodes.
///
//===----------------------------------------------------------------------===//
#include "Context.h"
#include "OwnershipPhiOperand.h"
#include "Transforms.h"
#include "swift/Basic/Assertions.h"
#include "swift/Basic/Defer.h"
#include "swift/Basic/STLExtras.h"
#include "swift/SILOptimizer/Utils/OwnershipOptUtils.h"
using namespace swift;
using namespace swift::semanticarc;
namespace {
using ConsumingOperandState = Context::ConsumingOperandState;
} // anonymous namespace
template <typename OperandRangeTy>
static bool canEliminatePhi(
OperandRangeTy optimizableIntroducerRange,
ArrayRef<OwnershipPhiOperand> incomingValueOperandList,
SmallVectorImpl<OwnedValueIntroducer> &ownedValueIntroducerAccumulator) {
for (auto incomingValueOperand : incomingValueOperandList) {
SILValue incomingValue = incomingValueOperand.getValue();
// Before we do anything, see if we have an incoming value with trivial
// ownership. This can occur in the case where we are working with enums due
// to trivial non-payloaded cases. Skip that.
if (incomingValue->getOwnershipKind() == OwnershipKind::None) {
continue;
}
// Then see if this is an introducer that we actually saw as able to be
// optimized if we could flip this joined live range.
//
// NOTE: If this linear search is too slow, we can change the multimap to
// sort the mapped to list by pointer instead of insertion order. In such a
// case, we could then bisect.
if (llvm::find(optimizableIntroducerRange,
incomingValueOperand.getOperand()) ==
optimizableIntroducerRange.end()) {
return false;
}
// Now that we know it is an owned value that we saw before, check for
// introducers of the owned value which are the copies that we may be able
// to eliminate. Since we do not look through joined live ranges, we must
// only have a single introducer. So look for that one and if not, bail.
auto singleIntroducer = getSingleOwnedValueIntroducer(incomingValue);
if (!singleIntroducer) {
return false;
}
// Then make sure that our owned value introducer is able to be converted to
// guaranteed and that we found it to have a LiveRange that we could have
// eliminated /if/ we were to get rid of this phi.
if (!singleIntroducer.isConvertableToGuaranteed()) {
return false;
}
// Otherwise, add the introducer to our result array.
ownedValueIntroducerAccumulator.push_back(singleIntroducer);
}
#ifndef NDEBUG
// Other parts of the pass ensure that we only add values to the list if their
// owned value introducer is not used by multiple live ranges. That being
// said, lets assert that.
{
SmallVector<OwnedValueIntroducer, 32> uniqueCheck;
llvm::copy(ownedValueIntroducerAccumulator,
std::back_inserter(uniqueCheck));
sortUnique(uniqueCheck);
assert(
uniqueCheck.size() == ownedValueIntroducerAccumulator.size() &&
"multiple joined live range operands are from the same live range?!");
}
#endif
return true;
}
static bool getIncomingJoinedLiveRangeOperands(
SILValue joinedLiveRange,
SmallVectorImpl<OwnershipPhiOperand> &resultingOperands) {
if (auto *phi = dyn_cast<SILPhiArgument>(joinedLiveRange)) {
return phi->visitIncomingPhiOperands([&](Operand *op) {
if (auto phiOp = OwnershipPhiOperand::get(op)) {
resultingOperands.push_back(*phiOp);
return true;
}
return false;
});
}
if (auto *svi = dyn_cast<SingleValueInstruction>(joinedLiveRange)) {
return llvm::all_of(svi->getAllOperands(), [&](const Operand &op) {
// skip type dependent operands.
if (op.isTypeDependent())
return true;
auto phiOp = OwnershipPhiOperand::get(&op);
if (!phiOp)
return false;
resultingOperands.push_back(*phiOp);
return true;
});
}
llvm_unreachable("Unhandled joined live range?!");
}
//===----------------------------------------------------------------------===//
// Top Level Entrypoint
//===----------------------------------------------------------------------===//
// This needs `SemanticARCOptVisitor::performGuaranteedCopyValueOptimization` to
// run before so that joinedOwnedIntroducerToConsumedOperands is populated.
bool swift::semanticarc::tryConvertOwnedPhisToGuaranteedPhis(Context &ctx) {
bool madeChange = false;
// First freeze our multi-map so we can use it for map queries. Also, setup a
// defer of the reset so we do not forget to reset the map when we are done.
ctx.joinedOwnedIntroducerToConsumedOperands.setFrozen();
SWIFT_DEFER { ctx.joinedOwnedIntroducerToConsumedOperands.reset(); };
// Now for each phi argument that we have in our multi-map...
SmallVector<OwnershipPhiOperand, 4> incomingValueOperandList;
SmallVector<OwnedValueIntroducer, 4> ownedValueIntroducers;
for (auto pair : ctx.joinedOwnedIntroducerToConsumedOperands.getRange()) {
SWIFT_DEFER {
incomingValueOperandList.clear();
ownedValueIntroducers.clear();
};
// First compute the LiveRange for ownershipPhi value. For simplicity, we
// only handle cases now where the result does not have any additional
// ownershipPhi uses.
SILValue joinedIntroducer = pair.first;
OwnershipLiveRange joinedLiveRange(joinedIntroducer);
if (bool(joinedLiveRange.hasUnknownConsumingUse())) {
continue;
}
// Ok, we know that our phi argument /could/ be converted to guaranteed if
// our incoming values are able to be converted to guaranteed. Now for each
// incoming value, compute the incoming values ownership roots and see if
// all of the ownership roots are in our owned incoming value array.
if (!getIncomingJoinedLiveRangeOperands(joinedIntroducer,
incomingValueOperandList)) {
continue;
}
// Grab our list of introducer values paired with this SILArgument. See if
// all of these introducer values were ones that /could/ have been
// eliminated if it was not for the given phi. If all of them are, we can
// optimize!
{
std::function<Operand *(const Context::ConsumingOperandState)> lambda =
[&](const Context::ConsumingOperandState &state) -> Operand * {
unsigned opNum = state.operandNumber;
if (isa<SILBasicBlock *>(state.parent)) {
SILBasicBlock *block = cast<SILBasicBlock *>(state.parent);
return &block->getTerminator()->getAllOperands()[opNum];
}
SILInstruction *inst = cast<SILInstruction *>(state.parent);
return &inst->getAllOperands()[opNum];
};
auto operandsTransformed = makeTransformRange(pair.second, lambda);
if (!canEliminatePhi(operandsTransformed, incomingValueOperandList,
ownedValueIntroducers)) {
continue;
}
}
// Ok, at this point we know that we can eliminate this phi. First go
// through the list of incomingValueOperandList and stash the value/set the
// operand's stored value to undef. We will hook them back up later.
SmallVector<SILValue, 8> originalIncomingValues;
for (auto &incomingValueOperand : incomingValueOperandList) {
originalIncomingValues.push_back(incomingValueOperand.getValue());
incomingValueOperand.markUndef();
}
// Then go through all of our owned value introducers, compute their live
// ranges, and eliminate them. We know it is safe to remove them from our
// previous proofs.
//
// NOTE: If our introducer is a copy_value that is one of our
// originalIncomingValues, we need to update the originalIncomingValue array
// with that value since we are going to delete the copy_value here. This
// creates a complication since we want to binary_search on
// originalIncomingValues to detect this same condition! So, we create a
// list of updates that we apply after we no longer need to perform
// binary_search, but before we start RAUWing things.
SmallVector<std::pair<SILValue, unsigned>, 8> incomingValueUpdates;
for (auto introducer : ownedValueIntroducers) {
SILValue v = introducer.value;
SmallVector<std::pair<Operand *, SILValue>, 4> extraConsumes;
for (auto op : incomingValueOperandList) {
extraConsumes.push_back({op.getOperand(), op.getOriginal()});
}
OwnershipLiveRange lr(v, extraConsumes);
// For now, we only handle copy_value for simplicity.
//
// TODO: Add support for load [copy].
if (introducer.kind == OwnedValueIntroducerKind::Copy) {
auto *cvi = cast<CopyValueInst>(v);
// Before we convert from owned to guaranteed, we need to first see if
// cvi is one of our originalIncomingValues. If so, we need to set
// originalIncomingValues to be cvi->getOperand(). Otherwise, weirdness
// results since we are deleting one of our stashed values.
auto iter = find(originalIncomingValues, cvi);
if (iter != originalIncomingValues.end()) {
// We use an auxillary array here so we can continue to bisect on
// original incoming values. Once we are done processing here, we will
// not need that property anymore.
unsigned updateOffset =
std::distance(originalIncomingValues.begin(), iter);
incomingValueUpdates.emplace_back(cvi->getOperand(), updateOffset);
}
std::move(lr).convertToGuaranteedAndRAUW(cvi->getOperand(),
ctx.instModCallbacks);
continue;
}
llvm_unreachable("Unhandled optimizable introducer!");
}
// Now go through and update our original incoming value array now that we
// do not need it to be sorted for bisection purposes.
while (!incomingValueUpdates.empty()) {
auto pair = incomingValueUpdates.pop_back_val();
originalIncomingValues[pair.second] = pair.first;
}
// Now if our phi operand consumes/forwards its guaranteed input, insert a
// begin_borrow along the incoming value edges. We have to do this after
// converting the incoming values to be guaranteed to avoid tripping
// SILBuilder checks around simple ownership invariants (namely that def/use
// line up) when creating instructions.
assert(incomingValueOperandList.size() == originalIncomingValues.size());
while (!incomingValueOperandList.empty()) {
auto incomingValueOperand = incomingValueOperandList.pop_back_val();
SILValue originalValue = originalIncomingValues.pop_back_val();
incomingValueOperand.getOperand()->set(originalValue);
}
// Then convert the phi's live range to be guaranteed.
std::move(joinedLiveRange)
.convertJoinedLiveRangePhiToGuaranteed(
ctx.getDeadEndBlocks(), ctx.lifetimeFrontier, ctx.instModCallbacks);
madeChange = true;
ctx.verify();
}
if (madeChange)
updateAllGuaranteedPhis(ctx.pm, &ctx.fn);
return madeChange;
}