Skip to content

Commit f22d085

Browse files
committed
Add a CanonicalOSSALifetime utility.
Canonicalizing OSSA provably minimizes the number of retains and releases within the boundaries of that lifetime. This eliminates the need for ad-hoc optimization of OSSA copies. This initial implementation only canonicalizes owned values, but canonicalizing guaranteed values is a simple extension. This was originally part of the CopyPropagation prototype years ago. Now OSSA is specified completely enough that it can be turned into a simple utility instead. CanonicalOSSALifetime uses PrunedLiveness to find the extended live range and identify the consumes on the boundary. All other consumes need their own copy. No other copies are needed. By running this after other transformations that affect OSSA lifetimes, we can avoid the need to run pattern-matching optimization to SemanticARC to recover from suboptimal patterns, which is not robust, maintainable, or efficient.
1 parent a6bce7e commit f22d085

File tree

6 files changed

+973
-672
lines changed

6 files changed

+973
-672
lines changed
Lines changed: 304 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,304 @@
1+
//===--- CanonicalOSSALifetime.h - Canonicalize OSSA lifetimes --*- C++ -*-===//
2+
//
3+
// This source file is part of the Swift.org open source project
4+
//
5+
// Copyright (c) 2014 - 2020 Apple Inc. and the Swift project authors
6+
// Licensed under Apache License v2.0 with Runtime Library Exception
7+
//
8+
// See https://swift.org/LICENSE.txt for license information
9+
// See https://swift.org/CONTRIBUTORS.txt for the list of Swift project authors
10+
//
11+
//===----------------------------------------------------------------------===//
12+
///
13+
/// Canonicalize the copies and destroys of a single owned or guaranteed OSSA
14+
/// value.
15+
///
16+
/// This top-level API rewrites the extended OSSA lifetime of a SILValue:
17+
///
18+
/// void canonicalizeValueLifetime(SILValue def, CanonicalOSSALifetime &)
19+
///
20+
/// The extended lifetime transitively includes the uses of `def` itself along
21+
/// with the uses of any copies of `def`. Canonicalization provably minimizes
22+
/// the OSSA lifetime and its copies by rewriting all copies and destroys. Only
23+
/// consusming uses that are not on the liveness boundary require a copy.
24+
///
25+
/// Example #1: Handle consuming and nonconsuming uses.
26+
///
27+
/// bb0(%arg : @owned $T, %addr : @trivial $*T):
28+
/// %copy = copy_value %arg : $T
29+
/// debug_value %copy : $T
30+
/// store %copy to [init] %addr : $*T
31+
/// debug_value %arg : $T
32+
/// debug_value_addr %addr : $*T
33+
/// destroy_value %arg : $T
34+
///
35+
/// Will be transformed to:
36+
///
37+
/// bb0(%arg : @owned $T, %addr : @trivial $*T):
38+
/// // The original copy is deleted.
39+
/// debug_value %arg : $T
40+
/// // A new copy_value is inserted before the consuming store.
41+
/// %copy = copy_value %arg : $T
42+
/// store %copy to [init] %addr : $*T
43+
/// // The non-consuming use now uses the original value.
44+
/// debug_value %arg : $T
45+
/// // A new destroy is inserted after the last use.
46+
/// destroy_value %arg : $T
47+
/// debug_value_addr %addr : $*T
48+
/// // The original destroy is deleted.
49+
///
50+
/// Example #2: Handle control flow.
51+
///
52+
/// bb0(%arg : @owned $T):
53+
/// cond_br %_, bb1, bb2
54+
/// bb1:
55+
/// br bb3
56+
/// bb2:
57+
/// debug_value %arg : $T
58+
/// %copy = copy_value %arg : $T
59+
/// destroy_value %copy : $T
60+
/// br bb3
61+
/// bb3:
62+
/// destroy_value %arg : $T
63+
///
64+
/// Will be transformed to:
65+
///
66+
/// bb0(%arg : @owned $T):
67+
/// cond_br %_, bb1, bb2
68+
/// bb1:
69+
/// destroy_value %arg : $T
70+
/// br bb3
71+
/// bb2:
72+
/// // The original copy is deleted.
73+
/// debug_value %arg : $T
74+
/// destroy_value %arg : $T
75+
/// br bb3
76+
/// bb2:
77+
/// // The original destroy is deleted.
78+
///
79+
/// FIXME: Canonicalization currently bails out if any uses of the def has
80+
/// OperandOwnership::PointerEscape. Once project_box is protected by a borrow
81+
/// scope and mark_dependence is associated with an end_dependence,
82+
/// canonicalization will work everywhere as intended. The intention is to keep
83+
/// the canonicalization algorithm as simple and robust, leaving the remaining
84+
/// performance opportunities contingent on fixing the SIL representation.
85+
///
86+
/// FIXME: Canonicalization currently fails to eliminate copies downstream of a
87+
/// ForwardingBorrow. Aggregates should be fixed to be Reborrow instead of
88+
/// ForwardingBorrow, then computeCanonicalLiveness() can be fixed to extend
89+
/// liveness through ForwardingBorrows.
90+
///
91+
//===----------------------------------------------------------------------===//
92+
93+
#ifndef SWIFT_SILOPTIMIZER_UTILS_CANONICALOSSALIFETIME_H
94+
#define SWIFT_SILOPTIMIZER_UTILS_CANONICALOSSALIFETIME_H
95+
96+
#include "llvm/ADT/DenseMap.h"
97+
#include "llvm/ADT/SetVector.h"
98+
#include "swift/SIL/SILInstruction.h"
99+
#include "swift/SILOptimizer/Utils/PrunedLiveness.h"
100+
101+
namespace swift {
102+
103+
/// Information about consumes on the extended-lifetime boundary. Consuming uses
104+
/// within the lifetime are not included--they will consume a copy after
105+
/// rewriting. For borrowed def values, the consumes do not include the end of
106+
/// the borrow scope, rather the consumes transitively include consumes of any
107+
/// owned copies of the borrowed value.
108+
///
109+
/// This result remains valid during copy rewriting. The only instructions
110+
/// referenced it contains are consumes that cannot be deleted.
111+
class CanonicalOSSAConsumeInfo {
112+
/// Map blocks on the lifetime boundary to the last consuming instruction.
113+
llvm::SmallDenseMap<SILBasicBlock *, SILInstruction *, 4> finalBlockConsumes;
114+
115+
/// Record any debug_value instructions found after a final consume.
116+
SmallVector<DebugValueInst *, 8> debugAfterConsume;
117+
118+
/// For borrowed defs, track per-block copies of the borrowed value that only
119+
/// have uses outside the borrow scope and will not be removed by
120+
/// canonicalization. These copies are effectively distinct OSSA lifetimes
121+
/// that should be canonicalized separately.
122+
llvm::SmallDenseMap<SILBasicBlock *, CopyValueInst *, 4> persistentCopies;
123+
124+
public:
125+
bool hasUnclaimedConsumes() const { return !finalBlockConsumes.empty(); }
126+
127+
void clear() {
128+
finalBlockConsumes.clear();
129+
debugAfterConsume.clear();
130+
persistentCopies.clear();
131+
}
132+
133+
bool hasFinalConsumes() const { return !finalBlockConsumes.empty(); }
134+
135+
void recordFinalConsume(SILInstruction *inst) {
136+
assert(!finalBlockConsumes.count(inst->getParent()));
137+
finalBlockConsumes[inst->getParent()] = inst;
138+
}
139+
140+
// Return true if this instruction is marked as a final consume point of the
141+
// current def's live range. A consuming instruction can only be claimed once
142+
// because instructions like `tuple` can consume the same value via multiple
143+
// operands.
144+
bool claimConsume(SILInstruction *inst) {
145+
auto destroyPos = finalBlockConsumes.find(inst->getParent());
146+
if (destroyPos != finalBlockConsumes.end() && destroyPos->second == inst) {
147+
finalBlockConsumes.erase(destroyPos);
148+
return true;
149+
}
150+
return false;
151+
}
152+
153+
/// Record a debug_value that is known to be outside pruned liveness. Assumes
154+
/// that instructions are only visited once.
155+
void recordDebugAfterConsume(DebugValueInst *dvi) {
156+
debugAfterConsume.push_back(dvi);
157+
}
158+
159+
ArrayRef<DebugValueInst *> getDebugInstsAfterConsume() const {
160+
return debugAfterConsume;
161+
}
162+
163+
bool hasPersistentCopies() const { return !persistentCopies.empty(); }
164+
165+
bool isPersistentCopy(CopyValueInst *copy) const {
166+
auto iter = persistentCopies.find(copy->getParent());
167+
if (iter == persistentCopies.end()) {
168+
return false;
169+
}
170+
return iter->second == copy;
171+
}
172+
173+
SWIFT_ASSERT_ONLY_DECL(void dump() const LLVM_ATTRIBUTE_USED);
174+
};
175+
176+
/// Canonicalize OSSA lifetimes.
177+
///
178+
/// Allows the allocation of analysis state to be reused across calls to
179+
/// canonicalizeValueLifetime().
180+
class CanonicalizeOSSALifetime {
181+
public:
182+
/// Find the original definition of a potentially copied value.
183+
///
184+
/// This use-def walk must be consistent with the def-use walks performed
185+
/// within the canonicalizeValueLifetime() implementation.
186+
static SILValue getCanonicalCopiedDef(SILValue v) {
187+
while (true) {
188+
if (auto *copy = dyn_cast<CopyValueInst>(v)) {
189+
v = copy->getOperand();
190+
continue;
191+
}
192+
return v;
193+
}
194+
}
195+
196+
private:
197+
/// If true, then debug_value instructions outside of non-debug
198+
/// liveness may be pruned during canonicalization.
199+
bool pruneDebug;
200+
201+
/// Current copied def for which this state describes the liveness.
202+
SILValue currentDef;
203+
204+
/// If an outer copy is created for uses outside the borrow scope
205+
CopyValueInst *outerCopy = nullptr;
206+
207+
/// Cumulatively, have any instructions been modified by canonicalization?
208+
bool changed = false;
209+
210+
/// Original points in the CFG where the current value's lifetime is consumed
211+
/// or destroyed. For guaranteed values it remains empty. A backward walk from
212+
/// these blocks must discover all uses on paths that lead to a return or
213+
/// throw.
214+
///
215+
/// These blocks are not necessarily in the pruned live blocks since
216+
/// pruned liveness does not consider destroy_values.
217+
SmallSetVector<SILBasicBlock *, 8> consumingBlocks;
218+
219+
/// Record all interesting debug_value instructions here rather then treating
220+
/// them like a normal use. An interesting debug_value is one that may lie
221+
/// outisde the pruned liveness at the time it is discovered.
222+
llvm::SmallDenseSet<DebugValueInst *, 8> debugValues;
223+
224+
/// Reuse a general worklist for def-use traversal.
225+
SmallSetVector<SILValue, 8> defUseWorklist;
226+
227+
/// Reuse a general worklist for CFG traversal.
228+
SmallSetVector<SILBasicBlock *, 8> blockWorklist;
229+
230+
/// Pruned liveness for the extended live range including copies. For this
231+
/// purpose, only consuming instructions are considered "lifetime
232+
/// ending". end_borrows do not end a liverange that may include owned copies.
233+
PrunedLiveness liveness;
234+
235+
/// Information about consuming instructions discovered in this caonical OSSA
236+
/// lifetime.
237+
CanonicalOSSAConsumeInfo consumes;
238+
239+
public:
240+
CanonicalizeOSSALifetime(bool pruneDebug) : pruneDebug(pruneDebug) {}
241+
242+
SILValue getCurrentDef() const { return currentDef; }
243+
244+
void initDef(SILValue def) {
245+
assert(consumingBlocks.empty() && debugValues.empty() && liveness.empty());
246+
consumes.clear();
247+
248+
currentDef = def;
249+
outerCopy = nullptr;
250+
liveness.initializeDefBlock(def->getParentBlock());
251+
}
252+
253+
void clearLiveness() {
254+
consumingBlocks.clear();
255+
debugValues.clear();
256+
liveness.clear();
257+
}
258+
259+
bool hasChanged() const { return changed; }
260+
261+
void setChanged() { changed = true; }
262+
263+
SILValue createdOuterCopy() const { return outerCopy; }
264+
265+
/// Top-Level API: rewrites copies and destroys within \p def's extended
266+
/// lifetime. \p lifetime caches transient analysis state across multiple
267+
/// calls.
268+
///
269+
/// Return false if the OSSA structure cannot be recognized (with a proper
270+
/// OSSA representation this will always return true).
271+
///
272+
/// Upon returning, isChanged() indicates, cumulatively, whether any SIL
273+
/// changes were made.
274+
///
275+
/// Upon returning, createdOuterCopy() indicates whether a new copy was
276+
/// created for uses outside the borrow scope. To canonicalize the new outer
277+
/// lifetime, call this API again on the value defined by the new copy.
278+
bool canonicalizeValueLifetime(SILValue def);
279+
280+
protected:
281+
void recordDebugValue(DebugValueInst *dvi) {
282+
debugValues.insert(dvi);
283+
}
284+
285+
void recordConsumingUse(Operand *use) {
286+
consumingBlocks.insert(use->getUser()->getParent());
287+
}
288+
289+
bool computeBorrowLiveness();
290+
291+
void consolidateBorrowScope();
292+
293+
bool computeCanonicalLiveness();
294+
295+
void findOrInsertDestroyInBlock(SILBasicBlock *bb);
296+
297+
void findOrInsertDestroys();
298+
299+
void rewriteCopies();
300+
};
301+
302+
} // end namespace swift
303+
304+
#endif

include/swift/SILOptimizer/Utils/PrunedLiveness.h

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -139,6 +139,8 @@ class PrunedLiveBlocks {
139139

140140
void clear() { liveBlocks.clear(); SWIFT_ASSERT_ONLY(seenUse = false); }
141141

142+
unsigned numLiveBlocks() const { return liveBlocks.size(); }
143+
142144
void initializeDefBlock(SILBasicBlock *defBB) {
143145
assert(!seenUse && "cannot initialize more defs with partial liveness");
144146
markBlockLive(defBB, LiveWithin);
@@ -204,10 +206,17 @@ class PrunedLiveness {
204206
users.clear();
205207
}
206208

209+
unsigned numLiveBlocks() const { return liveBlocks.numLiveBlocks(); }
210+
207211
void initializeDefBlock(SILBasicBlock *defBB) {
208212
liveBlocks.initializeDefBlock(defBB);
209213
}
210214

215+
/// For flexibility, \p lifetimeEnding is provided by the
216+
/// caller. PrunedLiveness makes no assumptions about the def-use
217+
/// relationships that generate liveness. For example, use->isLifetimeEnding()
218+
/// cannot distinguish the end of the borrow scope that defines this extended
219+
/// live range vs. a nested borrow scope within the extended live range.
211220
void updateForUse(Operand *use, bool lifetimeEnding);
212221

213222
PrunedLiveBlocks::IsLive getBlockLiveness(SILBasicBlock *bb) const {

lib/SIL/IR/OperandOwnership.cpp

Lines changed: 4 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -314,8 +314,7 @@ FORWARDING_OWNERSHIP(LinearFunction)
314314
#undef FORWARDING_OWNERSHIP
315315

316316
// Arbitrary value casts are forwarding instructions that are also allowed to
317-
// propagate Unowned values. If the result is Unowned, then the operand must
318-
// also be Unowned.
317+
// propagate Unowned values.
319318
#define FORWARDING_ANY_OWNERSHIP(INST) \
320319
OperandOwnership OperandOwnershipClassifier::visit##INST##Inst( \
321320
INST##Inst *i) { \
@@ -414,7 +413,7 @@ static OperandOwnership getFunctionArgOwnership(SILArgumentConvention argConv,
414413
// owned values may be passed to guaranteed arguments without an explicit
415414
// borrow scope in the caller. In contrast, a begin_apply /does/ have an
416415
// explicit borrow scope in the caller so we must treat arguments passed to it
417-
// as being borrowed for the entire borrow scope.
416+
// as being borrowed for the entire region of coroutine execution.
418417
case SILArgumentConvention::Indirect_In_Constant:
419418
case SILArgumentConvention::Indirect_In_Guaranteed:
420419
case SILArgumentConvention::Direct_Guaranteed:
@@ -780,10 +779,8 @@ OperandOwnership OperandOwnershipClassifier::visitBuiltinInst(BuiltinInst *bi) {
780779
//===----------------------------------------------------------------------===//
781780

782781
OperandOwnership Operand::getOperandOwnership() const {
783-
// NOTE: NonUse distinguishes itself from InstantaneousUse because it does not
784-
// require liveness. Discrimating such uses in the enum avoids the need to
785-
// return an Optional<OperandOwnership>::None, which could be confused with
786-
// OwnershipKind::None.
782+
// A type-dependent operant is a NonUse (as opposed to say an
783+
// InstantaneousUse) because it does not require liveness.
787784
if (isTypeDependent())
788785
return OperandOwnership::NonUse;
789786

0 commit comments

Comments
 (0)