-
Notifications
You must be signed in to change notification settings - Fork 10.6k
[ownership] Implement movable guaranteed scopes in ossa. #29875
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
[ownership] Implement movable guaranteed scopes in ossa. #29875
Conversation
|
@swift-ci test |
|
I am not expecting this to result in any /true/ differences since I don't think any passes produce this today. |
|
@swift-ci benchmark |
|
@swift-ci test source compatibility |
Performance: -OCode size: -OPerformance: -Osize
Code size: -OsizePerformance: -OnoneCode size: -swiftlibsHow to read the dataThe tables contain differences in performance which are larger than 8% and differences in code size which are larger than 1%.If you see any unexpected regressions, you should consider fixing the Noise: Sometimes the performance results (not code size!) contain false Hardware Overview |
|
Build failed |
|
Build failed |
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
The main change is that we do not eliminate end_borrows when propagating guaranteed phis. This is because phis now forward guaranteed ownership like owned ownership and since we only eliminate these arguments if all incomign values to the argument is the same (providing dominance).
c98c88c to
a084c54
Compare
|
@swift-ci test |
2 similar comments
|
@swift-ci test |
|
@swift-ci test |
|
I think my commit msg got mangled in a funky way. I need to repost. |
a084c54 to
d66d329
Compare
|
Fixed. |
|
@swift-ci test |
3 similar comments
|
@swift-ci test |
|
@swift-ci test |
|
@swift-ci test |
|
Build failed |
|
@swift-ci source compatibility |
1 similar comment
|
@swift-ci source compatibility |
|
@swift-ci test source compatibility |
|
Same failures on Debug source compatibility as on the bots, so it isn't me. See: |
|
Same on the release side. Merging! |
|
The description in the PR makes perfect sense. This is how I imagined |
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:
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:
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.
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:
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:
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:
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:
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.
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
Replace this paragraph with a description of your changes and rationale. Provide links to external references/discussions if appropriate.
Resolves SR-NNNN.