6

Is it true that ARC keeps a count of unowned references to an object?

So, if the strong reference count of an object reaches 0 and the unowned reference count of that object is > 0 the object is de-initialized but not de-allocated? And only when the strong and unowned reference count reaches 0 does it get de-allocated?

I read that in an article, on Medium I think) but I'm not sure it's correct.

rmaddy
  • 314,917
  • 42
  • 532
  • 579
  • 2
    I’d suggest you include a link to the article and include the relevant excerpt in the question. But in answer to your question, when the last strong reference is removed, the object _is_ deallocated, regardless of what dangling unowned references might be out there. – Rob Feb 22 '19 at 23:58
  • At the minimum, you should link to the article you're referring to, so we can address the claims as they're stated, not the claims as you understood/represented them. – Alexander Feb 23 '19 at 00:29
  • @Rob Actually, dangling references don't remain. Weak references actaully point to an entry in a "side table", which itself contains a reference to the "main" object. The side table entry remains so long as there are outstanding weak refs, to keep the pointers from dangling. Upon accessing a weak ref with a deallocated main object, the weak ref is deincremented in the side table, and that particular weak ref is `nil`ed out. https://www.mikeash.com/pyblog/friday-qa-2017-09-22-swift-4-weak-references.html – Alexander Feb 23 '19 at 00:31
  • @Alexander - Those are `weak` references. He asked about `unowned` references. Perhaps he just misremembered the article, which may have been about `weak`. – Rob Feb 23 '19 at 00:45
  • @Rob Ah, indeed. However, when you try to access an unowned ref for a deallocated object, you get an error ("attempted to read an unowned reference but the object was already deallocated"), not just some random dangling pointer undefined behavior. There's some mechanism to prevent the dangling pointer issue, like what you would otherwise see in C. I suspect side tables are used for weak refs, too. – Alexander Feb 23 '19 at 00:52
  • 1
    @Alexander - For debug builds you’ll see that behavior, but for release builds, you’ll see something more akin to true dangling pointer behavior. – Rob Feb 23 '19 at 01:00
  • @Rob Ah okay, makes sense. If debug: use tombstones, else: SNAFU – Alexander Feb 23 '19 at 01:08
  • Definitive answers for the fearless can be found in the source on github (at least definitive for a particular version): https://github.com/apple/swift/blob/main/stdlib/public/SwiftShims/RefCount.h – Avitzur Nov 24 '21 at 22:22

1 Answers1

12

First of all, let's be aware that the answers to these questions are all implementation details that we should generally avoid relying on. Now, on to the answers:

Is it true that ARC keeps a count of unowned references to an object?

Yes, it is true. Each object has three reference counts: the strong count, the unowned count, and the weak count.

  • The strong count is always stored (but is stored with an adjustment of -1, so a stored 0 means a strong reference count of 1, and a stored 1 means a strong reference count of 2, and so on).

  • The unowned count is also always stored, with an adjustment of +1 that represents all strong references and is removed at the end of deinitialization.

  • The weak reference count is only stored after the first weak reference to the object is created. The weak reference count, if it is stored, is stored with a +1 adjustment, which represents all unowned references and is removed after the object is deallocated.

So, if the strong reference count of an object reaches 0 and the unowned reference count of that object is > 0 the object is de-initialized but not de-allocated?

Correct. The object is deinitialized: the deinits of the object's class and all superclasses are run, and any of the object's properties that are themselves references are set to nil. However, the object's memory is not deallocated, because the object's header has to remain valid until the last unowned reference to the object is destroyed.

And only when the strong and unowned reference count reaches 0 does it get de-allocated?

Correct. The object is deallocated when both the strong and unowned reference counts reach zero. Since most objects are never referenced by unowned references, this is usually when the last strong reference is destroyed.

You didn't ask about weak references, but for the sake of completeness, I'll explain them also. When an object is (or has ever been) referenced weakly, Swift allocates what it calls a “side table entry” (or sometimes just “side table”) for the object.

  • If an object has no side table, then the strong and unowned counts are stored directly in the object, and the weak count (which must be zero) is not stored.

  • If an object has a side table, then a pointer to the side table is stored in the object. The strong, unowned, and weak counts, and a pointer back to the object, are stored in the side table.

A weak reference to an object is stored as a pointer to the side table, not to the object. This means that an object can be deallocated (not just deinitialized) even if there are still weak references to it.

The side table is deallocated when the object is deallocated if there are no weak references to the object. If there are still weak references, the object is deallocated but the side table remains allocated. When the last weak reference to a deallocated object is destroyed, the side table is deallocated.

Note that weak references are not set to nil (destroyed) immediately when a Swift object is deinitialized or deallocated! A weak reference to a deinitialized object is only set to nil when the program tries to load the reference, or when the container of the weak reference is deinitialized. (What I mean by “the container” is, for example, when an object has a weak var property. The object is the container of the weak var reference.)


A large comment at the top of RefCount.h in the Swift source code explains all of these details and more.


P.S. There is one more kind of reference, unowned(unsafe), which does not adjust any reference counts. You should avoid this kind of reference if at all possible (and avoidance is almost always possible).

rob mayoff
  • 375,296
  • 67
  • 796
  • 848
  • `unowned` yields `unowned(unsafe)` behavior with optimized builds, e.g. `-Ofast`, no? – Rob Feb 23 '19 at 02:35
  • 1
    In my testing just now using Apple Swift version 4.2.1 (swiftlang-1000.11.42 clang-1000.11.45.1), there is no `-Ofast`. In both `-O` and `-Ounchecked` builds, a program that tries to use an invalid `unowned` reference reports “Fatal error: Attempted to read an unowned reference but object 0x7fb358e06a10 was already deallocated” – rob mayoff Feb 23 '19 at 02:45
  • I just tested in both Xcode 10.1 and 10.2 beta 3 and when choosing “optimize for speed” for the "Swift Compiler - Code Generation” (the default setting for release builds), I’m seeing this unsafe behavior. It’s a bit academic, but that’s what I’m seeing. Regardless, great answer. – Rob Feb 23 '19 at 02:55
  • [Here's my test program.](https://gist.github.com/mayoff/2c788003c1b5161da36bec807cc2a8e5) For me, it aborts “safely” with `-Onone`, `-O`, and `-Osize` in Xcode 10.1 on macOS 10.13.6. – rob mayoff Feb 23 '19 at 03:01
  • While this answer does cover implementation details, one important thing to take away from it is the fact that `unowned` is *guaranteed* to be memory safe (which conforms to Swift's philosophy of being safe by default). It is guaranteed to produce a runtime error if you use a reference after it has been deallocated. As you say, you can opt-out of this memory safety using `unowned(unsafe)`, which yields undefined behaviour instead. – Hamish Feb 24 '19 at 11:34
  • @robmayoff `The unowned count is also always stored, with an adjustment of +1 that represents all strong references and is removed at the end of deinitialization.` AS far as I know it is not guaranteed that `deinit` will be called on the main thread! Then what will happen if someone on the main thread will try to access an object via unowned ref during its `deinit` is running on another thread. Since unowned ref count is cleared in the end of dinitializtion it is theoretically possible – starwarrior8809 Nov 26 '20 at 21:38