Open Nuts and Bolts of Memory Management in iOS : Swift Part 1
Memory management is one of the crucial part in any development. Infect mobile devices needs more focus on this area. We are going to cover below points one by one :
- What is memory?
- How iOS app do memory management? (ARC)
- Value type and Reference type in memory semantics.
- Stack vs Heap allocation
Memory ?
In simple words, memory is a collection of bytes, every byte have it’s own address. If we talk in terms of programming then, range of these byte addresses called as address space. An address space can consist of following components : “text”, “data”, “heap” and “stack”.
Want to read this story later? Save it in Journal.
- text segment contains machine instructions that form the app’s executable code.
- data segment stores Swift static variables, constants and type metadata.
- stack is used for local scoped variables and value types(with some exception), it contains method parameters and local variables.
- heap is for objects those have a lifetime, it’s mainly used for the reference type variables with some value type exception. (We will discuss later)
Tip : In general with some exceptions, Swift value types are allocated on the stack. Reference types are allocated on the heap.
As shown in above image heap and stack grows towards each other.
Automatic Reference Counting (iOS Memory Management)
Memory management is the way of controlling memory allocation and to make it free. Automatic Reference Counting is the mechanism used for it in iOS. It is the ownership of ARC to frees up the memory used by instances when those no longer needed.
ARC is a compile time feature. The compiler inserts the necessary retain/release calls at compile time, but those calls are executed at runtime, just like any other code.
How ARC works ? Every time we create a new instance of a class, ARC allocates a chunk of memory to store information about that instance. This memory holds information about the type of the instance, together with the values associated with that instance. The name by which an object can be pointed is called a reference.
Reference counting applies only to instances of classes.
Reference count determine when an object is no longer needed. This counter gets incremented by one for every new reference to the instance. When it becomes zero that means the object is no longer needed. The object then deinitializes and deallocates. Swift references can be of two types: strong and weak. Plus, weak references also have a flavour, called unowned. We will discuss these separately later.
Stack vs Heap, where memory is allocated and cost of allocation?
As we know, Stack is used for local scoped variables and value types in iOS (with some exception, we will move in these expectations in sometime). Stack is a data structure having two operations push and pop. A pointer to the top of the stack is enough to implement both operations, so we just need to increment or decrement pointer when new allocation or deallocs occur. So, cost of allocation on stack is like assigning some integer value.
In case of heap we have lot more to do, first we have to search for appropriate memory block required, then also need to Synchronise the process, as heaps are generally used for reference types in iOS and multiple threads can be allocating memory there at the same time. Also to deallocate memory from the heap we have to reinsert that memory back to the appropriate position.
Cost of allocation and deallocation on stack is much less than allocation and deallocation on heap, because of which sometime compiler may promote reference type to store on stack also, this process is the part of SIL (Swift Intermediate Language) generation.
Let’s check by taking some examples, how SIL process swap behaviour between value and reference type to optimise things. Before we jump to example, let’s define Boxing first, boxing is a technique of placing something inside a box. Refer below example :
Here we have an struct “User”, as struct is of value type, if we want to assign that it’s value to multiple places each will get its own unique copy. This means one place can’t make a change and have that affect all other places. But it’s required that change on a single value would get reflected in rest of all. So somehow this value type needs to be treated as reference type. Here boxing can help us, we wrap it inside a class and share that. We create UserBox class like below:
class UserBox {
var user: User
init(user: User) {
self.user = user
}
}//Usage
let userObject = User(name: "Varun", profession: "Engineer")
let box = UserBox(user: userObject)
Above box is a reference type object, contains a struct i.e value type.
class Test {
var box: UserBox!
}
let test1 = Test()
let test2 = Test()
test1.box = box
test2.box = box//Usage
test1.box.user.name = "Varun Tomar"print(test1.box.user.name) // print Varun Tomar
print(test2.box.user.name) // print Varun Tomar
In above code we created test class having “UserBox” type variable. We created 2 instance of “Test” class “test1 & test2”. If we change user name of test1 then it gets changed for test2 as well. So, using boxing we change behaviour of a “value type” to “reference type” and get stored on heap.
Now coming to main point, SIL also used boxing concept internally to optimise things and store value type on heap. Let’s discuss this using an example.
protocol Wings { }
struct Bird: Wings { }
In above code we have a “struct”, which is supposed to be stored on stack (value type)🧐 but actually it’s not, let's generate its Swift Intermediate Language code. To generate SIL
use “swiftc -emit-silgen -0 <filename>.swift”
To run 🏃♂️ this command, go to swift file directory on terminal. You may get surprised 😱 😮 to 👀 below SIL generated code :
Clearly we can see how “Bird” struct get promoted to “reference type” from “value type” during SIL generation phase. This is one ☝️ of exceptions we discussed earlier when SIL code swap between “value type” and “reference type” to optimise things. Let's take one more , suppose this time we have a “struct” holding a reference to a class instance.👇
class Bird {}
struct Wing {
let bird = Bird()
}
SIL generated code from above file 👇
In above SIL code struct “Wing” gets wrapped in box and behave like “reference type”. There are few more exceptions, i think 🤔 you want to explore those yourself 😀😀. Nevertheless if you don’t, add a comment after this article. I can tell you more.🙃
As we know most value types are allocated on the stack and copying them takes constant time. Important thing is that if we have to copy a lot in a program, that may also lead to fair cost.To avoid it, swift follow “copy on write” approach. Most of Swift extensible types like strings, arrays, sets and dictionaries are copied on write. This means that copy only happens at the point of mutation.
On the other hand reference type only deals with reference counting costs, but each operation may requires several levels of indirection and must be performed atomically, since heap can be shared between multiple threads at the same time.
Note : Compiler do a lot of optimisation in different phases like in SIL. Sometimes even you don’t know, you are actually using a value type or reference type.
It’s interesting 🤔 🧐 huh !
📝 Save this story in Journal.
👩💻 Wake up every Sunday morning to the week’s most noteworthy stories in Tech waiting in your inbox. Read the Noteworthy in Tech newsletter.