Questions on retain/release count balancing and the change of func parameter calling convention from @owned to @guaranteed

Hi,

With the nice optimization of changing default ownership of func parameters from "owned" to "guaranteed", we have a few questions related to the calling conventions and managing retain/release instructions.

  1. For a function foo(x: SomeClass) that we defined, is there a Swift level annotation (e.g. attribute) that we can use to make x @owned instead of @guaranteed?

  2. SomeClass is a class that we defined. Is there a way to guarantee that a SomeClass-typed value is always consumed at +0, and never +1? It seems no, because for special methods like initializer and setter, params are taken as @owned (swift/SILFunctionType.cpp at f65000ae57ddc5f5fc20fa8d31c9c1c6f771d8e2 · apple/swift · GitHub), but I'd like to confirm on this point.

  3. Is there a SIL verifier pass that verifies for a given function foo(), for each param v of it, the refcount bias of v at the exit of foo() is the same, across all control flow paths? Furthermore, it seems w can verify that value is -1 if v as a param is taken at +1, or 0 if taken at +0. This question also applies to local values in foo().

For more context, we have a compiler pass (in Swift for Tensorflow project) that moves around or deletes SIL code related to SomeClass values local to a function (SomeClass is Tensor in our case), and thus need to redo refcount (retain/release) balance.

Regarding #1, when we downstream the default ownership change from master (this commit) to our branch, we can use such an annotation to stabilize our existing code.

Regarding #2: For any Tensor-typed value v (it's local to a SIL function), we know it's created at +1; if we can guarantee that all uses of v take it at +0, then we can be free to move or delete any such uses, as long as the final strong_release v is kept.

#3 would in general be useful when we generate or delete SIL code in the compiler.

(Looping in some ownership / retain-release experts.)
@Michael_Gottesman
@John_McCall

Thanks!

For a function foo(x: SomeClass) that we defined, is there a Swift level annotation (e.g. attribute) that we can use to make x @owned instead of @guaranteed?

Yes, and it's currently spelled __owned. It will need to go through Evolution before it becomes source-stable, of course (i.e. lose the underscores and possibly get a new name).

SomeClass is a class that we defined. Is there a way to guarantee that a SomeClass-typed value is always consumed at +0, and never +1?

Why would you want this at the type level? If it's for your code-motion rebalancing, I think the semantic-SIL efforts to use copy_value / destroy_value instead of retain_value / release_value will be the right solution instead of trying to pretend that references can never meaningfully have their ownership transferred between contexts. The invariants that that rule establishes are the invariants you want for your "refcount bias" verification.

For a function foo(x: SomeClass) that we defined, is there a Swift level annotation (e.g. attribute) that we can use to make x @owned instead of @guaranteed?

Yes, and it's currently spelled __owned. It will need to go through Evolution before it becomes source-stable, of course (i.e. lose the underscores and possibly get a new name).

Thanks!

SomeClass is a class that we defined. Is there a way to guarantee that a SomeClass-typed value is always consumed at +0, and never +1?

Why would you want this at the type level? If it's for your code-motion rebalancing, I think the semantic-SIL efforts to use copy_value / destroy_value instead of retain_value / release_value will be the right solution instead of trying to pretend that references can never meaningfully have their ownership transferred between contexts. The invariants that that rule establishes are the invariants you want for your "refcount bias" verification.

The SILGen-created code already has retain/release insts. e.g. With the old convention where @foo() takes param x at +1 (@owned), the original SIL pseudo-code can be:

%x = SomeClass(...)
strong_retain %x
%y = foo(%x)
strong_release %x // x is destroyed here
<code chunk 1>
<code chunk 2>

Since foo() takes +1 and returns +1, we can move foo() below, as in:

%x = SomeClass(...)
strong_retain %x
strong_release %x
<code chunk 1>
%y = foo(%x)  // x is destroyed here
<code chunk 2>

With the new param convention of @guaranteed, the original SIL code becomes:

%x = SomeClass(...)
strong_retain %x
%y = foo(%x)
strong_release %x // extra strong_release here compared to above
strong_release %x // x is destroyed here
<code chunk 1>
<code chunk 2>

Now we cannot simply move down foo(%x). One option is to make sure when foo(%x) gets moved down, we move down a subsequent strong_release accordingly, but that is somewhat more complexity.

How can I use copy_value / destroy_value in the above case to simplify the design?

BTW, I don't know why the SILGen-generated code in the new case is not:

%x = SomeClass(...)
%y = foo(%x)
strong_release %x // x is destroyed here
<code chunk 1>
<code chunk 2>

But that seems a minor issue at this point and a later peephole pass can in theory clean it up.

The main thing is that copy_value leads towards SIL being able to express a simple structural validation rule: it becomes easy to both (1) know at the definition point of a value whether the function has local ownership of it and (2) prove that the value is uniquely consumed on every path through the function. So the SIL becomes

%x = SomeClass(...)
%x2 = copy_value %x
%x2_borrow = begin_borrow %x2
%y = foo(%x2_borrow)
end_borrow %x2_borrow
destroy_value %x2
destroy_value %x
<code chunk 1>
<code chunk 2>

It should be much more straightforward to prove that the borrow of x2 can be changed to a borrow of x because you don't have to reason about reference counts, you just have to reason about the static lifetimes of the values in the function.

1 Like

I believe SILGen already emits code this way (at for some types?), but they get lowered to the retain_value instructions as part of transitioning from raw to canonical SIL. That's something we'd like to fix, but to do so we have to make sure that various passes actually handle the new instructions correctly.

1 Like