Memory layout in Swift
Start learning about how Swift manages, stores and references various data types and objects using a memory safe approach.
Memory layout of value types in Swift
Memory is just a bunch of 1
s and 0
s, simply called bits (binary digits). If we group the flow of bits into groups of 8, we can call this new unit byte (eight bit is a byte, e.g. binary 10010110 is hex 96). We can also visualize these bytes in a hexadecimal form (e.g. 96 A6 6D 74 B2 4C 4A 15 etc). Now if we put these hexa representations into groups of 8, weβll get a new unit called word.
This 64bit memory (a word represents 64bit) layout is the basic foundation of our modern x64 CPU architecture. Each word is associated with a virtual memory address which is also represented by a (usually 64bit) hexadecimal number. Before the x86-64 era the x32 ABI used 32bit long addresses, with a maximum memory limitation of 4GiB. Fortunately we use x64 nowadays. πͺ
So how do we store our data types in this virtual memory address space? Well, long story short, we allocate just the right amount of space for each data type and write the hex representation of our values into the memory. Itβs magic, provided by the operating system and it just works.
We could also start talking about memory segmentation, paging, and other low level stuff, but honestly speaking I really donβt know how those things work just yet. As Iβm digging deeper and deeper into low level stuff like this Iβm learning a lot about how computers work under the hood.
One important thing is that I already know and I want to share with you. It is all about memory access on various architectures. For example if a CPUβs bus width is 32bit that means the CPU can only read 32bit words from the memory under 1 read cycle. Now if we simply write every object to the memory without proper data separation that can cause some trouble.
ββββββββββββββββββββββββββββ¬βββββββ¬ββββββββββββββββββββββββββββ
β ... β 4b β ... β
ββββββββββββββββββββββββββββ΄ββββ¬βββ΄ββββββββββββββββββββββββββββ€
β 32 bytes β 32 bytes β
ββββββββββββββββββββββββββββββββ΄βββββββββββββββββββββββββββββββ
As you can see if our memory data is misaligned, the first 32bit read cycle can only read the very first part of our 4bit data object. Itβll take 2 read cycles to get back our data from the given memory space. This is very inefficient and also dangerous, thatβs why most of the systems wonβt allow you unaligned access and the program will simply crash. So how does our memory layout looks like in Swift? Letβs take a quick look at our data types using the built-in MemoryLayout enum type.
print(MemoryLayout<Bool>.size) // 1
print(MemoryLayout<Bool>.stride) // 1
print(MemoryLayout<Bool>.alignment) // 1
print(MemoryLayout<Int>.size) // 8
print(MemoryLayout<Int>.stride) // 8
print(MemoryLayout<Int>.alignment) // 8
As you can see Swift stores a Bool value using 1 byte and (on 64bit systems) Int will be stored using 8 bytes. So, what the heck is the difference between size, stride and alignment?
The alignment will tell you how much memory is needed (multiple of the alignment value) to save things perfectly aligned on a memory buffer. Size is the number of bytes required to actually store that type. Stride will tell you about the distance between two elements on the buffer. Donβt worry if you donβt understand a word about these informal definitions, itβll all make sense just in a moment.
struct Example {
let foo: Int // 8
let bar: Bool // 1
}
print(MemoryLayout<Example>.size) // 9
print(MemoryLayout<Example>.stride) // 16
print(MemoryLayout<Example>.alignment) // 8
When constructing new data types, a struct in our case (classes work different), we can calculate the memory layout properties, based on the memory layout attributes of the participating variables.
βββββββββββββββββββββββββββββββββββββββ¬ββββββββββββββββββββββββββββββββββββββ
β 16 bytes stride (8x2) β 16 bytes stride (8x2) β
ββββββββββββββββββββ¬βββββββ¬ββββββββββββΌβββββββββββββββββββ¬βββββββ¬ββββββββββββ€
β 8 bytes β 1b β 7 bytes β 8 bytes β 1b β 7 bytes β
ββββββββββββββββββββ΄βββββββΌββββββββββββΌβββββββββββββββββββ΄βββββββΌββββββββββββ€
β 9 bytes size (8+1) β padding β 9 bytes size (8+1) β padding β
βββββββββββββββββββββββββββ΄ββββββββββββ΄ββββββββββββββββββββββββββ΄ββββββββββββ
In Swift, simple types have the same alignment value size as their size. If you store standard Swift data types on a contiguous memory buffer thereβs no padding needed, so every stride will be equal with the alignment for those types.
When working with compound types, such as the Example struct is, the memory alignment value for that type will be selected using the maximum value (8) of the properties alignments. Size will be the sum of the properties (8 + 1) and stride can be calculated by rounding up the size to the next the next multiple of the alignment. Is this true in every case? Well, not exactlyβ¦
struct Example {
let bar: Bool // 1
let foo: Int // 8
}
print(MemoryLayout<Example>.size) // 16
print(MemoryLayout<Example>.stride) // 16
print(MemoryLayout<Example>.alignment) // 8
What the heck happened here? Why did the size increase? Size is tricky, because if the padding comes in between the stored variables, then itβll increase the overall size of our type. You canβt start with 1 byte then put 8 more bytes next to it, because youβd misalign the integer type, so you need 1 byte, then 7 bytes of padding and finally the 8 bypes to store the integer value.
βββββββββββββββββββββββββββββββββββββββ¬ββββββββββββββββββββββββββββββββββββββ
β 16 bytes stride (8x2) β 16 bytes stride (8x2) β
ββββββββββββββββββββ¬ββββββββββββ¬βββββββΌβββββββββββββββββββ¬ββββββββββββ¬βββββββ€
β 8 bytes β 7 bytes β 1b β 8 bytes β 7 bytes β 1b β
ββββββββββββββββββββΌββββββββββββΌβββββββ΄βββββββββββββββββββΌββββββββββββΌβββββββ
β padding β β padding β
ββββββββββββββββββββ΄ββββββββββββ΄βββββββ¬βββββββββββββββββββ΄ββββββββββββ΄βββββββ
β 16 bytes size (1+7+8) β 16 bytes size (1+7+8) β
βββββββββββββββββββββββββββββββββββββββ΄ββββββββββββββββββββββββββββββββββββββ
This is the main reason why the second example struct has a slightly increased size value. Feel free to create other types and practice by drawing the memory layout for them, you can always check if you were correct or not by printing the memory layout at runtime using Swift. π‘
NOTE: This whole problem is real nicely explained on the [swift unboxed] blog. I would also like to recommend this article by Steven Curtis and there is one more great post about Unsafe Swift: A road to memory. These writings helped me a lot to understand memory layout in Swift. π
Reference types and memory layout in Swift
I mentioned earlier that classes behave quite different thatβs because they are reference types. Let me change the Example type to a class and see what happens with the memory layout.
class Example {
let bar: Bool = true // 1
let foo: Int = 0 // 8
}
print(MemoryLayout<Example>.size) // 8
print(MemoryLayout<Example>.stride) // 8
print(MemoryLayout<Example>.alignment) // 8
What, why? We were talking about memory reserved in the stack, until now. The stack memory is reserved for static memory allocation and thereβs an other thing called heap for dynamic memory allocation. We could simply say, that value types (struct, Int, Bool, Float, etc.) live in the stack and reference types (classes) are allocated in the heap, which is not 100% true. Swift is smart enough to perform additional memory optimizations, but for the sake of βsimplicityβ letβs just stop here.
You might ask the question: why is there a stack and a heap? The answer is that they are quite different. The stack can be faster, because memory allocation happens using push / pop operations, but you can only add or remove items to / from it. The stack size is also limited, have you ever seen a stack overflow error? The heap allows random memory allocations and you have to make sure that you also deallocate what youβve reserved. The other downside is that the allocation process has some overhead, but there is no size limitation, except the physical amount of RAM. The stack and the heap is quite different, but they are both extremely useful memory storage. π
Back to the topic, how did we get 8 for every value (size, stride, alignment) here? We can calculate the real size (in bytes) of an object on the heap by using the class_getInstanceSize
method. A class always has a 16 bytes of metadata (just print the size of an empty class using the get instance size method) plus the calculated size for the instance variables.
class Empty {}
print(class_getInstanceSize(Empty.self)) // 16
class Example {
let bar: Bool = true // 1 + 7 padding
let foo: Int = 0 // 8
}
print(class_getInstanceSize(Example.self)) // 32 (16 + 16)
The memory layout of a class is always 8 byte, but the actual size that itβll take from the heap depends on the instance variable types. The other 16 byte comes from the βis aβ pointer and the reference count. If you know about the Objective-C runtime a bit then this can sound familiar, but if not, then donβt worry too much about ISA pointers for now. Weβll talk about them next time. π
Swift uses Automatic Reference Counting (ARC) to track and manage your appβs memory usage. In most of the cases you donβt have to worry about manual memory management, thanks to ARC. You just have to make sure that you donβt create strong reference cycles between class instances. Fortunately those cases can be resolved easily with weak or unowned references. π
class Author {
let name: String
/// weak reference is required to break the cycle.
weak var post: Post?
init(name: String) { self.name = name }
deinit { print("Author deinit") }
}
class Post {
let title: String
/// this can be a strong reference
var author: Author?
init(title: String) { self.title = title }
deinit { print("Post deinit") }
}
var author: Author? = Author(name: "John Doe")
var post: Post? = Post(title: "Lorem ipsum dolor sit amet")
post?.author = author
author?.post = post
post = nil
author = nil
/// Post deinit
/// Author deinit
As you can see in the example above if we donβt use a weak reference then objects will reference each other strongly, this creates a reference cycle and they wonβt be deallocated (deinit wonβt be called at all) even if you set individual pointers to nil. This is a very basic example, but the real question is when do I have to use weak, unowned or strong? π€
I donβt like to say βit dependsβ, so instead, Iβd like to point you into the right direction. If you take a closer look at the official documentation about Closures, youβll see what captures values:
- Global functions are closures that have a name and donβt capture any values.
- Nested functions are closures that have a name and can capture values from their enclosing function.
- Closure expressions are unnamed closures written in a lightweight syntax that can capture values from their surrounding context.
As you can see global (static functions) donβt increment reference counters. Nested functions on the other hand will capture values, same thing applies to closure expressions and unnamed closures, but itβs a bit more complicated. Iβd like to recommend the following two articles to understand more about closures and capturing values:
Long story short, retain cycles suck, but in most of the cases you can avoid them just by using just the right keyword. Under the hood, ARC does a great job, except a few edge cases when you have to break the cycle. Swift is a memory-safe programming language by design. The language ensures that every object will be initialized before you could use them, and objects living in the memory that arenβt referenced anymore will be deallocated automatically. Array indices are also checked for out-of-bounds errors. This gives us an extra layer of safety, except if you write unsafe Swift codeβ¦ π€
Anyway, in a nutshell, this is how the memory layout looks like in the Swift programming language.
Related posts
All about the Bool type in Swift
Learn everything about logical types and the Boolean algebra using the Swift programming language and some basic math.
Async HTTP API clients in Swift
Learn how to communicate with API endpoints using the brand new SwiftHttp library, including async / await support.
Beginners guide to functional Swift
The one and only tutorial that you'll ever need to learn higher order functions like: map, flatMap, compactMap, reduce, filter and more.
Beginner's guide to modern generic programming in Swift
Learn the very basics about protocols, existentials, opaque types and how they are related to generic programming in Swift.