Skip to content

Commit faf4519

Browse files
committed
[semantic-arc-opts] Instead of tracking owned value introducers, track incoming values that are seen from owned value introducers.
TLDR: This will allow me to in a forthcoming commit to eliminate the extra run of the peephole optimizer to seed the phi arg analysis since this eliminates potential data invalidation issues. That being said, I am formalizing the language/model in the pass a little in this commit, so I wanted to give a long description below. ---- In this commit, I am formalizing some of the language around this optimization away phi nodes specifically and more towards a notion that Andy and I have discussed called "Joined Live Ranges". The idea is a "Live Range" in OSSA is an owned value together with its set of forwarding uses. The owned value is in a certain sense the equivalence class representative of all of the values that are derived from the owned value via forwarding instructions. This works well as long as all of the "forwarding" instructions only derive their ownership from a single operand. Some instructions without that property are branch, tuple, struct. Since such a "forwarding" instruction derives its ownership by merging the ownership of multiple operands, if multiple of its operands are non-trivial owned operands, we are in a sense joining together the live ranges of those non-trivial owned operands. The main implication of this is that if we want to convert the joined live range from owned to guaranteed, we can only do it if we can also do it for all of its incoming values at the same time. This creates a conundrum though since this pass has been written like a peephole pass which is ill-suited to be converted into this form. So instead, this pass seeds an analysis from peephole runs that /could/ have been converted from owned to guaranteed except for a specific joined live range. Then after we reach a fixed point, we use that information from the failed peepholes to try to eliminate the joined live ranges. This approach is a good approach, but if implemented in certain ways exposes the risk of problems around touching invalidated data. Specifically, before this patch, this was implemented with the concern that each branch instruction (currently the only supported type of joined live range instruction) could be paired with multiple owned value introducers. This is because initially, we were allowing for tuple, struct with multiple non-trivial operands to be used as owned value introducers. This implied that we could not rely on just storing a bool per joined live range operand since we would need to know that all introducers associated with the operand were optimizable. After some thought/discussion, I realized that this sort of optimization can also apply to tuple/struct/similar insts. This let to the shift in thinking/realization that we could talk instead about general joined live ranges and handle tuple/struct/similar instructions just like they were branch/phi! As such, if we assume that all such instructions are treated as separate LiveRanges, then we know that our branch can only have a single owned value introducer since we do not look through /any/ joined live ranges when computing LiveRanges. This realization then allows us to store a single bool value marking the operand on the joined live range as being one that we /may/ be able to optimize. This then allows us to simplify the state we handle and not have to worry about storing memory to instructions that may be eliminated. Since we have a joined live range use, we know that any value that is used by the joined live range can not be eliminated by the peephole optimizer.
1 parent a3979da commit faf4519

File tree

1 file changed

+95
-65
lines changed

1 file changed

+95
-65
lines changed

lib/SILOptimizer/Transforms/SemanticARCOpts.cpp

Lines changed: 95 additions & 65 deletions
Original file line numberDiff line numberDiff line change
@@ -447,8 +447,8 @@ LiveRange::hasUnknownConsumingUse(bool assumingAtFixPoint) const {
447447
return HasConsumingUse_t::No;
448448
}
449449

450-
// Ok, we do have some unknown consuming uses. If we aren't asked to
451-
// update phiToIncomingValueMultiMap, then just return true quickly.
450+
// Ok, we do have some unknown consuming uses. If we aren't assuming we are at
451+
// the fixed point yet, just bail.
452452
if (!assumingAtFixPoint) {
453453
return HasConsumingUse_t::Yes;
454454
}
@@ -628,21 +628,35 @@ struct SemanticARCOptVisitor
628628
/// Are we assuming that we reached a fix point and are re-processing to
629629
/// prepare to use the phiToIncomingValueMultiMap.
630630
bool assumingAtFixedPoint = false;
631-
FrozenMultiMap<SILPhiArgument *, OwnedValueIntroducer>
632-
phiToIncomingValueMultiMap;
633-
634-
/// Returns the phiToIncomingValueMultiMap if we are re-processing our
635-
/// worklist after fixed point to initialize our phi to incoming value
636-
/// multi-map. Otherwise returns nullptr.
637-
FrozenMultiMap<SILPhiArgument *, OwnedValueIntroducer> *
638-
getPhiToIncomingValueMultiMap() {
639-
if (assumingAtFixedPoint)
640-
return &phiToIncomingValueMultiMap;
641-
return nullptr;
642-
}
631+
632+
/// A map from a value that acts as a "joined owned introducer" in the def-use
633+
/// graph.
634+
///
635+
/// A "joined owned introducer" is a value with owned ownership whose
636+
/// ownership is derived from multiple non-trivial owned operands of a related
637+
/// instruction. Some examples are phi arguments, tuples, structs. Naturally,
638+
/// all of these instructions must be non-unary instructions and only have
639+
/// this property if they have multiple operands that are non-trivial.
640+
///
641+
/// In such a case, we can not just treat them like normal forwarding concepts
642+
/// since we can only eliminate optimize such a value if we are able to reason
643+
/// about all of its operands together jointly. This is not amenable to a
644+
/// small peephole analysis.
645+
///
646+
/// Instead, as we perform the peephole analysis, using the multimap, we map
647+
/// each joined owned value introducer to the set of its @owned operands that
648+
/// we thought we could convert to guaranteed only if we could do the same to
649+
/// the joined owned value introducer. Then once we finish performing
650+
/// peepholes, we iterate through the map and see if any of our joined phi
651+
/// ranges had all of their operand's marked with this property by iterating
652+
/// over the multimap. Since we are dealing with owned values and we know that
653+
/// our LiveRange can not see through joined live ranges, we know that we
654+
/// should only be able to have a single owned value introducer for each
655+
/// consumed operand.
656+
FrozenMultiMap<SILValue, Operand *> joinedOwnedIntroducerToConsumedOperands;
643657

644658
using FrozenMultiMapRange =
645-
decltype(phiToIncomingValueMultiMap)::PairToSecondEltRange;
659+
decltype(joinedOwnedIntroducerToConsumedOperands)::PairToSecondEltRange;
646660

647661
explicit SemanticARCOptVisitor(SILFunction &F) : F(F) {}
648662

@@ -800,88 +814,106 @@ static bool canEliminatePhi(
800814
SemanticARCOptVisitor::FrozenMultiMapRange optimizableIntroducerRange,
801815
ArrayRef<Operand *> incomingValueOperandList,
802816
SmallVectorImpl<OwnedValueIntroducer> &ownedValueIntroducerAccumulator) {
803-
// A set that we use to ensure we only add introducers to the accumulator
804-
// once.
805-
SmallVector<OwnedValueIntroducer, 16> scratch;
806817
for (Operand *incomingValueOperand : incomingValueOperandList) {
807-
SWIFT_DEFER { scratch.clear(); };
808-
809818
SILValue incomingValue = incomingValueOperand->get();
810819

811820
// Before we do anything, see if we have an incoming value with trivial
812821
// ownership. This can occur in the case where we are working with enums due
813-
// to trivial non-payloaded cases.
822+
// to trivial non-payloaded cases. Skip that.
814823
if (incomingValue.getOwnershipKind() == ValueOwnershipKind::None) {
815824
continue;
816825
}
817826

818-
// Now that we know it is an owned value, check for introducers of the owned
819-
// value which are the copies that we may be able to eliminate.
827+
// Then see if this is an introducer that we actually saw as able to be
828+
// optimized if we could flip this joined live range.
820829
//
821-
// If we can not find all of the owned value's introducers, bail.
822-
if (!getAllOwnedValueIntroducers(incomingValue, scratch)) {
830+
// NOTE: If this linear search is too slow, we can change the multimap to
831+
// sort the mapped to list by pointer instead of insertion order. In such a
832+
// case, we could then bisect.
833+
if (llvm::find(optimizableIntroducerRange, incomingValueOperand) ==
834+
optimizableIntroducerRange.end()) {
823835
return false;
824836
}
825837

826-
// Then make sure that all of our owned value introducers are able to be
827-
// converted to guaranteed and that we found it to have a LiveRange that we
828-
// could have eliminated /if/ we were to get rid of this phi.
829-
if (!llvm::all_of(scratch, [&](const OwnedValueIntroducer &introducer) {
830-
if (!introducer.isConvertableToGuaranteed()) {
831-
return false;
832-
}
833-
// If this linear search is too slow, we can change the
834-
// multimap to sort the mapped to list by pointer
835-
// instead of insertion order. In such a case, we could
836-
// then bisect.
837-
auto iter = llvm::find(optimizableIntroducerRange, introducer);
838-
return iter != optimizableIntroducerRange.end();
839-
})) {
838+
// Now that we know it is an owned value that we saw before, check for
839+
// introducers of the owned value which are the copies that we may be able
840+
// to eliminate. Since we do not look through joined live ranges, we must
841+
// only have a single introducer. So look for that one and if not, bail.
842+
auto singleIntroducer = getSingleOwnedValueIntroducer(incomingValue);
843+
if (!singleIntroducer.hasValue()) {
844+
return false;
845+
}
846+
847+
// Then make sure that our owned value introducer is able to be converted to
848+
// guaranteed and that we found it to have a LiveRange that we could have
849+
// eliminated /if/ we were to get rid of this phi.
850+
if (!singleIntroducer->isConvertableToGuaranteed()) {
840851
return false;
841852
}
842853

843-
// Otherwise, append all introducers from scratch into our result array.
844-
llvm::copy(scratch, std::back_inserter(ownedValueIntroducerAccumulator));
854+
// Otherwise, add the introducer to our result array.
855+
ownedValueIntroducerAccumulator.push_back(*singleIntroducer);
845856
}
846857

847-
// Now that we are done, perform a sort unique on our result array so that we
848-
// on return have a unique set of values.
849-
sortUnique(ownedValueIntroducerAccumulator);
858+
#ifndef NDEBUG
859+
// Other parts of the pass ensure that we only add values to the list if their
860+
// owned value introducer is not used by multiple live ranges. That being
861+
// said, lets assert that.
862+
{
863+
SmallVector<OwnedValueIntroducer, 32> uniqueCheck;
864+
llvm::copy(ownedValueIntroducerAccumulator,
865+
std::back_inserter(uniqueCheck));
866+
sortUnique(uniqueCheck);
867+
assert(
868+
uniqueCheck.size() == ownedValueIntroducerAccumulator.size() &&
869+
"multiple joined live range operands are from the same live range?!");
870+
}
871+
#endif
850872

851873
return true;
852874
}
853875

876+
static bool getIncomingJoinedLiveRangeOperands(
877+
SILValue joinedLiveRange, SmallVectorImpl<Operand *> &resultingOperands) {
878+
if (auto *phi = dyn_cast<SILPhiArgument>(joinedLiveRange)) {
879+
return phi->getIncomingPhiOperands(resultingOperands);
880+
}
881+
882+
llvm_unreachable("Unhandled joined live range?!");
883+
}
884+
854885
bool SemanticARCOptVisitor::performPostPeepholeOwnedArgElimination() {
855886
bool madeChange = false;
856887

857888
// First freeze our multi-map so we can use it for map queries. Also, setup a
858889
// defer of the reset so we do not forget to reset the map when we are done.
859-
phiToIncomingValueMultiMap.setFrozen();
860-
SWIFT_DEFER { phiToIncomingValueMultiMap.reset(); };
890+
joinedOwnedIntroducerToConsumedOperands.setFrozen();
891+
SWIFT_DEFER { joinedOwnedIntroducerToConsumedOperands.reset(); };
861892

862893
// Now for each phi argument that we have in our multi-map...
863894
SmallVector<Operand *, 4> incomingValueOperandList;
864895
SmallVector<OwnedValueIntroducer, 4> ownedValueIntroducers;
865-
for (auto pair : phiToIncomingValueMultiMap.getRange()) {
896+
for (auto pair : joinedOwnedIntroducerToConsumedOperands.getRange()) {
866897
SWIFT_DEFER {
867898
incomingValueOperandList.clear();
868899
ownedValueIntroducers.clear();
869900
};
870901

871-
// First compute the LiveRange for our phi argument. For simplicity, we only
872-
// handle cases now where our phi argument does not have any phi unknown
873-
// consumers.
874-
SILPhiArgument *phi = pair.first;
875-
LiveRange phiLiveRange(phi);
876-
if (bool(phiLiveRange.hasUnknownConsumingUse())) {
902+
// First compute the LiveRange for ownershipPhi value. For simplicity, we
903+
// only handle cases now where the result does not have any additional
904+
// ownershipPhi uses.
905+
SILValue joinedIntroducer = pair.first;
906+
LiveRange joinedLiveRange(joinedIntroducer);
907+
if (bool(joinedLiveRange.hasUnknownConsumingUse())) {
877908
continue;
878909
}
879910

880911
// Ok, we know that our phi argument /could/ be converted to guaranteed if
881912
// our incoming values are able to be converted to guaranteed. Now for each
882913
// incoming value, compute the incoming values ownership roots and see if
883914
// all of the ownership roots are in our owned incoming value array.
884-
if (!phi->getIncomingPhiOperands(incomingValueOperandList)) {
915+
if (!getIncomingJoinedLiveRangeOperands(joinedIntroducer,
916+
incomingValueOperandList)) {
885917
continue;
886918
}
887919

@@ -904,7 +936,7 @@ bool SemanticARCOptVisitor::performPostPeepholeOwnedArgElimination() {
904936
for (Operand *incomingValueOperand : incomingValueOperandList) {
905937
originalIncomingValues.push_back(incomingValueOperand->get());
906938
SILType type = incomingValueOperand->get()->getType();
907-
auto *undef = SILUndef::get(type, *phi->getFunction());
939+
auto *undef = SILUndef::get(type, F);
908940
incomingValueOperand->set(undef);
909941
}
910942

@@ -957,7 +989,7 @@ bool SemanticARCOptVisitor::performPostPeepholeOwnedArgElimination() {
957989
}
958990

959991
// Then convert the phi's live range to be guaranteed.
960-
std::move(phiLiveRange)
992+
std::move(joinedLiveRange)
961993
.convertArgToGuaranteed(getDeadEndBlocks(), lifetimeFrontier,
962994
getCallbacks());
963995

@@ -981,7 +1013,7 @@ bool SemanticARCOptVisitor::performPostPeepholeOwnedArgElimination() {
9811013

9821014
madeChange = true;
9831015
if (VerifyAfterTransform) {
984-
phi->getFunction()->verify();
1016+
F.verify();
9851017
}
9861018
}
9871019

@@ -1007,8 +1039,8 @@ bool SemanticARCOptVisitor::optimize() {
10071039

10081040
// Then re-run the worklist. We shouldn't modify anything since we are at a
10091041
// fixed point and are just using this to seed the
1010-
// phiToIncomingValueMultiMap after we have finished changing things. If we
1011-
// did change something, we did something weird, so assert!
1042+
// joinedOwnedIntroducerToConsumedOperands after we have finished changing
1043+
// things. If we did change something, we did something weird, so assert!
10121044
bool madeAdditionalChanges = processWorklist();
10131045
(void)madeAdditionalChanges;
10141046
assert(!madeAdditionalChanges && "Should be at the fixed point");
@@ -1177,7 +1209,7 @@ bool SemanticARCOptVisitor::performGuaranteedCopyValueOptimization(CopyValueInst
11771209
// (e.x. storing into memory).
11781210
LiveRange lr(cvi);
11791211
auto hasUnknownConsumingUseState =
1180-
lr.hasUnknownConsumingUse(getPhiToIncomingValueMultiMap());
1212+
lr.hasUnknownConsumingUse(assumingAtFixedPoint);
11811213
if (hasUnknownConsumingUseState == LiveRange::HasConsumingUse_t::Yes) {
11821214
return false;
11831215
}
@@ -1283,10 +1315,8 @@ bool SemanticARCOptVisitor::performGuaranteedCopyValueOptimization(CopyValueInst
12831315
// Otherwise, we know that our copy_value/destroy_values are all completely
12841316
// within the guaranteed value scope. So we /could/ optimize it. Now check if
12851317
// we were truly dead or if we are dead if we can eliminate phi arg uses. If
1286-
// we need to handle the phi arg uses, we bail. When we checked if the value
1287-
// was consumed, the hasConsumedUse code updated phiToIncomingValueMultiMap
1288-
// for us before returning its prognosis. After we reach a fixed point, we
1289-
// will try to eliminate this value then.
1318+
// we need to handle the phi arg uses, we bail. After we reach a fixed point,
1319+
// we will try to eliminate this value then.
12901320
if (hasUnknownConsumingUseState ==
12911321
LiveRange::HasConsumingUse_t::YesButAllPhiArgs) {
12921322
auto *op = lr.getSingleUnknownConsumingUse();
@@ -1320,7 +1350,7 @@ bool SemanticARCOptVisitor::performGuaranteedCopyValueOptimization(CopyValueInst
13201350

13211351
for (auto *succBlock : br->getSuccessorBlocks()) {
13221352
auto *arg = succBlock->getSILPhiArguments()[opNum];
1323-
phiToIncomingValueMultiMap.insert(arg, lr.getIntroducer());
1353+
joinedOwnedIntroducerToConsumedOperands.insert(arg, op);
13241354
}
13251355

13261356
return false;

0 commit comments

Comments
 (0)