Swift Intensely (Part 1)

Omar Radwan
10 min readApr 15, 2024

--

Let’s learn how Swift works internally and how to manage its performance.

Swift Performance

As iOS developers, Swift offers us aboard and powerful design space to explore. Swift has fantastic features with various mechanisms for code reusability and dynamism. With Swift, you have Struct, Class, Enum, Inheritance, Generics, and Protocols. Swift has different abstraction mechanisms to make a powerful app with design space, Modeling -> How to use protocol-oriented programming to model your app and Performance which will focus on this article.
Now let’s begin by identifying the different dimensions to achieve the best performance to clean up and speed up some swift code.

  • Memory Allocation
  • Reference counting
  • Method dispatch

Also, We will gonna evaluate the performance of POP and look at some advanced swift features in Part 2.

  • Protocol types
  • Generics

So let’s get started. When you writing a program and want to choose your abstraction mechanism you need to ask yourself some questions.
1- Is my instance gonna be allocated on the stack or heap?
2- When I pass this instance around how much reference counting overhead am I going to incur?
3- When I call a method on this instance, Is it going to be statically or dynamically dispatched?
If we want to write speed swift code we need to avoid paying for dynamism or runtime and heap allocation that we are not taking advantage of.

Memory Allocation

Swift automatically allocates and deallocates memory on your behalf. Some of that memory it allocates in the Stack.
A stack is a simple data structure, you can push at the end of the stack and you can pop off the end of the stack and we can implement the push and pop just by keeping the pointer to the end of the stack.

When we want to call a function or create an instance we can allocate that in memory just by decrementing the stack pointer to make a space and when to deallocate we can increment the pointer again. That’s how fast stack allocation and deallocation are!!. It’s literally the cost of assigning an integer.

Also, Some of that memory it allocates in the Heap.
A heap is more complex than a stack. it’s more dynamic but less efficient than the stack. The heap can allocate memory with a dynamic lifetime but that requires a more advanced data structure.
At heap allocation, you have to search for an unused block of memory with a suitable size, and at deallocation, it reinserts the block of memory back into the appropriate position.

We need to be careful when we deal with the heap that has a large cost like thread safety overhead and in this case, We have concurrency issues and can deal with it by locks or any other synchronization mechanisms.
Let’s take an example of Stack & Heap allocation.

// Stack allocation
// Struct

struct Point {
var x,y: Double
func draw() {}
}
let point1 = Point(x: 0, y: 0)
var point2 = point1
point2.x = 5

When we create instance point1 and because Point is a struct, the x and y properties are stored in line in the stack. When assigning point1 to point2 we create a copy of that point and allocate it also in the stack. Now point1 and point2 are independent instances so when assigning value 5 to point2.x point1.x is still 0 and this is known as value semantics.

Instance allocation in the stack

Now we will use Class instead of Struct to let us know what has changed in memory.

// Heap allocation
// Class

class Point {
var x,y: Double
func draw() {}
}
let point1 = Point(x: 0, y: 0)
var point2 = point1
point2.x = 5

We are allocating memory in the stack but instead of the actual storage of the properties on point, we are going to allocate memory for references to point1 and point2. References to memory going to be allocated on the heap. So when we create an instance swift will look at the heap and search for that data structure for unused blocks of memory with its size and store the reference of it in the stack. Swift in heap allocation to our class point allocated four words of storage. In addition to the x and y words storage, we’re allocated two more words that Swift is gonna manage on our behalf (Blue boxes). After assigning point1 to point2 we are not gonna copy it but it will share the same data with different references and this is called Reference semantic.

Instance allocation in the heap

Now we know why structs are cheaper than classes. classes require heap allocation and have reference semantics and because of that it has powerful characteristics like identity and indirect storage so if we don’t need this for our abstraction it will be better if we use a struct.

Hint: String in swift is a value type but actually in memory allocation it has the characteristics of references type and stores the contents of its characters indirectly on the heap.🤯

Reference Counting

After talking about heap allocation, Now we need to know how Swift knows it’s safe to deallocate memory allocated in the heap. Swift just keeps a count of the total number of references to any instance in the heap. When you add a reference or remove a reference that reference count is incrementing or decrementing. When this count is zero swift knows that no one is pointing to this instance on the heap anymore and it’s safe to deallocate memory.

Like heap allocation, we must take the thread safety overhead into consideration because every reference can be added or removed to any heap instance on multiple threads at the same time which gonna make an unexpected behavior to our program so this cost can add up.

// Heap allocation (Reference Counting)
// Class

class Point {
var x,y: Double
func draw() {}
}

let point1 = Point(x: 0, y: 0)
// retain is automatically increment our reference count
retain(point1)
var point2 = point1
//
retain(point2)
point2.x = 5
// release is automatically decrement our reference count
release(point1)
//
release(point2)
Reference counting on Point class

When we create an instance point1 swift increment ref. count to 1 and when creating another instance point2 and assigning point1 to it we increment ref. count to 2 after the end of function swift will deallocate the instances and decrement the ref. count 1 by 1 to zero and boom it’s clean now ❤️👏🏻.

So, what about Structs?
When we constructed our point as a struct there was no heap allocation involved and when we copied it there was no heap allocation. So, there is no reference counting overhead.

But, What if we have a more complicated struct that contains properties of type classes?

struct Label {
var text: String
var font: UIFont

func draw() {}
}

let label1 = Label(text: "Hello world", font: .fontSystem)
let label2 = label1

Now our struct contains a text of type String and font of type UIFont. String as I mentioned above stores its content of characters on the heap so, that needs to be reference counted and font is a class so also needs to be reference counted. If we look at the memory representation:-

Memory

label1 has two references one for text storage and one for font and when we copy it we will add two more references.
Swift tracks this process by retaining & releasing algorithms as we talk about above.

Conclusion — Swift has to track lifetime for each instance (class) on the heap by reference counting and this is just one more reason to use structs.
But, if struct contains references that will pay reference counting overhead as well. In fact struct will be pay reference counting overhead proportional to the number of references that they contain. if you have a struct that contains more than one reference they are retain more reference counting overhead than a class 😕. So, it will become bad idea if you use struct in this case.

Method Dispatch

Now let’s move on to our final dimension of performance Method Dispatch.

When you call a method at runtime, Swift needs to execute the correct implementation. If it can determine the implementation to execute at compile time, that’s known as Static Dispatch. At runtime, we are just going to be able to jump directly to the correct implementation. The compiler has visibility of the correct implementation so it’s able to optimize this code pretty aggressively including things like inlining.

This is in contrast to a Dynamic Dispatch. Dynamic dispatch isn’t going to determine a compile time directly which implementation to go to and look up implementation in the table at runtime and then jump to it.

Note: Dynamic dispatch is not that much more expensive than static dispatch. There’s just one level of indirection.

But, this dynamic dispatch blocks the visibility of the compiler and so while the compiler can do all these cool optimizations for static dispatch, a dynamic dispatch, the compiler is not going to be able to reason through it.

// Method Dispatch (Static)
// Struct (inlining)

struct Point {
var x, y: Double
func draw() {
// Some implementation
}
}

func drawAtPoint(_ point: Point) {
point.draw()
}

let point = Point(x: 0, y: 0)
drawAtPoint(point)

Well, in this example we have the draw method into our Point and drawAtPoint method which are both statically dispatched. What this means is the compiler knows exactly which implementation is going to be executed. That’s why static dispatch is Fast.

So, Why and when do we have the dynamic dispatch thing at all?
Well, one of the reasons is it enables really powerful things like Inheritance-Based Polymorphism.

Let’s look at the traditional Object-oriented programming code.

/// Drawabale Abstract Class
class Drawabale { func draw() {} }

class Point: Drawabale {
var x, y: Double
override func draw() {...}
}

class Line: Drawabale {
var x, y: Double
override func draw() {...}
}

var drawables: [Drawabale]
for drawable in drawables {
drawable.draw()
}

Now we have a program that can polymorphically create an array of Drawabales and each one of them has its custom draw implementation and that’s when the dynamic dispatch comes into play. So, How this works? How does the compiler know the actual implementation for each one of this array of classes?

Well, because Drawable, Line, and Point are all classes, We can create an array of these things and they all are the same size because we are storing them by reference in the array and then we go through each of them and call draw() to each of them. Now we know why the compiler can’t determine at compile time which is the correct implementation to execute. So, drawable.draw() could be a line or point or anything else. But still, we want to know how does it determine which one to call!?

Answer: Polymorphism Through V-Table Dispatch

V-Table Dispatch

Well, The compiler adds another field to classes which is a pointer to the type information of that class, and stores it in static memory.

When we go to call draw, The compiler looks through the type to something called the Virtual Method Table on the type and static memory, which contains a pointer to the actual implementation to execute. After looking at the correct draw implementation in the V-Table by its type, it passes the actual instance as the implicit self-parameter.

/// Compiler Execution

for drawable in drawables {
// Compiler replace this "drawable.draw()" with this
drawable.type.vTable.draw(drawable)
}

Well, Classes by default are dynamically dispatched to their methods. This doesn’t make a big difference on its own, but when it comes to method chaining and other things, it can prevent optimizations.

Note: Not all classes are dynamically dispatched 🧐. When you mark your class with the final keyword, That means you never intend for a class to be subclassed. The compiler will pick up on this and it’s going to be statically dispatched its methods.

Also plus its performance advantages, The final keyword has security importance check this amazing article for Dave Poirier.
https://blog.encoded.life/the-importance-of-final-for-security?source=more_articles_bottom_blogs

Conclusion

What I want you to know from this part is these questions to ask yourself every time you gonna design a model or read or write a Swift code.
Is this instance gonna be allocated in the stack or the heap?
How much reference counting do I have when passing a reference around?
when I call a method in an instance, Is it gonna be statically or dynamically dispatched?
I think now you have a full understanding and deep dive into how the swift things work.
Every iOS developer needs to understand this not just only struct is a value type and class is a reference type without knowing what happening behind the scenes.

So, That's the end of Swift Intensely (Part 1) ❤️ .

We will continue and wait for you in Part 2 to talk about more advanced topics in Swift like

  • Protocol types
  • Generics

See you 👋🏻🍎

--

--