Swift Intensely (Part 2)

Omar Radwan
9 min readApr 15, 2024

--

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

Swift Performance ||

Hello, welcome back again 🥰. In Part 1 we discussed a lot of interesting things like Memory allocation, Reference counting, and Method dispatch and learned how Swift manages its memory behind the scenes. I suggest going and reading it first before reading this part.

This part will talk about Protocol Types and Generics in a different way than you already know. So, Let’s get started.

Protocol Types

How can we go to writing a polymorphism code with structs?
Answer: Protocol Oriented Programming (POP)

With protocol types, we will look at how variables of protocol types are stored and copied and how method dispatch works. Let’s go back to the main example we have in the previous part but this time we will use protocol types and structs.

// Polymorphism without inhertance

protocol Drawable { func draw() }

struct Point: Drawable {
var x,y: Double
func draw() {...}
}

struct Line: Drawable {
var x1,x2,y1,y2: Double
func draw() {...}
}

var drawables: [Drawable]
for d in drawables {
d.draw()
}

Our program is now still polymorphic and each struct is conformed to Drawable will draw its implementation. But, there is one different thing compared to the last example in part 1.
Now we have value types and they don’t have a common inheritance relationship or reference in memory which is important to create the V-Table dispatch mechanism that reference types use.

So, How does Swift dispatch to the correct method?!
Answer: Protocol Witness Table (PWT)

Protocol Witness Table (PWT)

The table-based mechanism PWT is the table Swift creates for each protocol and what conforms to this protocol to find the correct implementation for each one by linking it. Now we have to know how to find the method but actually, we still have two questions 😕.
1- How do we get from the element into that array to that table? or How does the function know the location of this implementation in that Table?
2- How we will store our different sizes at the fixed offset of the array? (Line has 4 words and Point has 2 words)

Swift uses a special storage layout called The Existential Container.

In Swift, the term “The existential container” refers to the runtime representation of a value of a protocol type and Swift create it in the stack.

When you have a protocol type, Swift needs to store not only the value itself but also information about the type that conforms to the protocol. This information includes things like the type’s methods, properties, and associated types. The existential container is a runtime structure that holds both the value of the conforming type and the necessary type metadata.

The existential container allows you to work with values of protocol types in a type-erased manner, meaning you can interact with values without knowing the concrete types that conform to the protocol. This is commonly used in scenarios where you need to work with collections of values of different types that conform to the same protocol.

The existential container contains five rows. The first three rows or words are reserved for the value buffer. Small types like our point (x,y) fit into our value buffer in the first two words. Okay but wait a second! What about our line which contains four words? The answer is Swift allocates memory on the heap and takes a pointer to this memory.

The size of the value buffer within an existential container is not fixed at 3 words. It can vary depending on the size and alignment requirements of the concrete type being stored.

In Swift, the compiler dynamically allocates memory for the value buffer based on the requirements of the concrete type. If the concrete type fits within the value buffer (which typically includes a few words of memory), it is stored directly within the buffer. However, if the concrete type requires more space than the value buffer can hold, Swift allocates additional memory on the heap and stores a reference to it in the value buffer.

So, while the value buffer may be a few words in size for smaller types, it can expand dynamically to accommodate larger types or heap-allocated storage.

Now let’s ask another question. What about the last two words in the existential container? and how Swift manages the difference between point and line.

The answer is by our pointers to tables the first is the Protocol Witness Table (PWT) we talked about above and the new one is the table-based mechanism called the Value Witness Table (VWT). Let’s take a look ☺️

Value Witness Table

The Value Witness Table manages the lifetime of our value and there is one type of table per type in our program. The VWT contains four types of functions (Operations).

allocate: this function will allocate memory on the stack or allocate on the heap and store a pointer inside the value buffer of the existential container that is allocated in the stack.
copy: Swift needs to copy the value from the source of the assignment that initializes our local variable into the existential container.
destruct: decrement any reference counts for values that might be contained in our type.
deallocate: deallocate the memory allocated in the heap for the value.

So, We have seen the mechanics of how Swift manages values of protocol type. Let’s see it in actual code step by step.

struct Line: Drawable {
var x1,x2,y1,y2: Double
func draw() {...}
}

func drawACopy(local: Drawable) { // 3 copy
local.draw() // 4 (PWT)
} // 5 -> deallocate
let line: Drawable = Line(x1: 0.0, x2: 0.0, y1: 0.0, y2: 3.0) // 1
drawACopy(local: line) // 2 allocate

-------------------------------------------
// Pseudo code
// Generated code by compiler

struct ExistContainerDrawable {
var valueBuffer: (Int, Int, Int)
var vwt: ValueWitnessTable
var pwt: DrawableProtocolWitnessTable
}

func drawACopy(val: ExitContDrawable) {
var local = ExistContainerDrawable()
let vwt = val.vwt
let pwt = val.pwt
local.pwt = pwt
vwt.allocateBufferAndCopyValue(&local, val)
// projectBuffer get the address of the existential container
pwt.draw(vwt.projectBuffer(&local))
}

Simply, The swift code example above explains what happened in the pics. First, Swift initializes the local existential container in the stack when we create a line and pass it to the drawACopy function. Then, the Value Witness Table allocates a place for a line in memory (in heap to our line case) and copies the values x1,y1,x2, and y2 to put them in the memory. After that, it will call the PWT to call the draw() method. At last, Swift uses VWT-deallocate to deallocate the instance from memory (stack&heap).

Summary of Protocol Types
we saw how dynamic polymorphism works with value types throw protocol types and indirection through Witness Tables and Existential container.

Generics

In this last part, We will see generics but from another POV. I will look at how variables of generic type are stored and copied and how method dispatch works for them.

Generic code supports a more static form of polymorphism which is known as parametric polymorphism (One type per call context) and it supports a little bit more better performance.

protocol Drawable {
func draw()
}

struct Point: Drawable {
var x1,x2: Double
func draw() {...}
}

func foo<T: Drawable>(local: T) {
bar(local)
}

func bar<T: Drawable>(local: T) {....}

let point = Point(x1: 0.0, x2: 0.0)
foo(local: point)

We have a function foo which takes a generic parameter T constraint to Drawable and it passes this parameter to function bar which also takes generic parameter T. When function foo(local: point) executes, Swift will bind the generic type T to the type used at this call side (Point) -> foo<T = Point>(local: point). So, The type substituted down the call chain -> bar<T = Point>(local: point).
This is what we mean by a more static form of polymorphism.

Let’s take a look at how Swift implements this under the hood.

In Swift, there are two types of terms for generics. the terms “Specialization” and “Unspecialization” are often used in the context of generics and performance optimization.

Specialization: When a generic function or type is specialized, it means that the compiler generates separate implementations of that function or type for each concrete type it’s used with. This process allows the compiler to optimize the generated code specifically for each type, potentially improving performance by removing the overhead of dealing with generics at runtime. Specialization can result in more efficient code execution because the compiler can make assumptions about the concrete types involved.

// Define a generic function that adds two values of any type
func add<T>(_ a: T, _ b: T) -> T where T: Numeric {
return a + b
}

// Call the generic function with two integers
let resultInt = add(5, 3)
print("Result (Int): \(resultInt)")

// Call the generic function with two floating-point numbers
let resultDouble = add(3.5, 2.5)
print("Result (Double): \(resultDouble)")

In this example, the add function is generic and can work with any type that conforms to the Numeric protocol. When you call the function with Int arguments, the compiler generates a specialized version of the add function specifically for Int, optimizing the addition operation for integers. Similarly, when you call the function with Double arguments, the compiler generates another specialized version optimized for Double. These specialized versions improve performance by removing the overhead of generics and allowing the compiler to make assumptions about the concrete types involved.

Swift compiler creates type-specific version of method and this kind to be really fast code. Swift will create a version per type use at the call site and it will look like that at compile time :-

func add(_ a: Int, _ b: Int) -> Int{
return a + b
}

func add(_ a: Double, _ b: Double) -> Double{
return a + b
}

let resultInt = add(5, 3)
let resultDouble = add(3.5, 2.5)

Okay, that's really awesome no dynamic dispatch again everything is static, and Swift will create for each call a new static function at compile time and the code becomes really fast with Specialization.
But wait a second! Is this a potential to increase code size by a lot right?!

Actually, the answer is No :D. Because the static typing information that is now available enables aggressive compiler optimization. Swift can reduce the code size here.

// func add(_ a: Int, _ b: Int) -> Int{
// return a + b
// }

// func add(_ a: Double, _ b: Double) -> Double{
// return a + b
// }

let resultInt = 5 + 3
let resultDouble = 3.5 + 2.5

Unspecialization: Unsplization refers to the process of reverting from specialized code back to more general, generic code. This might occur in scenarios where the compiler determines that specialization is no longer necessary or beneficial. For example, if a specialized function is used with a wide variety of types, the compiler might choose to unspecialize it to reduce code size or improve compilation times.

// Define a generic function that prints the description of a value
func printDescription<T>(_ value: T) {
print("Description: \(value)")
}

// Call the generic function with different types
printDescription(42) // Int
printDescription("Hello") // String

In this example, the printDescription function is generic and can be called with values of any type. When you call the function with different types (e.g., Int and String), the compiler generates specialized versions of the function for each type. However, if the function is used with a wide variety of types or if the optimization benefits are limited, the compiler might choose to unspecialize the function to reduce code size or improve compilation times.

These examples demonstrate how specialization and unspecialization work in Swift’s generics system, optimizing code performance and compilation efficiency based on the usage context.

In summary, specialization involves generating optimized code tailored to specific types, while unspecialization involves reverting to more generic code when optimization is not needed or when it’s more efficient to do so. These concepts are important for understanding how Swift’s generics system works and how it impacts the performance of your code.

Conclusion

Always choose a fitting abstraction with the best dynamic runtime type requirements.

  • struct types: Value semantics
  • class types: Identity & OOP style polymorphism
  • Generics: Static polymorphism (Homogeneous)
  • Protocol types: Dynamic polymorphism (Heterogeneous)

I hope you enjoyed this article and if you have a question you can ask me anytime on my LinkedIn.

Thanks a lot ❤️👋🏻

--

--