[Pitch] Task owned objects , which show up as not requiring retain/release during Task

Think of the performance!

1 Like

Task owned objects would still require retain/release operations to appropriately implement the CoW property, unless we either constrained all Task-owned objects to be reference types or otherwise limited their functionality substantially.

Put another way, in Swift the difference between a refcount of 1 and a refcount of n where n > 1 is an observable property of the language, and the Task necessarily holds a reference to the object. As a result, Task owned objects would simply behave as though they were task-locals: the Task holds a reference to the object, and you might be able to trigger a path through a _modify accessor that can elide a refcount operation, but that will depend very much on exactly what you do.

Classes without ref counting is a start. If the object were private to task, and during debug you could prove there were no external refs, could you call immortalize on the ref at task begin, and then just delete it at then end? And do no book keeping at all in the meantime ?

The idea is the Task owns it and when the Task ends it dies, so its like the unmanaged stuff where during a closure you have a ref that doesn't need ARC, but this is for the duration of the Task, with the additional benefit the runtime people own the executor, so they can feel safer, and allow more performance.

Not trivially, no. Users may always copy the task (e.g. by assigning it to a function-local variable) which will trigger a retain. Swift could optimise that away if it sees the reference drop, but escape analysis is not generally possible, so there are plenty of cases where it cannot: maybe you pass the task-local to a function defined in another module, maybe you temporarily wrap it in a structure without a _modify accessor on a field, maybe you mistakenly return it or do something else.

The only general solution to this problem is to adopt some of the lifetime techniques made popular by Rust. If Swift had a notion of a "scoped borrow", then it would be possible for Tasks to only vend "scoped borrows" to their task private data. If Swift could then enforce the lifetimes of function arguments and other types then those scoped borrows could be validated and escape could be prevented.

This approach is possible, but it is far from straightforward, and Task-owned objects are probably the easiest part of it. A comprehensive lifetime model is far and away the harder part, and not necessarily a natural fit for Swift.


Does immortalize() turn off ref counting ?


A couple corrections.

Tasks have reference semantics; copying a reference to the task doesn’t introduce more references to state that’s internal to the task. Presumably an instance of a task-local class would not be Sendable and so would remain internal to the task.

Scoped borrows aren’t especially problematic in Swift’s model as long as they can be declaration-driven and we don’t have to e.g. put them in an array. We are likely to have the ability to express this in the language eventually; we just aim for it to be a more “expert-oriented” feature than it is in Rust.

While task-duration objects are imaginable, they do have the obvious disadvantage of causing the task to accumulate unbounded amounts of memory for the duration of the task. Plus, of course, they would require extra storage in every task to maintain a list of all the objects that need to be destroyed at the end of the task. There are two optimizations that are both less of an imposition and more generically powerful:

  • non-atomic reference counts for classes that aren’t Sendable and that we’re willing to guarantee are never shared between concurrent contexts
  • unique references for classes that are willing to suffer the inconveniences of a strict single-ownership model

These both sound great! I was thinking about how Tasks let you semantically say - "I need this for a while"- but non-sendable thing gives us the same thing. For the new actor model, that means it would live in one actor context, right ? So you could still get 8-way or so concurrency because of the current executor using async let? Oh and we currently maintain a large immortalized and other hacky cache for our recognitions to keep ARC at bay, and would gladly dump all of that. What do you mean by strict single -ownership ?

Tasks is an orthogonal dimension of concurrency to actors:

  • A task is a single asynchronous "thread" of execution; code running as part of a task can run concurrently with other tasks, but the task alone is never doing multiple things concurrently.
  • An actor is a domain of isolation. Code may be isolated to a particular actor; such code can run concurrently with code isolated to other actors (or not isolated at all), but not with other code isolated to the same actor.

A task's execution can flow between actors arbitrarily depending on the isolation of the code it's currently executing.

Sendable checking, as designed in Swift, locks non-Sendable values to the current point in both dimensions. That is, you can't currently say something like "this value is locked to the current task", allowing to be shared between functions isolated to different actors as long as it doesn't escape the task (e.g. by being captured in a Sendable closure or stored into actor-isolated storage). That's theoretically expressible someday, if we find it useful.

A static restriction that you can't have multiple references to any given object, with the result that the object doesn't have to use reference-counting at all. However, it also means you can't pass the object around without being very clear about whether you're transferring ownership or just allowing someone else to use it temporarily. It would be a kind of move-only type; I suggest reading the ownership manifesto if you'd like more information.


when will these features be available?

So sounds like locking it in 2 dimensions gives us the ability to do this one:

  • non-atomic reference counts for classes that aren’t Sendable and that we’re willing to guarantee are never shared between concurrent contexts

Do we have a protocol already that we can use to mark this way? Im guessing we don't want all classes not marked sendable to suddenly have this.

And yeah, I thought that's what you mean by "strict single -ownership" - just like to ask. And "static lets" are very easy to use, as a bonus. What about static immutable caches? Can they just be no-ARC, shared by all?

This matches our use case pretty closely - we have a static cache handing out "heaps" on checkout to a Task, which owns the heap for its duration, and then hands it back. Our case is wide concurrency of same type objects, which use the caches "heaps" for doing their temp work, while emitting "permanent" objects. So the heaps would get checked out from the static let, and the Task could own it for its duration, and hand it back. We are currently doing this with DispatchQ's, etc, and it works fine, but im this would be simpler, and using your Task work, perhaps more performant, given the more gracious use of the concurrency stuff under the hood. (And definitely nicer to the dispatchQ folks wanting less thread explosion)

The heaps are preallocated structs and things in unsafebuffers, that are used ephemerally like a stack, but there's some init work to be done, so we save a ton of cost keeping them at the ready.

This is an example of our usage:

   if let heap =  PhoneticComparisonHeap.heaps.take()
        defer {
        took = true
        let workID = heap.currentWorkID
        let task =  heap.localAllocRecogTask()


We have these preallocated and one per worker thread. Typically 8-16 firing at once depending on CPU, but on big linux boxes with high CPU counts, 32,64, etc.)

Yup, good catch: I intended to type "task-local" but missed the trailing -local.

So sounds like there are 2 wins here? How does something like this go from pitch to "yeah lets do it" ?