Specifically if we have a begin_access [read] or a begin_access [modify] that is
never actually written to. In that case if we can prove that the load [copy] is
completely within the exclusive access region, we will load_borrow. Example:
```
%0 = begin_access [read] ...
%1 = load [copy] %0
...
destroy_value %1
end_access %0
```
=>
```
%0 = begin_access [read] ...
%1 = load_borrow %0
...
end_borrow %1
end_access %0
```
rdar://60064692
This is just better information to have since one wants to not only know the
instruction, but also the specific value used on the instruction since behavior
can vary depending upon that. The operand is what ties the two together so it is
a natural fit.
Now that this is done, I am going to work on refactoring out converting a
LiveRange from @owned -> @guaranteed. It will be a consuming operation using a
move operator since once the transformation has completed, the LiveRange no
longer exists.
I need this refactored functionality since I am going to need it when
eliminating phi-webs.
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.
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 looked when building the stdlib/overlays and the vast majority of uses were < 2 for all
of these values, so it makes sense to shrink them.
Part of the reason I am doing this is that I think the pass is starting to
coalesce a little bit and also I want to start optimizing owned phi arguments so
I am going to need to start storing these. I don't want to be storing such large
small arrays in any data structure anywhere.
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.
Otherwise, on the face of it we do not invalidate analyses as we should. That
being said, I do not think we actually could hit this in practice today since a
trivially dead instruction can only enter the worklist as a result of us
removing some sort of other instruction causing madeChange to be already true.
That being said, I would rather not rely on that in the face of future
change. Found via site reading code.
Now, guaranteed phi args can also consume begin_borrow. This means our simple
analysis here is not sufficient. I am going to add support for this in a
forthcoming commit.
Optimizations are still not creating guaranteed phi arguments, so no breakage
can result.