Andy and I for some time have been discussing the right name for these two
"ownership concepts". What we realized is that the "ing" on
BorrowScopeIntroducingValue is very unfortunate since this value is the result
of a new borrow scope being introduced. So the name should be really:
BorrowScopeIntroducedValue. Given that is sort of unsatisfying, we settled on
the name BorrowedValue.
Once we found the name BorrowedValue, we naturally realized that
BorrowScopeOperand -> BorrowingOperand followed. This is because the operand is
the operand of the instruction that is creating the new borrow scope. So in a
sense the Operand is the "Use" that causes the original value to become
borrowed. So a BorrowingOperand is where the action is and is "active".
This means that it can only have a guaranteed object as an operandand that we
validate that all uses of the result address of open_existential_box occur only
within the lifetime of said object's borrow scope.
This is the most simple initial version that I can commit. The hope is that this will help to bring this up in a nice way.
I am going to handle the multiple phi node and load [copy] case later to reduce
code churn.
<rdar://problem/56720436>
These enable robust walking up the def-use ossa graph to find the value that
introduces an independent underlying owned value, looking through forwarding
insts.
This is a similar concept to "rc-identity" that we talk about in the low level
ARC optimizer. The main idea is that all owned values in a program can be
partitioned into two different kinds of values:
1. Introducer values that exist independently of any other local values in the
function. It is a point of truth from which the owned objects lifetime extends
from and is in a certain sense an initialization (in a category theoretic sense)
in the lifetime of the underlying object that we are manipulating.
2. Forwarding values do not represent an object lifetime that is "truly"
independent of any other value locally: its liveness comes from passing on
liveness from some introducer or some other forwarding value.
The reason why I am adding this new construct is that I am beginning to
implement a new form of ARC optimization that enables us to convert @owned SIL
phi arguments to @guaranteed phi arguments. As part of that, I need to have a
way to in a systematic way finding the underlying incoming values (using the
logic used to determine forwarding in the ownership verifier).
This is the first part of that effort, defining the ontology we are going to
work with. Keep in mind this is just a seed ontology, if I missed any "owned
introducers" (I am sure I did), feel free to add them.
Currently, we only classify ref_element_addr and ref_tail_addr. But we should
expand this to project_box, project_existential_box and open_existential_box as
well.
The only reason why BranchPropagatedUser existed was because early on in SIL, we
weren't sure if cond_br should be able to handle non-trivial values in
ossa. Now, we have reached the point where we have enough experience to make the
judgement that it is not worth having in the representation due to it not
holding its weight.
Now that in ToT we have banned cond_br from having non-trivial operands in ossa,
I can just eliminate BranchPropagatedUser and replace it with the operands that
we used to construct them!
A few notes:
1. Part of my motiviation in doing this is that I want to change LiveRange to
store operands instead of instructions. This is because we are interested in
being able to understand the LiveRange at a use granularity in cases where we
have multiple operands. While doing this, I discovered that I needed
SILInstructions to use the Linear Lifetime Checker. Then I realized that now was
the time to just unwind BranchPropagatedUser.
2. In certain places in SemanticARCOpts, I had to do add some extra copies to
transform arrays of instructions from LiveRange into their operand form. I am
going to remove them in a subsequent commit when I change LiveRange to work on
operands. I am doing this split to be incremental.
3. I changed isSingleInitAllocStack to have an out array of Operand *. The only
user of this code is today in SemanticARCOpts and this information is fed to the
Linear Lifetime Checker, so I needed to do it.
OwnershipUtils.h is growing a bit and I want to use it to store abstract higher
level utilities for working with ossa. LinearLifetimeChecker is just a low level
detail of that, so it makes sense to move it out now.
Specifically, I got rid of the constructors for BorrowScopeIntroducingValue and
made it so one can only construct BorrowScopeIntroducingValue from the static
constructor.
I also changed the place where we mapped values from BorrowScopeIntroducingValue
-> BorrowScopeIntroducingValueKind. This simplifies the whole thing.
I added a new API into OwnershipUtils called
getSingleBorrowIntroducingValue. This API returns a single
BorrowScopeIntroducingValue for a passed in guaranteed value. If we can not find
such a BorrowScopeIntroducingValue or we find multiple such, we return None.
Using that, I refactored StorageGuaranteesLoadVisitor::visitClassAccess(...) to
use this new API which should make it significantly more robust since the
routine uses the definitions of "guaranteed forwarding" that the ownership
verifier uses when it verifies meaning that we can rely on the routine to be
exhaustive and correct. This means that we now promote load [copy] ->
load_borrow even if our borrow scope introducer feeds through a switch_enum or
checked_cast_br result (the main reason I looked into this change).
To create getSingleBorrowIntroducingValue, I refactored
getUnderlyingBorrowIntroucingValues to use a generator to find all of its
underlying values. Then in getSingleBorrowIntroducingValue, I just made it so
that we call the generator 1-2 times (as appropriate) to implement this query.
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.
I also added a comment to getAllBorrowIntroducingValues(...) that explained the
situations where one could have multiple borrow introducing values:
1. True phi arguments.
2. Aggregate forming instructions.
This patch implements movable guaranteed scopes in ossa. This pattern is
currently not generated anywhere in the compiler, but my hope is to begin
emitting these in SemanticARCOpts. The idea is that these model true phi nodes
and thus can be used to fuse multiple guaranteed scopes into one using br
instructions. This is treated similarly to how owned instructions are forwarded
through /all/ terminators. This will enable us to use the SILSSAUpdater with
guaranteed arguments as well as enable the expression of sets of borrow scopes
that minimally jointly-dominate a guaranteed argument. This will enable us to
express +0 merge points like the following:
```
bb1:
%0a = begin_borrow %0 : $Klass
br bb3(%0a : $Klass)
bb2:
%1a = load_borrow %1 : $*Klass
br bb3(%1a : $Klass)
bb3(%2 : @guaranteed $Klass)
...
end_borrow %2 : $Klass
```
I describe below what the semantics of guaranteed block arguments were
previously, what they are now, and a little bit of interesting things from a
semantic perspective around implicit sub-scope users.
Before this patch in ossa, guaranteed block arguments had two different sets of
semantics:
1. Given a checked_cast_br or a switch_enum, the guaranteed block argument was
treated like a forwarding instruction. As such, the guaranteed argument's did
not require an end_borrow and its uses were validated as part of the use list
of the switch_enum/checked_cast_br operand's borrow introducer. It also was
not classified as a BorrowScopeValueIntroducer since it was not introducing a
new scope.
2. Given any other predecessor terminator, we treated the guaranteed argument as
a complete sub-scope of its incoming values. Thus we required the guaranteed
argument to have its lifetime eneded by an end_borrow and that all incoming
values of the guaranteed argument to come from a borrow introducer whose set
of jointly post-dominating end_borrows also jointly post-dominates the set of
end_borrows associated with the guaranteed argument itself. Consider the
following example:
```
bb0:
%1 = begin_borrow %foo : $Foo // (1)
%2 = begin_borrow %foo2 : $Foo2 // (2)
cond_br ..., bb1, bb2
bb1:
br bb3(%1 : $Foo)
bb2:
br bb3(%2 : $Foo)
bb3(%3 : @guaranteed $Foo)
...
end_borrow %3 : $Foo // (3)
end_borrow %2 : $Foo // (4)
end_borrow %1 : $Foo // (5)
...
```
Notice how due to SSA, (1) and (2) must dominate (4) and (5) and thus must
dominate bb3, preventing the borrows from existing within bb1, bb2.
This dominance property is actively harmful to expressivity in SIL since it
means that guaranteed arguments can not be used to express (without contortion)
sil code patterns where an argument is jointly-dominated by a minimal set of
guaranteed incoming values. For instance, consider the following SIL example:
```
bb0:
cond_br ..., bb1, bb2
bb1:
%0 = load [copy] %globalAddr : $Foo
br bb3(%0 : $Foo)
bb2:
%1 = copy_value %guaranteedFunctionArg : $Foo
br bb3(%1 : $Foo):
bb3(%2 : @owned $Foo):
apply %useFoo(%2)
destroy_value %2 : $Foo
```
As a quick proof: Assume the previous rules for guaranteed arguments. Then to
promote the load [copy] -> load_borrow and the copy_value to a begin_borrow, we
would need to place an end_borrow in bb3. But neither bb1 or bb2 dominates bb3,
so we would violate SSA dominance rules.
To enable SIL to express this pattern, we introduce a third rule for terminator
in ossa that applies only to branch insts. All other branches that obeyed the
previous rules (cond_br), still follow the old rule. This is not on purpose, I
am just being incremental and changing things as I need to. Specifically,
guaranteed arguments whose incoming values are defined by branch instructions
now act as a move on guaranteed values. The intuition here is that these
arguments are acting as true phis in an SSA sense and thus are just new names
for the incoming values. This implies since it is just a new name (not a
semantic change) that the guaranteed incoming value's guaranteed scopes should
be fused into one scope. The natural way to model this is by treating branch
insts as consuming guaranteed values. This then lets us express the example
above without using copies as follows:
```
bb0:
cond_br ..., bb1, bb2
bb1:
%0 = load_borrow %globalAddr : $Foo
br bb3(%0 : $Foo) // consumes %0 and acts as %0's end_borrow.
bb2:
// We need to introduce a new begin_borrow here since function
// arguments are required to never be consumed.
%1 = begin_borrow %guaranteedFunctionArg : $Foo
br bb3(%1 : $Foo) // consumes %1 and acts as %1's end_borrow
// %2 continues the guaranteed scope of %0, %1. This time fused with one name.
bb3(%2 : @guaranteed $Foo):
apply %useFoo(%2)
// End the lifetime of %2 (which implicitly ends the lifetime of %0, %1).
end_borrow %2 : $Foo
...
```
The main complication for users is that now when attempting to discover the set
of implicit users on an owned or guaranteed value caused by their usage as an
argument of a borrow introducer like begin_borrow. For those who are unaware, a
begin_borrow places an implicit requirement on its parent value that the parent
value is alive for the entire part of the CFG where this begin_borrow is
live. Previously, one could just look for the end_borrows of the
begin_borrow. Now one must additionally look for consuming branch insts. This is
because the original value that is being borrowed from must be alive over the
entire web of guaranteed values. That is the entire web of guaranteed values act
as a liveness requirement on the begin_borrow's operand.
The way this is implemented is given a use that we are validating, if the use is
a BorrowScopeOperand (1), we see if the borrow scope operand consumes the given
guaranteed scope and forwards it into a borrow scope introducer. If so, we add
the list of consuming uses of the borrow scope introducer to the worklist to
visit and then iterate.
In order to avoid working with cycles, for now, the ownership verifier bans
liveness requiring uses that have cycles in them. This still allows us to have
loop carried guaranteed values.
(1) A BorrowScopeOperand is a concept that represents an operand to a SIL
instruction that begins a guaranteed scope of some sort. All BorrowScopeOperand
are thus at a minimum able to compute a compile time the static region in which
they implicitly use their operands. NOTE: We do not require the scope to be
represented as a SILValue in the same function.
We achieve some nice benefit by introducing this. Specifically:
1. We can optimize the pattern I mentioned above. This is a common pattern in
many frameworks that want to return a default object if a computation fails
(with the default object usually being some sort of global or static
var). This will let us optimize that case when the global is a let global.
2. The SSA Updater can now be used with guaranteed values without needing to
introduce extra copies. This will enable predictable mem opts to introduce
less copies and for semantic arc opts to optimize the remaining copies that
PMO exposes but does not insert itself.
rdar://56720519
A branch propagated user that isn't a cond_br is layout compatible with a
SILInstruction *. This helper function converts from ArrayRef<SILInstruction *>
-> ArrayRef<BranchPropagatedUser> but also in asserts builds checks that our
invariant (namely all of the 'SILInstruction *' are not cond_br.
All non cond_br SILInstructions are layout compatible with BranchPropagatedUser
since BPU just stores a PointerIntPair that leaves its low bits as zero unless
one adds a cond_br. Since in these cases, the SILInstructions are all not
cond_br, we can use this alternative API.
I also added some asserts to validate that the assumption that all such
SILInstructions are not cond_br is respected.
By convention, most structs and classes in the Swift compiler include a `dump()` method which prints debugging information. This method is meant to be called only from the debugger, but this means they’re often unused and may be eliminated from optimized binaries. On the other hand, some parts of the compiler call `dump()` methods directly despite them being intended as a pure debugging aid. clang supports attributes which can be used to avoid these problems, but they’re used very inconsistently across the compiler.
This commit adds `SWIFT_DEBUG_DUMP` and `SWIFT_DEBUG_DUMPER(<name>(<params>))` macros to declare `dump()` methods with the appropriate set of attributes and adopts this macro throughout the frontend. It does not pervasively adopt this macro in SILGen, SILOptimizer, or IRGen; these components use `dump()` methods in a different way where they’re frequently called from debugging code. Nor does it adopt it in runtime components like swiftRuntime and swiftReflection, because I’m a bit worried about size.
Despite the large number of files and lines affected, this change is NFC.
This just follows automagically from the recent work that I did with introducing
BorrowScopeIntroducingOperands.
I confirmed locally that the negative tests do not fail as expected when
visiting the destroys of the guaranteed parameters.
For now this only is used for ensuring that an owned value that has a
begin_borrow use, adds the end_borrows matched to the begin_borrow to the owned
values implicit regular user list.
I am going to use this same functionality to model the requirements around
begin_apply and mark_dependence (when I add an end_mark_dependence instruction).
This commit only changes how BranchPropagatedUser is constructed and does not
change the internal representation. This is a result of my noticing that
BranchPropagatedUser could also use an operand internally to represent its
state. To simplify how I am making the change, I am splitting the change into
two PRs that should be easy to validate:
1. A commit that maps how the various users of BranchPropagatedUser have been
constructing BPUs to a single routine that takes an Operand. This leaves
BranchPropagatedUser's internal state alone as well as its user the Linear
Lifetime Checker.
2. A second commit that changes the internal bits of the BranchPropagatedUser to
store an Operand instead of a PointerUnion.
This will allow me to use the first commit to validate the second.
This is the first in a series of patches that begin to hide the underlying
linear lifetime implementation underneath the facade of the linear lifetime
checker. I only updated the users that this applies to.
This is just a simple refactoring commit in preparation for hiding more of the
details of the linear lifetime checker. This is NFC, just moving around code.
The idea is that this can be used to work with things like load_borrow,
begin_borrow, SILFunctionArgument, results of begin_apply(in the future) and the like in a
generic way using exhaustive switches to make sure this code stays up to date.
I refactored code in SemanticARCOpts and some utilities in OwnershipUtils.cpp to
use these new APIs. The code looks a lot nicer and should be quite easy to
expand to handle new borrow introducers (e.x.: end_apply).
Instructions that start a scope should have a (discoverable) method
that retrieves the end of scope. This is a basic structural property
of the instruction.
I removed the makeEndBorrowRange helper because it adds overall
complexity and doesn't provide any value. If some code wants to be
generic over BeginBorrow/LoadBorrow, then that code should have it's
own trivial generic helper:
EndBorrowRange getEndBorrows<T>(T *beginBorrow) {
return beginBorrow->getEndBorrows()
}
This reduces the diff in between -Onone output when stripping before/after
serialization.
We support load_borrow by translating it to the load [copy] case. Specifically,
for +1, we normally perform the following transform.
store %1 to [init] %0
...
%2 = load [copy] %0
...
use(%2)
...
destroy_value %2
=>
%1a = copy_value %1
store %1 to [init] %0
...
use(%1a)
...
destroy_value %1a
We analogously can optimize load_borrow by replacing the load with a
begin_borrow:
store %1 to [init] %0
...
%2 = load_borrow %0
...
use(%2)
...
end_borrow %2
=>
%1a = copy_value %1
store %1 to [init] %0
...
%2 = begin_borrow %1a
...
use(%2)
...
end_borrow %2
destroy_value %1a
The store from outside a loop being used by a load_borrow inside a loop is a
similar transformation as the +0 version except that we use a begin_borrow
inside the loop instead of a copy_value (making it even more efficient).
I am starting to use the linear lifetime checker in an optimizer role where it
no longer asserts but instead tells the optimizer pass what is needed to cause
the lifetime to be linear. To do so I need to be able to return richer
information to the caller such as whether or not a leak, double consume, or
use-after-free occurs.
Originally I wanted to try to use this code to optimize on ownership sil without
assuming verification passed (since the utility would check verification). In
the end, this was not useful.
A common pattern when working in ossa is the introduction of a copy for lifetime
reasons at a dominating point in a program with a single use that may not
post-dominate the use. This API gives a "tight lifetime" bound by returning the
blocks where the value needs to be destroyed to prevent leaks.
NOTE: I was not 100% sure if consume errors should still assert. It seems like a
double consume error would be user error, so an assert is probably fine.
Last week I split out operand ownership classification from the
SILOwnershipVerifier into the OperandOwnershipKindClassifier. Now move that
classifier code to another file so that SILOwnershipVerifier.cpp just consists
of the actual checker code. This makes sense since this type of classifier is
describing a separate structural aspect of SIL rather than something intrinsic
to the ownership verifier.
Keep in mind that this is not the final form of this classifier. Just an
incremental step forward.
NOTE: This is not the final form of how operand ownership restraints will be
represented. This patch is instead an incremental change that extracts out this
functionality from the ownership verifier as a pure refactor.
rdar://44667493
This does not eliminate the entrypoints on SILBuilder yet. I want to do this in
two parts so that it is functionally easier to disentangle changing the APIs
above SILBuilder and changing the underlying instruction itself.
rdar://33440767
I am doing this for two reasons:
1. I am going to use this in function signature opts to prove that a guaranteed
parameter is "consumed".
2. It puts the ownership verifier on a diet.
rdar://38196046