Is this a bug in automatic reference counting?

When I run the following code

class OwnedValue {
    var value: Int

    init(_ value: Int) {
        self.value = value
    }
}

struct ValueOwner {
    let ownedValue1: OwnedValue
    let ownedValue2: OwnedValue
}

func updateValue(owndedValue ownedValue: consuming OwnedValue, with newValue: Int) {
    if isKnownUniquelyReferenced(&ownedValue) {
        print("Value \(ownedValue.value) is unique")
    } else {
        print("Value \(ownedValue.value) is not unique")
    }

    ownedValue.value = newValue
}


func testOwnership() {
    let valueOwner = ValueOwner(
        ownedValue1: OwnedValue(10),
        ownedValue2: OwnedValue(15))
    
    let valueOwner2 = consumeValueOwner(valueOwner: consume valueOwner)
    print("\(valueOwner2.ownedValue1.value), \(valueOwner2.ownedValue2.value)")
}

@discardableResult
func consumeValueOwner(valueOwner: consuming ValueOwner) -> ValueOwner {
    let ownedValue1 = valueOwner.ownedValue1
    let actualOwnedValue1 = ownedValue1.value
    let ownedValue2 = valueOwner.ownedValue2
    let actualOwnedValue2 = ownedValue2.value
    _ = consume valueOwner
    updateValue(owndedValue: ownedValue2, with: actualOwnedValue1 + 10)
    updateValue(owndedValue: ownedValue1, with: actualOwnedValue2 + 10)


    return ValueOwner(
        ownedValue1: OwnedValue(actualOwnedValue1),
        ownedValue2: OwnedValue(actualOwnedValue2))
}

testOwnership()

the output is

Value 15 is not unique
Value 10 is unique

If I reorder the last lines in testOwnership the output becomes

Value 10 is not unique
Value 15 is unique

Now, after this line
_ = consume valueOwner
there should be only one reference for each one of the ownedValues. Yet, for some reason, a reference stays alive during first call.
Now replacing

    updateValue(owndedValue: ownedValue2, with: actualOwnedValue1 + 10)
    updateValue(owndedValue: ownedValue1, with: actualOwnedValue2 + 10)

with

    updateValue(owndedValue: consume ownedValue2, with: actualOwnedValue1 + 10)
    updateValue(owndedValue: consume ownedValue1, with: actualOwnedValue2 + 10)

does result in correct output. (it is enough to do it in the first line TBH)

Value 15 is unique
Value 10 is unique

Is it a bug or a feature?
It looks like a bug TBH.
It occurs in release mode.

Not exactly. From what I remember, objects are held until end of scope by default, so that weak references continue to work after what would otherwise be the last use. Years ago there was an experiment to change this that broke a lot of idiomatic Cocoa code, iirc.

1 Like

But the object is explicitly consumed by

_ = consume valueOwner

Should not it end its lifetime?

Also, has it been the case - ValueOwner alive until last use, why after changing

updateValue(owndedValue: ownedValue2, with: actualOwnedValue1 + 10)
    updateValue(owndedValue: ownedValue1, with: actualOwnedValue2 + 10)

to

updateValue(owndedValue: consume ownedValue2, with: actualOwnedValue1 + 10)
updateValue(owndedValue: ownedValue1, with: actualOwnedValue2 + 10)

we do gt the expected behavior?

If valueOwner is alive until

updateValue(owndedValue: ownedValue1, with: actualOwnedValue2 + 10)

then consume should not have any effect - we still have two references - one in the alive valueOwner and second local to updateValue.

It makes a difference whether the local variables are consumed, as you already noticed. If they are, then yes, inside updateValue the references should always be unique. If not, then you’re leaving it up to the optimizer. It would be valid for both ownedValue1 and ownedValue2 to be kept alive until the end of consumeValueOwner.

So, why doesn’t the optimizer treat them the same? I’m a bit bemused by this too; it feels like if the optimizer can “forward” the last use of one refcounted object, it should be able to do the same for another, independent refcounted object. Even if they were dynamically the same object, there are distinct, immutable local references to it. Maybe an optimizer person can answer your actual question; all I’ve done is elaborate on bbrk’s point that this is permitted behavior.

To summarize: this is a missed optimization, and a missed optimization is not a bug, even if it’s observable by the program.

4 Likes

Okay. But is not

_ = consume valueOwner

supposed to explicitly end the lifetime of valueOwner? Or only end its scope?

Correct me if I am wrong, but accessing members of valueOwner, using a weak reference, after it has been explicitly consumed is not idiomatic Cocoa code, but undefined behavior. (Old idiomatic Cocoa code exists from times long before consume, borrow, became a thing as far as I am aware).

I think Jordan is saying you need to make this change:

 @discardableResult
 func consumeValueOwner(valueOwner: consuming ValueOwner) -> ValueOwner {
     let ownedValue1 = valueOwner.ownedValue1
     let actualOwnedValue1 = ownedValue1.value
     let ownedValue2 = valueOwner.ownedValue2
     let actualOwnedValue2 = ownedValue2.value
     _ = consume valueOwner
-      updateValue(owndedValue: ownedValue2, with: actualOwnedValue1 + 10)
-      updateValue(owndedValue: ownedValue1, with: actualOwnedValue2 + 10)
+     updateValue(owndedValue: consume ownedValue2, with: actualOwnedValue1 + 10)
+     updateValue(owndedValue: consume ownedValue1, with: actualOwnedValue2 + 10)

     return ValueOwner(
         ownedValue1: OwnedValue(actualOwnedValue1),
         ownedValue2: OwnedValue(actualOwnedValue2))
 }
1 Like

Formally, let ownedValue2 = valueOwner.ownedValue2 creates an independent owned reference to ownedValue2. It doesn’t matter to the local variable that valueOwner gets destroyed afterwards. In manual-retain-release terms, it’s closer to id ownedValue2 = [valueOwner.ownedValue2 retain]. (Just like ObjC ARC, for the record.)

In practice, there’s almost never an actual retain performed here, because the compiler can see that it would be redundant (this is better than ObjC ARC, which often can’t be quite as confident). But when we’re talking about what is and isn’t permitted, and what is and isn’t a bug, the answer is that the local variable is an independent owner. Which is why you can consume it at all.

If you can use an object after it’s been destroyed, that’s a bug. But that shouldn’t be possible in Swift without going through an API labeled “unsafe”, sidestepping the concurrency rules to make a data race, or calling back to an ObjC API that does something Swift can’t check.

1 Like

I am aware making this change works, and I've mentioned it in my original post. The thing I don't understand is - why is it necessary.
Citing E366

In this document, we propose adding a new operator, marked by the context-sensitive keyword consume, to the language. consume ends the lifetime of a specific local let, local var, or function parameter, and enforces this by causing the compiler to emit a diagnostic upon any use after the consume. This allows for code that relies on forwarding ownership of values for performance or correctness to communicate that requirement to the compiler and to human readers. As an example
In this document, we propose adding a new operator, marked by the context-sensitive keyword consume, to the language. consume ends the lifetime of a specific local let, local var, or function parameter, and enforces this by causing the compiler to emit a diagnostic upon any use after the consume. This allows for code that relies on forwarding ownership of values for performance or correctness to communicate that requirement to the compiler and to human readers. As an example:

useX(x) // do some stuff with local variable x

// Ends lifetime of x, y's lifetime begins.
let y = consume x // [1]

useY(y) // do some stuff with local variable y
useX(x) // error, x's lifetime was ended at [1]

// Ends lifetime of y, destroying the current value.
_ = consume y // [2]
useX(x) // error, x's lifetime was ended at [1]
useY(y) // error, y's lifetime was ended at [2]

It seems that

_ = consume valueOwner

is supposed to end the lifetime, not just the scope.

It ends the lifetime of valueOwner, but just like in ObjC that doesn’t immediately end the lifetime of its members. It merely releases them.

1 Like

But what about

This allows for code that relies on forwarding ownership of values for performance or correctness to communicate that requirement to the compiler and to human readers

part?

This is a toy example, but it does rely on the fact that valueOwner is not alive, with naive assumption that its members are not alive either.

You are the one who assigned its members to local variables. Now you need to consume those to get the forwarding guarantee you’re looking for.

1 Like

I had deleted my post because I realized that @theundergroundsorcerer had written updateValue() to take consuming arguments, so I assumed I had misunderstood the problem. But it seems like you’re saying the consume at the call site is necessary.

This is not always an option. What happens if there are more “wrapping levels” but my code relies on the fact that the object (and its part as well) ended its lifetime, but consuming the wrapped references is not an option? (They could be private for example).

Passing a copyable value to a consuming parameter in Swift will generally still perform a copy (if the expression does not naturally produce an owned value). If Swift always consumed in these situations (as Rust does), making a parameter consuming would be source-breaking, and we did not want programmers to have to make that choice. The consequence is that, for now, clients have to be explicit about wanting to consume.

It would be good if Swift could promise to consume in cases like this one where it can clearly see that the variable is not used after the potentially-consuming use. As was noted earlier in the thread, however, this could also be source-breaking (in a different sense) because it could end the lifetime of an object that the code was implicitly relying on staying alive, whether through weak references or something spookier. It’s something we’re still interested in, but it’s not an easy change to make.

6 Likes

If you cannot consume it explicitly, I don’t know how you’re expecting the compiler to do it implicitly.

3 Likes

Please correct me if I am wrong, but I recall that one of the SE proposals said that copyable variable is copied instead of being consumed if it is being used later. My issue not with consuming parameters, but with consume operator which is supposed to end the life of an object, but seemingly doesn't and is "subject to optimization". An object is being consumed explicitly, using consume operator.
Citing SE-0377

SE-0366 introduced an operator that explicitly ends the lifetime of a variable before the end of its scope. This allows the compiler to reliably destroy the value of the variable, or transfer ownership, at the point of its last use, without depending on optimization and vague ARC optimizer rules. When the lifetime of the variable ends in an argument to a consume parameter, then we can transfer ownership to the callee without any copies.

In addition, almost all examples using consume seem to imply that it ends the lifetime of operator, while things still seem to happen behind the scenes and "depend" on the optimizer.

I can consume the wrapper, but I don't see whether it guarantees that references it stores are consumed as well, or implicitly alive references that are part of another object that was consumed earlier, as in my example.

The consume operator is not itself subject to optimization. But consume on a storage location of copyable type only transfers ownership of the value stored in that specific location. Other locations will have independent ownership of the values they store. So if you have a reference to an object in two locations, and you transfer ownership of only one of them, the object will stay alive.

Allow me to simplify your example:

func consumeValueOwner(valueOwner: consuming ValueOwner) {
  // At this point, `valueOwner` has ownership of its current value, which
  // is a struct value consisting of two strong object references.
  // Let's assume these are references to different objects, which we'll
  // call A and B, and that this value is the only remaining reference
  // to them.

  let obj = valueOwner.ownedValue1

  // `obj` now has independent ownership of the object A.
  // There are two references to this object.

  _ = consume valueOwner

  // We have consumed the value out of `valueOwner`, leaving it empty.
  // Ownership is transferred to a temporary, which is immediately dropped.
  // In other words, we destroy the value that was previously stored in
  // `valueOwner`, which is to say, we destroy the two strong references
  // by decrementing the reference count on the objects they refer to.

  // There is still a reference to A in `obj`.
  // There are no longer any references to B, and the object will be
  // immediately destroyed at this point.

  let propertyValue = obj.value

  // The set of extant references to object A has not changed.

  updateValue(ownedValue: obj, with: propertyValue + 10)

  // Because arguments passed to consuming parameters are copied,
  // the parameter to `updateValue` is an independent reference to
  // object A, and `obj` continues to have its value. Therefore there are
  // two references to object A, and the unique reference test within
  // `updateValue` fails.

  // When we reach the end of the scope, all local variables (that have
  // not already been consumed from) are destroyed, dropping the
  // last reference to object A and causing it to be immediately
  // destroyed.
}

So whether consume consumes the value in the named location is not subject to optimization — it must do so, and if that is the only reference to the value, you should see that reflected in reference counts and so on. But if there are other copies of the same value (or its component values) in other locations, they will maintain independent ownership. Those other locations are not somehow magically affected by an independent copy of their value having been consumed from somewhere else.

Explicitly using consume on the argument to updateValue forces the compiler to transfer ownership from obj to the consuming parameter, rather than copying, and is the right thing to do if you need a guarantee of no copies. This could also be done automatically by Swift's optimizer, but currently this does not happen reliably for reasons already discussed.

This sensitivity of reference counts to optimization — and the finickiness of manually optimizing it — definitely makes unique-reference tests somewhat semantically frustrating. That is why we encourage them to be used mostly for things like copy-on-write data structures, where the expected cost of a non-unique reference is just a performance hit. (And that performance hit, and the difficulty of optimizing it, is definitely something that programmers have also noted as frustrating when doing Swift performance work.)

9 Likes

Thanks. Great explanation. So the problem is not that valueOwner stays alive, but with the fact that whether consuming ownedValue parameters are copied or not is a subject to optimization, independent of valueOwner being consumed?

Hence, if we are interested in forcing forwarding, every consuming function argument should be passed with preceeding consume word (when it's possible)?

The origin of my question was dealing with some COW issues - it just boiled to this toy example.

Correct me if I am wrong, but using consuming parameters together with copyable allows modifiying the function argument, which can be a good thing in some use scenarios. Previously we needed either to make a local copy within the function (which introduces extra overhead) or pass them as inout which breaks protection mechanisms (because I want to modify the copy, not the object itself) Is this intended use or just a "side effect"? Or we are not supposed to use them in this way?

Thanks for your elaboration. Can you please clarify in what circumstances are consuming parameters for copyable types not implicitly copied? Is it what you put in the parentheses?

For example, for one of the motivating examples of consuming parameter in the proposal:

extension String {
  // Append `self` to another String, using in-place modification if
  // possible
  consuming func plus(_ other: String) -> String {
    // Modify our owned copy of `self` in-place, taking advantage of
    // uniqueness if possible
    self += other
    return self
  }
}

// This is amortized O(n) instead of O(n^2)!
let helloWorld = "hello ".plus("cruel ").plus("world")

Although String is copyable, this optimization can still work here if we can rely on self not being implicitly copied when passed to plus. It is crucial that self does have an exclusive ownership of the string buffer, otherwise the following += will trigger COW.

So what do you mean by saying "if the expression (does or) does not naturally produce an owned value"?. Does this implicit copy happen on the callee's side?

I would say that the core problem is just that code is allowing the caller to still have an independent copy by not consuming ownedValue1. The optimization question is just something that adds some unpredictability on top of that, because it could have saved you here by implicitly consuming, it just didn’t.

Correct.

This is an intended use. If you don’t semantically want to modify the caller’s copy of the value, it shouldn’t be inout. Having decided that, if it’s still good for performance for the callee to have ownership of the value, then yeah, you should take it consuming. This works well in some ways with Swift’s semantics of copying into consuming parameters because the caller isn’t forced to worry about it — they’ll get the optimization opportunistically, and if they want to cooperate to get it in more places, they can explicitly consume. But you should understand the flip side of that: if this optimization is really important to you — if you would actually prefer it if callers had to explicitly copy when they cannot cooperate with the optimization — the language is going to be somewhat at odds with that goal because of the copying into consuming thing.

2 Likes