I'm sending out an updated version of my notes on SIL redesign in support of reasoning about lifetimes, including ownership and borrowing. I sent a very preliminary version of these notes a year ago. Since then, a lot has changed. Work on the SIL representation has been going on "in the background" in several different areas. It's sometimes hard to understand how it all relates to each other. -Andy |
# SIL implementation of ownership and borrowing SIL representation is going through an evolution in several areas: Ownership SSA (OSSA), @guaranteed calling convention, lifetime guarantees, closure conventions, opaque values, borrowing, exclusivity, and __shared parameter modifiers. These are all loosely related, but it's important to understand how they fit together. Here are some notes that I gathered and found useful to write down. (Coroutines support is also related, but I'm avoiding that topic for now.) Disclaimer: None of this is authoritative. I hope that any misunderstandings and misstatements will be corrected by reviewers.
## Borrowed (shared) Swift values
I'm not sure exactly what borrowing will look like in the language,
but conceptually there are two things to support:
- borrow y = x
- foo(borrow x)
Both of these operations imply the absence of a copy and read-only
access to an immutable value within some scope: either the borrowed
variable's scope or the statement's scope. Exclusivity enforcement
guarantees that no writes to the borrowed variable occur within the
scope, thereby ensuring immutability without copying.
For copyable types, the implementation can always introduce a copy
without changing formal program behavior. However, exclusivity still
needs to be enforced to preserve the borrow's original
semantics. Adding a copy would ensure local immutability, but removing
exclusivity would broaden the set of legal programs--it's not a source
compatible change.
Maybe the user doesn't care about formal semantics and only requested
a `borrow` as a performance assurance. Exclusivity should still be
enforced because it is the only way to reliably avoid copying and
provide that assurance.
In short, `borrow => exclusively-immutable and no copy`.
As such, the entire borrow scope will initially need to be
protected by SIL exclusivity markers: `begin_access [read] /
end_access`:
```
%x_address = project_box ...
// initialize %x_address
%x_access = begin_access [read] %x_address
apply @foo(%x_access) : $(@in_guaranteed) -> ()
end_access %x_access
```
If the boxed variable is promoted to a SILValue, the exclusivity
markers can be trivially eliminated. This is valid because promoting
to a SILValue requires static proof of exclusive-immutability. All of
the above instructions can be eliminated except the call itself:
```
apply @foo(%x) : $(@in_guaranteed) -> ()
```
Where `%x` is the value that `%x_address` was initialized to. The only
problem is that this breaks the calling convention. The original SIL
passes an address as the `@in_guaranteed` argument and the optimized SIL
passes a value. They can't both be right. Knowing which to fix depends
on the type of the borrowed variable.
The `@in_guaranteed` parameter convention passes the value by address,
which is required for address-only types. Types that are opaque due to
generic abstraction or resilience are address-only. Some move-only
types are also address-only.
If the type is *not* address-only (neither borrowed-move-only nor opaque), then
it's possible to pass the value to a function with a direct @guaranteed
parameter convention:
```
apply @foo(%x) : $(@guaranteed AnyObject) -> ()
```
With a direct convention, we cannot pass a SIL address, so the original
SIL would need to be written as:
```
%x_address = project_box ...
%x_access = begin_access [read] %x_address : $*AnyObject
%x_borrow = load_borrow %x_address : $*AnyObject
apply @foo(%x_borrow) : $(@guaranteed AnyObject) -> ()
end_borrow %x_borrow : $AnyObject
end_access %x_access
```
We still need to solve the problem of address-only types. A new
feature called SIL opaque values does that for us. It allows
address-only types to be operated on in SIL just like loadable
types. The only difference is that indirect parameter conventions are
required. With SIL opaque values enabled, the original code will be:
```
%x_address = project_box ...
%x_access = begin_access [read] %x_address : $*T
%x_borrow = load_borrow %x_address : $*T
apply @foo(%x_borrow) : $(@in_guaranteed T) -> ()
end_borrow %x_borrow : $T
end_access %x_access : $*T
```
And the code can now be optimized just like before:
```
apply @foo(%x) : $(@in_guaranteed T) -> ()
```
`load_borrow` is an OSSA feature that allows an addressable formal
memory location to be viewed as an SSA value *without* a separate
lifetime. But it has a secondary role. It also allows the
compiler to view the borrowed memory address as being reserved
for that borrowed value up to the `end_borrow`. `begin_access [read]`
only enforces the language level guarantee the formal memory is
exclusively immutable. `load_borrow` provides a SIL-level guarantee
that physical memory remains exclusively immutable. So while
`begin_access` can be eliminated once its conditions are satisfied,
`load_borrow` must remain until address-only values are lowered to a
direct representation of the ABI, otherwise known "address
lowering". With a normal `load`, the compiler would
still need to bitwise copy the value `%x_borrow` when passing it
`@in_guaranteed`. Not only does that defeat a performance goal, but it
is not possible for some types, such as opaque move-only types, or,
hypothetically, move-only types with "mutable" properties, as required
for atomics.
Ownership SSA (OSSA) form also introduces `begin_borrow / end_borrow`
scopes for SILValues, but those are mostly unrelated to borrowing at
the language level. SILValue borrowing is not necessary to represent
borrowed Swift values (the `load_borrow` is no longer needed once the
Swift value at `%x_address` is promoted to the SILValue
`%x`). `begin_borrow / end_borrow` scopes *are* currently used in
situations that are unrelated to Swift borrowing, as explained in
below in the OSSA section.
> The naming conflict between borrowed Swift values and borrowed
> SILValues should eventually be resolved. For example, language
> review may decide that "shared" is a better name than "borrowed". I
> use the `borrow` here because I don't want to create more confusion
> surrounding our existing (unsupported) `__shared` parameter
> modifier, which does not currently have semantics that I described
> above.
## @guaranteed (aka +0) parameter ABI
In the previous section, we saw that a `@guaranteed` convention is
required to implement borrowed Swift values simply because the
borrowed value cannot be copied or destroyed within the borrow
scope. Beyond that, the calling convention is purely an ABI feature
unrelated to borrowing at the language level.
`@guaranteed` (+0) and `@owned` (+1) conventions determine where
copies are needed but otherwise have no effect on variable
lifetimes. In the `@owned` case, lifetime ends before the return,
and in the `@guaranteed` case it ends after the return. From the point
of view of both the user and the optimizer, this is semantically the
same lifetime. In other words, manually inlining an unadorned pure
Swift function cannot affect formal program behavior.
> SILValues are currently wrapped in `begin_borrow` before being
> passed `@guaranteed`, but there's no reason to do that other than to
> avoid inserting the `begin_borrow` during inlining, and in fact it
> contradicts the purpose of `begin_borrow` as described in the OSSA
> section.
It is up to the compiler to decide which convention to use for
parameters in a given position with a given type. Sometimes `@owned`
is clearly best (initializers). Sometimes `@guaranteed` is clearly
best (self, stdlib protocols, closures). Since those decisions
are about to become ABI, they're being evaluated carefully. Regardless
of the outcome, programmers need a way to override the convention with
a parameter attribute or modifier.
## __shared parameter modifier
The `__shared` parameter modifier simply forces the compiler to
choose the `@guaranteed` (+0) convention. This is unsupported but
important for evaluating alternative ABI decisions and preparing for
ABI compatibility with move-only types.
In the first section, I defined a hypothetical `borrow` caller-side
modifier. To understand the difference between a `__shared` convention
and a `borrowed` value consider this example:
```
func foo<T>(_ t: __shared T, _ f: ()->()) {
f()
print(t)
}
func bar<T>(_ t: inout T) { ... }
func test<T>(_ t: T) {
var t = move(t)
foo(t) { bar(&t) }
}
```
This is legal code today, but `t` will be copied when passed to
`foo`. This likely defies the user's expectation that they see the
value printed after modification by `bar`.
If `T` is ever a move-only type, this will simply be undefined without
additional exclusivity enforcement. Requiring that a variable be
`borrowed` before being passed `__shared` catches that:
```
func test<T>(_ t: T) {
var t = move(t)
foo(borrow t) { bar(&t) }
}
```
Now this is a static compiler error.
Of course, we could introduce an implicit borrow whenever move-only
types are passed `__shared`. But I believe that is too subtle and
misleading of a rule to expose to users.
This becomes much simpler if `@guaranteed` is the default
convention that users can override with a `__consumed` parameter
modifier. The allowable argument and parameter pairings would be:
- non-borrowed value -> __consumed parameter
- borrowed value -> default parameter
- copyable value -> default parameter
## Ownership SSA (OSSA)
Michael Gottesman is working on extensively documenting this
feature. Briefly, a SILValue is a singly defined node in an SSA graph
that represents one instance of a Swift value. With OSSA, each
SILValue now has an independent lifetime--it has "ownership" of its
lifetime. The SILValue's lifetime must be ended by destroying the
value (e.g. decrementing the refcount), or moving the value into
another SILValue (e.g. passing an @owned argument).
A SILValue can be "borrowed" across some program scope via
`begin/end_borrow`. The borrowed value can then be "projected" out into
subobjects or cast to another type. The original value cannot be
destroyed within the borrow scope. This representation allows trivial
lifetime analysis. There's no need to reason about projections, casts
and the like. That's all hidden by the borrow scope.
So, SILValue borrowing isn't required for modeling language level
semantics. Instead, it's a convenient way to verify that SIL
transformations obey the rules of OSSA. In fact, these instructions
are trivially removed as soon as the compiler no longer needs to
verify OSSA.
What do we mean by OSSA rules? Here's a quick summary.
The users of SILValues can be divided into these groups.
Uses independent of ownership:
U1. Use the value instantaneously (`copy_value`, `@guaranteed` argument)
U2. Escape the nontrivial contents of the value (`ref_to_unowned`,
`ref_to_rawpointer`, `unchecked_trivial_bitcast`)
Uses that require an owned value:
O3. Propagate the value without consuming it (`mark_dependence`, `begin_borrow`)
O4. Consume the value immediately (`store`, `destroy`, `@owned` argument)
O5. Consume the value indirectly via a move (`tuple`, `struct`)
Uses that require a borrowed value:
B6. Project the borrowed value (`struct_extract`, `tuple_extract`,
`ref_element_addr`, `open_existential_value`)
`begin_borrow` is only needed to handle uses in (B6). Support for
`struct_extract` and `tuple_extract` was the most compelling need for
`begin_borrow`. It maybe be best though to eliminate those
instructions instead using a more OSSA-friendly `destructure`. Doing
this would enable normal optimization of tuples and struct copies.
`begin_borrow` may still remain useful to make it easier to handle
the other uses that fall into (B6). For example, rather than analyzing
all uses of `ref_element_addr`, the compiler can treat the entire
scope like a single use at `end_borrow`.
```
%borrow = begin_borrow %0 : $Class
%addr = ref_element_addr %borrow : $Class, #Class.property
%value = load %addr
// Inner Instructions
end_borrow %borrow : $Class
// Outer Instructions
destroy_value %obj
```
Here, the value of `%addr` depends on lifetime of `%borrow`. The
compiler can choose to ignore that dependent lifetime and consider the
`end_borrow` a normal use. Even this simplified view of lifetime
allows hoisting `destroy_value %obj` above "Outer
Instructions". Alternatively, the compiler can analyze the uses of the
dependent value (`%addr`) and see that it's safe to hoist both
the `begin_borrow` and `destroy_value` above "Inner Instructions".
So, uses in (O3 - propagate) can be either analyzed transitively or
skipped to the end of their scope.
Uses in (U2 - escape) cannot be safely analyzed transitively,
requiring some additional mechanism to provide safety, as described in
the section "Dependent Lifetime".
## Dependent Lifetime
A value's lifetime cannot be verified if a use has escaped the
contents that value into a trivial type (U2). In those cases, it is
the user's responsibility to designate the value's lifetime. API's
like `withExtendedLifetime` do this by emitting a `fix_lifetime`
instruction. Without `fix_lifetime`, the compiler would be able to
hoist either a `destroy` or an `end_borrow` above any uses of the
escaped nontrivial value.
`fix_lifetime` is too conservative for performance critical code. It
prevents surrounding code motion and effectively disables dead code
elimination. `mark_dependence` is a more precise instruction that ties
an "owner" value lifetime to an otherwise unrelated (likely trivial)
"dependent" value. If the compiler is able to analyze the uses of the
trivial value, then it can more aggressively optimize the owner's
lifetime.
> I'll send a separate detailed proposal for a adding an
> `withDependentLifetime` API and `end_dependence` instruction.
Here is some SIL code with an explicitly dependent lifetime:
```
bb0(%0 : @owned $Class)
%unowned = ref_to_unowned %obj : $Class to $@sil_unowned Class
%dependent = mark_dependence %unowned on %obj
store %dependent to ...
// Inner Instructions
end_dependence %dependent
// Outer Instructions
destroy_value %obj
```
In this example, the `%dependent` value itself escapes, so the
compiler knows nothing of its lifetime. However, it can still hoist
`destroy_value` above "Outer Instructions" because the
`end_dependence`
In the next example, the compiler can determine that the dependent
value does not escape:
```
bb0(%0 : @owned $Class)
%unowned = ref_to_unowned %obj : $Class to $@sil_unowned Class
%dependent = mark_dependence %unowned on %obj
load_unowned %dependent
// Inner Instructions
end_dependence %dependent
// Outer Instructions
destroy_value %obj
```
In that case both `destroy` and `end_dependence` can be hoisted above
"Inner Instructions".
Furthermore, if the `%dependent` value becomes dead, then the entire
`mark_dependence` scope can be eliminated.
Note that mark dependence does not require the compiler to discover
any relationship between the owner and its dependent value. The
instruction makes that relationship explicit (in the example below
`%trivial` depends on `%copy`). This is an important difference
between the lifetime propagation of `begin_borrow` and
`mark_dependence`.
```
bb0(%0 : @owned $Class)
%objptr = ref_to_raw_pointer %obj : $Class to $Builtin.RawPointer
store %objptr to %temp : $*Builtin.RawPointer
%copy = copy_value %obj
%trivial = load %temp : $*Builtin.RawPointer
%dependent = mark_dependence %trivial on %copy
load_unowned %dependent
// Inner Instructions
end_dependence %dependent
// Outer Instructions
destroy_value %obj
```
Both `begin_borrow` and `mark_dependence` instructions open a scope for
dependent lifetimes. In both cases, the only dependent values that
affect the owner's lifetime are values that directly derive from the
`begin_borrow` or `mark_dependence` SSA value. The differences are:
- The `begin_borrow` value is simply a borrowed instance of the owner.
- The `mark_dependence` value is any arbitrary trivial or non-trivial value.
- Only non-trivial values derived from `begin_borrow` are considered
relevant. Casting the borrowed value to a trivial value will require
a separate `mark_dependence`.
- All values derived from `mark_dependence` are considered, whether
trivial or non-trivial.
SIL implementation of ownership and borrowingSIL representation is going through an evolution in several areas: Ownership SSA (OSSA), @guaranteed calling convention, lifetime guarantees, closure conventions, opaque values, borrowing, exclusivity, and __shared parameter modifiers. These are all loosely related, but it's important to understand how they fit together. Here are some notes that I gathered and found useful to write down. (Coroutines support is also related, but I'm avoiding that topic for now.) Disclaimer: None of this is authoritative. I hope that any misunderstandings and misstatements will be corrected by reviewers. Borrowed (shared) Swift valuesI'm not sure exactly what borrowing will look like in the language, but conceptually there are two things to support:
Both of these operations imply the absence of a copy and read-only access to an immutable value within some scope: either the borrowed variable's scope or the statement's scope. Exclusivity enforcement guarantees that no writes to the borrowed variable occur within the scope, thereby ensuring immutability without copying. For copyable types, the implementation can always introduce a copy without changing formal program behavior. However, exclusivity still needs to be enforced to preserve the borrow's original semantics. Adding a copy would ensure local immutability, but removing exclusivity would broaden the set of legal programs--it's not a source compatible change. Maybe the user doesn't care about formal semantics and only requested a In short, As such, the entire borrow scope will initially need to be protected by SIL exclusivity markers: If the boxed variable is promoted to a SILValue, the exclusivity markers can be trivially eliminated. This is valid because promoting to a SILValue requires static proof of exclusive-immutability. All of the above instructions can be eliminated except the call itself: Where The If the type is not address-only (neither borrowed-move-only nor opaque), then it's possible to pass the value to a function with a direct @guaranteed parameter convention: With a direct convention, we cannot pass a SIL address, so the original SIL would need to be written as: We still need to solve the problem of address-only types. A new feature called SIL opaque values does that for us. It allows address-only types to be operated on in SIL just like loadable types. The only difference is that indirect parameter conventions are required. With SIL opaque values enabled, the original code will be: And the code can now be optimized just like before:
Ownership SSA (OSSA) form also introduces
@guaranteed (aka +0) parameter ABIIn the previous section, we saw that a
It is up to the compiler to decide which convention to use for parameters in a given position with a given type. Sometimes __shared parameter modifierThe In the first section, I defined a hypothetical This is legal code today, but If Now this is a static compiler error. Of course, we could introduce an implicit borrow whenever move-only types are passed This becomes much simpler if
Ownership SSA (OSSA)Michael Gottesman is working on extensively documenting this feature. Briefly, a SILValue is a singly defined node in an SSA graph that represents one instance of a Swift value. With OSSA, each SILValue now has an independent lifetime--it has "ownership" of its lifetime. The SILValue's lifetime must be ended by destroying the value (e.g. decrementing the refcount), or moving the value into another SILValue (e.g. passing an @owned argument). A SILValue can be "borrowed" across some program scope via So, SILValue borrowing isn't required for modeling language level semantics. Instead, it's a convenient way to verify that SIL transformations obey the rules of OSSA. In fact, these instructions are trivially removed as soon as the compiler no longer needs to verify OSSA. What do we mean by OSSA rules? Here's a quick summary. The users of SILValues can be divided into these groups. Uses independent of ownership: U1. Use the value instantaneously ( U2. Escape the nontrivial contents of the value ( Uses that require an owned value: O3. Propagate the value without consuming it ( O4. Consume the value immediately ( O5. Consume the value indirectly via a move ( Uses that require a borrowed value: B6. Project the borrowed value (
Here, the value of So, uses in (O3 - propagate) can be either analyzed transitively or skipped to the end of their scope. Uses in (U2 - escape) cannot be safely analyzed transitively, requiring some additional mechanism to provide safety, as described in the section "Dependent Lifetime". Dependent LifetimeA value's lifetime cannot be verified if a use has escaped the contents that value into a trivial type (U2). In those cases, it is the user's responsibility to designate the value's lifetime. API's like
Here is some SIL code with an explicitly dependent lifetime: In this example, the In the next example, the compiler can determine that the dependent value does not escape: In that case both Furthermore, if the Note that mark dependence does not require the compiler to discover any relationship between the owner and its dependent value. The instruction makes that relationship explicit (in the example below Both
|
_______________________________________________ swift-dev mailing list [email protected] https://lists.swift.org/mailman/listinfo/swift-dev
