Memory layout in Swift

Start learning about how Swift manages, stores and references various data types and objects using a memory safe approach.

Swift

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. πŸ’‘

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 storages. πŸ‘

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.

Share this article on Twitter.
Thank you. πŸ™

Picture of Tibor BΓΆdecs

Tibor BΓΆdecs

Creator of https://theswiftdev.com (weekly Swift articles), server side Swift enthusiast, full-time dad. -- Follow me & feel free to say hi. 🀘🏻 -- #iOSDev #SwiftLang

Twitter · GitHub


πŸ“¬

100% Swift news, delivered right into your mailbox

Subscribe to my monthly newsletter. On the first Monday of every month, you'll get an update about the most important Swift community news, including my articles.