ARC : Advanced Memory Management with SWIFT
Memory management is the process of controlling program’s memory. It is critical to understand how it works, otherwise you are likely to run across random crashes and subtle bugs.
Memory management is tightly connected with the concept of Ownership. Ownership is the responsibility of some piece of code to eventually cause an object to be destroyed.
Our discussion of Swift memory management is bound to be at a lower level of abstraction. We will dive into how ARC is implemented on the compiler level and which steps every Swift object undergoes before being destroyed.
Role Of Swift Run-time
The mechanism of ARC is implemented in a library called Swift Runtime. It implements such core features like dynamic casting, generics, and protocol conformance registration.
Swift Runtime represents every dynamically allocated object with HeapObject
struct. It contains all the pieces of data which make up an object in Swift: reference counts and type metadata.
Internally every Swift object has three reference counts: one for each kind of reference. At the SIL generation phase, swiftc compiler inserts calls to the methods swift_retain()
and swift_release()
, wherever it’s appropriate. This is done by intercepting initialization and destruction of HeapObject
s.
If you have touched the flavour of being an Objective-C programmer and wonder where is autorelease, then I have some news for you: there is no such thing for pure Swift objects.
Now let’s move on to the weak references. The way they are implemented is closely connected with the concept of side tables.
Role of Side Tables in Swift Runtime :
Side tables are the mechanism for implementing Swift weak references.
Typically objects don’t have any weak references, hence it is wasteful to reserve space for weak reference count in every object. This information is stored externally in side tables, so that it can be allocated only when it’s really needed.
Instead of directly pointing to an object, weak reference points to the side table, which in its turn points to the object. This solves two problems: saves memory for weak reference count, until an object really needs it; allows to safely zero out weak reference, since it does not directly point to an object, and no longer a subject to race conditions.
Side table is just a reference count and a pointer to an object. They are declared in Swift Runtime as follows :
class HeapObjectSideTableEntry {
std::atomic<HeapObject*> object;
SideTableRefCounts refCounts;
// Operations to increment and decrement reference counts
}
Life Cycle of Swift Object :
Swift objects have their own life cycle. There are in total five states.
- Live
- Deiniting
- Deinited
- Freed
- Dead
Description of State transition of Object Lifecycle :
In live state an object is alive. Its reference counts are initialized to 1 strong, 1 unowned and 1 weak (side table starts at +1). Strong and unowned reference access work normally. Once there is a weak reference to the object, the side table is created. The weak reference points to the side table instead of the object.
From the live state, the object moves into the deiniting state once strong reference count reaches zero. The deiniting state means that deinit()
is in progress. At this point strong ref operations have no effect. Weak reference reads return nil
, if there is an associated side table (otherwise there are no weak refs). Unowned reads trigger assertion failure. New unowned references can still be stored. From this state, the object can take two routes:
- A shortcut in case there no weak, unowned references and the side table. The object transitions to the dead state and is removed from memory immediately.
- Otherwise, the object moves to deinited state.
In the deinited state deinit()
has been completed and the object has outstanding unowned references (at least the initial +1). Strong and weak stores and reads cannot happen at this point. Unowned stores also cannot happen. Unowned reads trigger assertion error. The object can take two routes from here:
- In case there are no weak references, the object can be deallocated immediately. It transitions into the dead state.
- Otherwise, there is still a side table to be removed and the object moves into the freed state.
In the freed state the object is fully deallocated, but its side table is still alive. During this phase the weak reference count reaches zero and the side table is destroyed. The object transitions into its final state.
In the dead state there is nothing left from the object, except for the pointer to it. The pointer to the HeapObject
is freed from the Heap
, leaving no traces of the object in memory.
Summary
Automatic reference counting is no magic and the better we understand how it works internally, the less our code is prone to memory management errors. Here are the key points to remember:
- Weak references point to side a table. Unowned and strong references point to an object.
- Automatic referencing count is implemented on the compiler level. The swiftc compiler inserts calls to release and retain wherever appropriate.
- Swift objects are not destroyed immediately. Instead, they undergo 5 phases in their life cycle: live -> deiniting -> deinited -> freed -> dead.
Thanks for reading!
If you enjoyed this post, please hit the like button.