Skip to content

Commit bdf6143

Browse files
committed
[ownership] Add simple support for concatenating together simple live ranges that do not need LiveRange analysis.
Specifically, this PR adds support for optimizing simple cases where we do not need to compute LiveRanges with the idea of first doing simple transforms that involve small numbers of instructions first. With that in mind, we only optimize cases where our copy_value has a single consuming user and our owned value has a single destroy_value. To understand the transform here, consider the following SIL: ``` %0 = ... %1 = copy_value %0 (1) apply %guaranteedUser(%0) (2) destroy_value %0 (3) apply %cviConsumer(%1) (4) ``` We want to eliminate (2) and (3), effectively joining the lifetimes of %0 and %1, transforming the code to: ``` %0 = ... apply %guaranteedUser(%0) (2) apply %cviConsumer(%0) (4) ``` Easily, we can always do this transform in this case since we know that %0's lifetime ends strictly before the end of %1's due to (3) being before (4). This means that any uses that require liveness of %0 must be before (4) and thus no use-after-frees can result from removing (3) since we are not shrinking the underlying object's lifetime. Lets consider a different case where (3) and (4) are swapped. ``` %0 = ... %1 = copy_value %0 (1) apply %guaranteedUser(%0) (2) apply %cviConsumer(%1) (4) destroy_value %0 (3) ``` In this case, since there aren't any liveness requiring uses of %0 in between (4) and (3), we can still perform our transform. But what if there was a liveness requiring user in between (4) and (3). To analyze this, lets swap (2) and (4), yielding: ``` %0 = ... %1 = copy_value %0 (1) apply %cviConsumer(%1) (4) apply %guaranteedUser(%0) (2) destroy_value %0 (3) ``` In this case, if we were to perform our transform, we would get a use-after-free due do the transform shrinking the lifetime of the underlying object here from ending at (3) to ending at (4): ``` %0 = ... apply %cviConsumer(%1) (4) apply %guaranteedUser(%0) (2) // *kaboom* ``` So clearly, if (3) is after (4), clearly, we need to know that there aren't any liveness requiring uses in between them to be able to perform this optimization. But is this enough? Turns out no. There are two further issues that we must consider: 1. If (4) is forwards owned ownership, it is not truly "consuming" the underlying value in the sense of actually destroying the underlying value. This can be worked with by using the LiveRange abstraction. That being said, this PR is meant to contain simple transforms that do not need to use LiveRange. So, we bail if we see a forwarding instruction. 2. At the current time, we may not be able to find all normal uses since all of the instructions that are interior pointer constructs (e.x.: project_box) have not been required yet to always be guarded by borrows (the eventual end state). Thus we can not shrink lifetimes in general safely until that piece of work is done. Given all of those constraints, we only handle cases here where (3) is strictly before (4) so we know 100% we are not shrinking any lifetimes. This effectively is causing our correctness to rely on SILGen properly scoping lifetimes. Once all interior pointers are properly guarded, we will be able to be more aggressive here. With that in mind, we perform this transform given the following conditions noting that this pattern often times comes up around return values: 1. If the consuming user is a return inst. In such a case, we know that the destroy_value must be before the actual return inst. 2. If the consuming user is in the exit block and the destroy_value is not. 3. If the consuming user and destroy_value are in the same block and the consuming user is strictly later in that block than the destroy_value. In all of these cases, we are able to optimize without the need for LiveRanges. I am going to add support for this in a subsequent commit.
1 parent fd6060b commit bdf6143

File tree

5 files changed

+438
-7
lines changed

5 files changed

+438
-7
lines changed

include/swift/SIL/OwnershipUtils.h

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -218,6 +218,10 @@ bool isGuaranteedForwardingValue(SILValue value);
218218
/// forward guaranteed ownership.
219219
bool isOwnedForwardingValueKind(SILNodeKind kind);
220220

221+
/// Does this SILInstruction 'forward' owned ownership, but may not be able to
222+
/// forward guaranteed ownership.
223+
bool isOwnedForwardingInstruction(SILInstruction *inst);
224+
221225
struct BorrowScopeOperandKind {
222226
enum Kind {
223227
BeginBorrow,

include/swift/SIL/SILValue.h

Lines changed: 54 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -272,9 +272,22 @@ class ValueBase : public SILNode, public SILAllocated<ValueBase> {
272272
/// otherwise.
273273
inline Operand *getSingleUse() const;
274274

275+
/// Returns .some(single user) if this value is non-trivial, we are in ossa,
276+
/// and it has a single consuming user. Returns .none otherwise.
277+
inline Operand *getSingleConsumingUse() const;
278+
275279
template <class T>
276280
inline T *getSingleUserOfType() const;
277281

282+
template <class T> inline T *getSingleConsumingUserOfType() const;
283+
284+
/// Returns true if this operand has exactly two.
285+
///
286+
/// This is useful if one has found a predefined set of 2 unique users and
287+
/// wants to check if there are any other users without iterating over the
288+
/// entire use list.
289+
inline bool hasTwoUses() const;
290+
278291
/// Helper struct for DowncastUserFilterRange
279292
struct UseToUser;
280293

@@ -647,6 +660,10 @@ class Operand {
647660
/// Returns true if this operand acts as a use that consumes its associated
648661
/// value.
649662
bool isConsumingUse() const {
663+
// Type dependent uses can never be consuming and do not have valid
664+
// ownership maps since they do not participate in the ownership system.
665+
if (isTypeDependent())
666+
return false;
650667
auto map = getOwnershipKindMap();
651668
auto constraint = map.getLifetimeConstraint(get().getOwnershipKind());
652669
return constraint == UseLifetimeConstraint::MustBeInvalidated;
@@ -748,17 +765,48 @@ inline Operand *ValueBase::getSingleUse() const {
748765
return Op;
749766
}
750767

768+
inline Operand *ValueBase::getSingleConsumingUse() const {
769+
Operand *result = nullptr;
770+
for (auto *op : getUses()) {
771+
if (op->isConsumingUse()) {
772+
if (result) {
773+
return nullptr;
774+
}
775+
result = op;
776+
}
777+
}
778+
return result;
779+
}
780+
781+
inline bool ValueBase::hasTwoUses() const {
782+
auto iter = use_begin(), end = use_end();
783+
for (unsigned i = 0; i < 2; ++i) {
784+
if (iter == end)
785+
return false;
786+
++iter;
787+
}
788+
return iter == end;
789+
}
790+
751791
template <class T>
752792
inline T *ValueBase::getSingleUserOfType() const {
753-
T *Result = nullptr;
754-
for (auto *Op : getUses()) {
755-
if (auto *Tmp = dyn_cast<T>(Op->getUser())) {
756-
if (Result)
793+
T *result = nullptr;
794+
for (auto *op : getUses()) {
795+
if (auto *tmp = dyn_cast<T>(op->getUser())) {
796+
if (result)
757797
return nullptr;
758-
Result = Tmp;
798+
result = tmp;
759799
}
760800
}
761-
return Result;
801+
return result;
802+
}
803+
804+
template <class T> inline T *ValueBase::getSingleConsumingUserOfType() const {
805+
auto *op = getSingleConsumingUse();
806+
if (!op)
807+
return nullptr;
808+
809+
return dyn_cast<T>(op->getUser());
762810
}
763811

764812
struct ValueBase::UseToUser {

lib/SIL/OwnershipUtils.cpp

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -73,6 +73,16 @@ bool swift::isOwnedForwardingValueKind(SILNodeKind kind) {
7373
}
7474
}
7575

76+
bool swift::isOwnedForwardingInstruction(SILInstruction *inst) {
77+
auto kind = inst->getKind();
78+
switch (kind) {
79+
case SILInstructionKind::BranchInst:
80+
return true;
81+
default:
82+
return isOwnershipForwardingValueKind(SILNodeKind(kind));
83+
}
84+
}
85+
7686
bool swift::isGuaranteedForwardingValue(SILValue value) {
7787
// If we have an argument from a transforming terminator, we can forward
7888
// guaranteed.

lib/SILOptimizer/Transforms/SemanticARCOpts.cpp

Lines changed: 172 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -59,7 +59,6 @@ class LiveRange {
5959

6060
public:
6161
LiveRange(SILValue value);
62-
6362
LiveRange(const LiveRange &) = delete;
6463
LiveRange &operator=(const LiveRange &) = delete;
6564

@@ -454,6 +453,7 @@ struct SemanticARCOptVisitor
454453

455454
bool performGuaranteedCopyValueOptimization(CopyValueInst *cvi);
456455
bool eliminateDeadLiveRangeCopyValue(CopyValueInst *cvi);
456+
bool tryJoiningCopyValueLiveRangeWithOperand(CopyValueInst *cvi);
457457
};
458458

459459
} // end anonymous namespace
@@ -824,13 +824,184 @@ bool SemanticARCOptVisitor::eliminateDeadLiveRangeCopyValue(CopyValueInst *cvi)
824824
return true;
825825
}
826826

827+
// Handle simple checking where we do not need to form live ranges and visit a
828+
// bunch of instructions.
829+
static bool canSafelyJoinSimpleRange(SILValue cviOperand,
830+
DestroyValueInst *cviOperandDestroy,
831+
CopyValueInst *cvi) {
832+
// We only handle cases where our copy_value has a single consuming use that
833+
// is not a forwarding use. We need to use the LiveRange functionality to
834+
// guarantee correctness in the presence of forwarding uses.
835+
//
836+
// NOTE: This use may be any type of consuming use and may not be a
837+
// destroy_value.
838+
auto *cviConsumer = cvi->getSingleConsumingUse();
839+
if (!cviConsumer || isOwnedForwardingInstruction(cviConsumer->getUser())) {
840+
return false;
841+
}
842+
843+
// Ok, we may be able to eliminate this. The main thing we need to be careful
844+
// of here is that if the destroy_value is /after/ the consuming use of the
845+
// operand of copy_value, we may have normal uses of the copy_value's operand
846+
// that would become use-after-frees since we would be shrinking the lifetime
847+
// of the object potentially. Consider the following SIL:
848+
//
849+
// %0 = ...
850+
// %1 = copy_value %0
851+
// apply %cviConsumer(%1)
852+
// apply %guaranteedUser(%0)
853+
// destroy_value %0
854+
//
855+
// Easily, if we were to eliminate the copy_value, destroy_value, the object's
856+
// lifetime could potentially be shrunk before guaranteedUser is executed,
857+
// causing guaranteedUser to be a use-after-free.
858+
//
859+
// As an extra wrinkle, until all interior pointer constructs (e.x.:
860+
// project_box) are guaranteed to be guaranted by a begin_borrow, we can not
861+
// in general safely shrink lifetimes. So even if we think we can prove that
862+
// all non-consuming uses of %0 are before apply %cviConsumer, we may miss
863+
// implicit uses that are not guarded yet by a begin_borrow, resulting in
864+
// use-after-frees.
865+
//
866+
// With that in mind, we only handle cases today where we can prove that
867+
// destroy_value is strictly before the consuming use of the operand. This
868+
// guarantees that we are not shrinking the lifetime of the underlying object.
869+
//
870+
// First we handle the simple case: where the cviConsumer is a return inst. In
871+
// such a case, we know for sure that cviConsumer post-dominates the
872+
// destroy_value.
873+
auto cviConsumerIter = cviConsumer->getUser()->getIterator();
874+
if (isa<ReturnInst>(cviConsumerIter)) {
875+
return true;
876+
}
877+
878+
// Then see if our cviConsumer is in the same block as a return inst and the
879+
// destroy_value is not. In that case, we know that the cviConsumer must
880+
// post-dominate the destroy_value.
881+
auto *cviConsumingBlock = cviConsumerIter->getParent();
882+
if (isa<ReturnInst>(cviConsumingBlock->getTerminator()) &&
883+
cviConsumingBlock != cviOperandDestroy->getParent()) {
884+
return true;
885+
}
886+
887+
// Otherwise, we only support joining live ranges where the cvi and the cvi's
888+
// operand's destroy are in the same block with the destroy_value of cvi
889+
// operand needing to be strictly after the copy_value. This analysis can be
890+
// made significantly stronger by using LiveRanges, but this is simple for
891+
// now.
892+
auto cviOperandDestroyIter = cviOperandDestroy->getIterator();
893+
if (cviConsumingBlock != cviOperandDestroyIter->getParent()) {
894+
return false;
895+
}
896+
897+
// TODO: This should really be llvm::find, but for some reason, the templates
898+
// do not match up given the current state of the iterators. This impl works
899+
// in a pinch though.
900+
return llvm::any_of(
901+
llvm::make_range(cviOperandDestroyIter,
902+
cviOperandDestroyIter->getParent()->end()),
903+
[&](const SILInstruction &val) { return &*cviConsumerIter == &val; });
904+
}
905+
906+
// # The Problem We Are Solving
907+
//
908+
// The main idea here is that we are trying to eliminate the simplest, easiest
909+
// form of live range joining. Consider the following SIL:
910+
//
911+
// ```
912+
// %cviOperand = ... // @owned value
913+
// %cvi = copy_value %cviOperand // copy of @owned value
914+
// ...
915+
// destroy_value %cviOperandDestroy // destruction of @owned value
916+
// ...
917+
// apply %consumingUser(%cvi) // destruction of copy of @owned value
918+
// ```
919+
//
920+
// We want to reduce reference count traffic by eliminating the middle
921+
// copy/destroy yielding:
922+
//
923+
// ```
924+
// %cviOperand = ... // @owned value
925+
// // *eliminated copy_value*
926+
// ...
927+
// // *eliminated destroy_value*
928+
// ...
929+
// apply %consumingUser(%cviOperand) // destruction of copy of @owned
930+
// value
931+
// ```
932+
//
933+
// # Safety
934+
//
935+
// In order to do this safely, we need to take the union of the two objects
936+
// lifetimes since we are only joining lifetimes. This ensures that we can rely
937+
// on SILGen's correctness on inserting safe lifetimes. To keep this simple
938+
// today we only optimize if the destroy_value and consuming user are in the
939+
// same block and the consuming user is later in the block than the
940+
// destroy_value.
941+
//
942+
// DISCUSSION: The reason why we do not shrink lifetimes today is that all
943+
// interior pointers (e.x. project_box) are properly guarded by
944+
// begin_borrow. Because of that we can not shrink lifetimes and instead rely on
945+
// SILGen's correctness.
946+
bool SemanticARCOptVisitor::tryJoiningCopyValueLiveRangeWithOperand(
947+
CopyValueInst *cvi) {
948+
// First do a quick check if our operand is owned. If it is not owned, we can
949+
// not join live ranges.
950+
SILValue operand = cvi->getOperand();
951+
if (operand.getOwnershipKind() != ValueOwnershipKind::Owned) {
952+
return false;
953+
}
954+
955+
// Then check if our operand has a single destroy_value. If it does and that
956+
// destroy_value is strictly before the consumer of our copy_value in the same
957+
// block as the consumer of said copy_value then we can always join the live
958+
// ranges.
959+
//
960+
// Example:
961+
//
962+
// ```
963+
// %1 = copy_value %0
964+
// ...
965+
// destroy_value %0
966+
// apply %consumingUser(%1)
967+
// ```
968+
// ->
969+
//
970+
// ```
971+
// apply %consumingUser(%0)
972+
// ```
973+
//
974+
// DISCUSSION: We need to ensure that the consuming use of the copy_value is
975+
// strictly after the destroy_value to ensure that we do not shrink the live
976+
// range of the operand if the operand has any normal uses beyond our copy
977+
// value. Otherwise, we could have normal uses /after/ the consuming use of
978+
// our copy_value.
979+
if (auto *dvi = operand->getSingleConsumingUserOfType<DestroyValueInst>()) {
980+
if (canSafelyJoinSimpleRange(operand, dvi, cvi)) {
981+
eraseInstruction(dvi);
982+
eraseAndRAUWSingleValueInstruction(cvi, operand);
983+
NumEliminatedInsts += 2;
984+
return true;
985+
}
986+
}
987+
988+
// Otherwise, we couldn't handle this case, so return false.
989+
return false;
990+
}
991+
827992
bool SemanticARCOptVisitor::visitCopyValueInst(CopyValueInst *cvi) {
828993
// If our copy value inst has only destroy_value users, it is a dead live
829994
// range. Try to eliminate them.
830995
if (eliminateDeadLiveRangeCopyValue(cvi)) {
831996
return true;
832997
}
833998

999+
// Then see if copy_value operand's lifetime ends after our copy_value via a
1000+
// destroy_value. If so, we can join their lifetimes.
1001+
if (tryJoiningCopyValueLiveRangeWithOperand(cvi)) {
1002+
return true;
1003+
}
1004+
8341005
// Then try to perform the guaranteed copy value optimization.
8351006
if (performGuaranteedCopyValueOptimization(cvi)) {
8361007
return true;

0 commit comments

Comments
 (0)