ARC overhead when wrapping a class in a struct which is only borrowed

I'm not sure it is meaningful, because unless you suspend the entire process and don't call out to any code out of your immediate control, the answer you get is unusable as soon you get it, since anyone else borrowing the same strong reference can increment it. Doing something on behalf of the sole owner of the object seems to me like it'd require taking a lock of some sort to prevent anyone else from asserting ownership of the object while you need it to be uniquely-referenced. inout effectively statically implies that someone is asserting this unique ownership on the callee's behalf.

4 Likes

I think the term “uniquely referenced” has become overloaded in this context. I believe that ideally isKnownUniquelyReferenced() would return true if and only if the argument is either the sole strong reference to the object, or is the sole (transitive) borrow of the sole strong reference to the object. At the point that isKnownUniquelyReferenced() returns, I have created an invariant that my program must uphold. It’s still a meaningful question to ask, but one that Swift (even with move semantics) lacks the tools to answer.

The problem is, there isn't any such thing as a sole shared borrow, and there isn't any way to tell because shared borrows are only represented as bitwise copies of the object pointer. We can answer the "sole strong reference" question for the owner of a strong reference, like you'd get from a consuming parameter, because the owned value keeps the reference count incremented, and we can answer it for an inout parameter, since inout statically has to be the sole borrower and user of the value, so we know the reference count can't change as long as the parameter holds that exclusive access.

2 Likes

I think we’re ultimately saying the same thing in different ways. :)

Sorry to make you repeat yourself, but this distinction hasn't sunk in to Swift community's diction yet. I often see noncopyable references referred to as unique references (see current proposals). We probably shouldn't be calling them that.

A borrow can tell you whether a reference has unique ownership the same way that move-only references give you unique ownership. But mutation of the referenced object can happen via other borrows, so it isn't a safe form of uniqueness.

To be unambiguous:

let object = Object()
borrowing b1 = object
borrowing b2 = object
isUniquelyReferenced(b1) // true
isUniquelyReferenced(b2) // true

Only useful if you know you're only mutating through object, not b1/b2.

The nice thing about noncopyable references is that an inout binding is sufficient to know that the binding is unique without calling isUniquelyReferenced at all.

Adding -enable-ossa-modules results in the following error for me (Linux, using 5.7.2 from swiftlang.xyz):

swiftc -Xfrontend -enable-ossa-modules  -O bla.swift
bla.swift:6:39: error: cannot find 'ProcessInfo' in scope
                let arguments = Array(ProcessInfo.processInfo.arguments.dropFirst())
import Foundation

class Main {

        static func main() {
                let arguments = Array(ProcessInfo.processInfo.arguments.dropFirst())
                print(arguments)
        }
}

It seems Foundation is not found anymore. Is this expected?

Which is incredibly useful. There are two other situations where I felt I needed it:

  1. DebugStringConvertible instances where it's just useful to know when you're debugging something where you don't expect CoWs (broadly the same as your assert).
  2. There are situations where it'd be good to know if a CoW copy is likely going to happen or not. I had this in a slab allocator recently where I'd like (if possible) to choose a slab (for adding more data) that has free space and is uniquely referenced. That was for a cache where I slice out parts of the caches slabs and sent them over the network. In almost all cases that will immediately send the data and the cache's slab is uniquely referenced so I can modify it without risking a CoW copy. But in certain cases that's not the case so I'd rather have my "which is the best slab to add the new data to" algorithm take the unique referencedness into account. Right now I'm doing something very conservative where I only modify the latest slab and I just never slice out of the latest one (and copy instead).

The other thing about allowing isKnownUniquelyReferenced on borrows is that, to give the results people want, we would have to prevent copies from being reduced to borrows if we couldn’t prove that the refcount wasn’t observed during that time.

The intent of iKUR is to conservatively answer the question “will this object be accessed other than through this specific reference or something later derived from it?”. This question must be conservatively answered false in at least some cases where the object will not be accessed, because of Rice’s Theorem, but a positive answer is required to be definitively accurate. This intent is why it’s acceptable for optimizations such as lifetime shortening to change the answer to this function: as long as we don’t cause a reference’s lifetime to end before it is last accessed, we’re actually just improving the accuracy of iKUR. But weakening a copy to a borrow would allow iKUR on the borrow to falsely return true.

The requirement of exclusivity forbids these simultaneous borrows and makes iKUR work today.

2 Likes

For the sake of further eliminating ambiguity, isn’t isKnownUniquelyReferenced(b1) a compiler error right now? A borrow can’t satisfy an inout parameter, and John’s point is that this is intentional and necessary for isKnownUniquelyReferenced() to work.

Yes, both the borrowing binding and iKUR that takes a borrow are hypothetical. When we do have borrowing then iKUR won't accept such a variable. John has pointed out three times now that it would be misused. Basically, if you were ever to mutate the referenced object without inout exclusivity, then aliasing borrows or even copies of the reference would end up observing the mutation.