What is swift_unownedRetainStrong?

I'm trying to optimize a CPU-intensive operation, and perplexingly, when I parallelize it, it runs slower when the work is distributed over multiple cores. The single-core implementation takes 3.1 milliseconds to perform one iteration over 16400 records, while the concurrent implementation takes 10.7 milliseconds.

Currently I'm using DispatchQueue.concurrentPerform for parallelization, with a stride of 256. I've experimented with stride values, and it doesn't seem to have an effect.

While profiling, I can see that for the working threads, 53% of the CPU time is being spent on swift_unownedRetainStrong, and 28% of the CPU time is being spent on swift_release_(swift::HeapObject*). These costs are not present in the single-threaded version, so I believe this explains the difference in performance.

I am explicitly passing all referenced into the closure using [unowned] because I want to avoid memory-management and reference counting overhead. My assumption was that this option would just skip reference counting, but this appears not to be the case.

So what I am wondering is, what exactly does swift_unownedRetainStrong mean, and is this avoidable?

swift_unownedRetainStrong is one of the Swift runtime functions for ARC. Contrary to what you're thinking, using unowned does not opt you out of all reference counting overhead. It merely increments a different counter in the runtime and manages the memory a little bit different from a strong or weak reference.

If you want to avoid memory management overhead from ARC, you'll need to break out UnsafePointers or Unmanaged

2 Likes

unowned references are still checked at runtime to see whether the referent exists. You can use unowned(unsafe) to completely give up runtime checking, if you're willing to accept the risk of undefined behavior if you use a dangling reference.

2 Likes

Also, if you're morbidly curious, you can see what it does here swift/HeapObject.cpp at main · apple/swift · GitHub, along with a lot of the other runtime functions for managing objects.

Ah ok that is interesting. So what exactly is the point of unowned then? Can't a closure with unowned references still receive unexpected nil values? I am a little confused about what that separate retain count would be used for.

They most certainly can.

Most of the time I've used unowned has been for types that have a strict Parent<->Child relationship, where neither type has any possibility of being used anywhere near an async context. Since most of the times I've used unowned near async code, it usually ends up coming back to bite me.

The bottom line is that unowned is useful where you want to avoid bumping the reference count of something, but at the same time, you know that the value will not go nil while you still want to use it.

unowned uses the separate refcount to keep a zombie shell of the object allocated, which lets the runtime check whether the full object is alive when an unowned reference is followed. This is cheaper than a full-blown weak reference since semantically it is not allowed at all for an unowned reference to refer to a deallocated object, so unowned references can be immutable and non-optional, whereas weak references are implicitly mutated by the runtime to become nil when the referenced object goes away.

3 Likes

This is also a good overview of how reference counting works in the runtime. I can't speak to whether or not it's still 100% accurate, but I think it still provides a good overview.

Ok makes sense. Thanks for clarifying!

Thanks I will definitely give it a read!