• Memory layout in Swift
  • By Tibor Bodecs
  • Translation from: The Gold Project
  • This article is permalink: github.com/xitu/gold-m…
  • Translator: LoneyIsError
  • Proofreader: liyaxuanliyaxuan PassionPenguin

The author referenced this article for the memory analysis of structs and classes in Swift.

Memory layout in Swift

Memory layout for value types in Swift

Memory is simply a string of 1s and 0s, called bits for short. If the bitstream is divided into groups of 8 bits, we can call this new unit bytes (8 bits is a byte, for example, binary 10010110 is hexadecimal 96). We can also visualize these bytes in hexadecimal form (e.g. 96 A6 6D 74 B2 4C 4A 15, etc.). Now if we divide these visual bytes into eight groups, we get a new unit called a word.

This 64-bit memory (that is, a word for 64-bit) 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 hexadecimal number (usually 64 bits). Prior to the x86-64 era, the x32 ABI used 32-bit long addresses with a maximum memory limit of 4GiB. Fortunately, we are using x64. 💪

So, how do we store data types in the virtual memory address space? Well, to make a long story short, we allocate an appropriate amount of space for each data type and write the hexadecimal representation of the value to memory. That’s the magic of the operating system, and that’s how it works.

We could also start talking about segmentation, paging, and other low-level stuff, but to be honest, I really don’t know how any of this works. As I delved deeper and deeper into this kind of low-level stuff, I learned a lot about how computers work behind the scenes.

I want to share with you an important point that I already know. This is about memory access on various architectures. For example, if the CPU’s bus width is 32 bits, this means that the CPU can only read 32 bits from memory in one read cycle. Now, if we simply write each object to memory without proper data separation, we might cause some trouble.

┌ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ┬ ─ ─ ─ ─ ─ ─ ┬ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ┐ │... │ │ 4 b... │ ├ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ┴ ─ ─ ─ ┬ ─ ─ ┴ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ┤ 32 bytes │ │ 32 bytes │ └ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ┴ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ┘Copy the code

As you can see, the first read cycle can only read the first part of the 4-bit data object if the memory data is not aligned. It takes two read cycles to get our data back from a given memory space. This is very inefficient and dangerous, which is why most operating systems do not allow unaligned access, and why programs crash immediately. So, what does the memory layout look like in Swift? Let’s take a quick look at our data types using the built-in MemoryLayout enumeration 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
Copy the code

As you can see, Swift uses one byte to store Bool values and (on 64-bit systems) eight bytes to store Int types. So what’s the difference between size, stride, and alignment?

Alignment will tell you how much memory (a multiple of its value) is required to store fully aligned content in the memory buffer. Size is the number of bytes needed to actually store the type. Stride will tell you the distance between two elements on the buffer. If you don’t know anything about these informal definitions, don’t worry, you’ll find out later.

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
Copy the code

When constructing a new data type, in our case a structure (classes work differently), we can calculate the memory layout of the entire structure based on the memory layout of the properties in the structure.

┌ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ┬ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ┐ │ 16 bytes stride 16 bytes (8 x2) │ stride (8 x2) │ ├ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ┬ ─ ─ ─ ─ ─ ─ ┬ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ┼ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ┬ ─ ─ ─ ─ ─ ─ ┬ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ┤ 8 bytes │ │ │ 1 b 7 bytes │ 8 bytes 7 bytes 1 b │ │ │ ├ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ┴ ─ ─ ─ ─ ─ ─ ┼ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ┼ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ┴ ─ ─ ─ ─ ─ ─ ┼ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ┤ │ 9 bytes size (8 + 1) │ The padding bytes │ 9 size (8 + 1) │ padding │ └ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ┴ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ┴ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ┴ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ┘Copy the code

In Swift, the size of a simple alignment is the same as its size. If the standard Swift data types are stored in a contiguous memory buffer, there is no need to fill, so each stride will be equivocal with these types.

When using a compound type, such as the structure in Example, the maximum value of the alignment attribute (8) is used for the memory alignment value of the type. Size is the sum of the properties (8 + 1), and the stride can be calculated by rounding the size to the next multiple of the alignment. Is that true in all circumstances? 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
Copy the code

What the hell is going on here? Why does the size increase? Increasing size becomes a little tricky, because if padding is between stored variables, it increases the overall size of our type. You can’t start with 1 byte and then add 8 bytes after it because that would misalign the integer type, so you need 1 byte, then 7 bytes of padding, and finally 8 bytes to store the integer value.

┌ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ┬ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ┐ │ 16 bytes stride 16 bytes (8 x2) │ stride (8 x2) │ ├ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ┬ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ┬ ─ ─ ─ ─ ─ ─ ┼ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ┬ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ┬ ─ ─ ─ ─ ─ ─ ┤ 8 bytes │ │ 7 bytes │ │ 1 b 8 bytes 1 b 7 bytes │ │ │ └ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ┼ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ┼ ─ ─ ─ ─ ─ ─ ┴ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ┼ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ┼ ─ ─ ─ ─ ─ ─ ┘ │ padding │ │ padding │ ┌ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ┴ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ┴ ─ ─ ─ ─ ─ ─ ┬ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ┴ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ┴ ─ ─ ─ ─ ─ ─ ┐ │ 16 bytes size 16 bytes (1 + 7 + 8) │ the size (1 + 7 + 8) │ └ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ┴ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ┘Copy the code

This is the main reason why the size value of the second example structure is slightly increased. Feel free to create other types and practice by drawing a memory layout for them. You can always use Swift to print the memory layout at run time to check that your drawing is correct. 💡

The whole issue is well explained on the [Swift Unboxed] blog. I’d also like to recommend this post by Steven Curtis, and this great post on Unsafe Swift: A Road to Memory. These articles have helped me a lot in understanding the memory layout in Swift. 🙏

Memory layout for reference types in Swift

As I mentioned earlier, classes behave very differently because they are reference types. Let me change the example type to a class and see what happens to 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
Copy the code

What? Why? Until now, we’ve been talking about memory held in the stack. Stack memory is reserved for static memory allocation, and there is also something called the heap for dynamic memory allocation. We can simply say that value types (struct, Int, Bool, Float, etc.) exist in the stack, while 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, we’ll leave it at that.

You might ask the question: Why is there a stack and a heap? That’s because they’re completely different. The stack can be faster because memory is allocated using push/pop operations, but you can only add or remove items from it. The stack size is also limited. Have you ever encountered stack overflow errors? The heap allows random memory allocation, but you must be sure to free up any memory you request. Another disadvantage is that there is some overhead associated with the allocation process, but there is no size limit other than the physical capacity of RAM. Stacks and heaps are completely different, but they are both very useful. 👍

Going back to the whole, how did you get that every value here (size, stride, alignment) is 8? We can use the class_getInstanceSize method to calculate the actual size (in bytes) of an object on the heap. A class contains at least 16 bytes of metadata (that is, just print out the size of the empty class using the class_getInstanceSize method) plus the calculated value of the instance variable.

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)
Copy the code

The memory layout of the class is always 8 bytes, but the actual size it gets from the heap depends on the instance variable type. The other 16 bytes come from the “is a” pointer and reference count. If you know anything about the Objective-C runtime, this might sound familiar, but if not, don’t worry too much about ISA Pointers here. We’ll talk about this next time. 😅

Swift uses automatic reference counting (ARC) to track and manage your application’s memory usage. Thanks to ARC, for the most part, you don’t have to worry about manual memory management. You just need to make sure that you don’t create strong reference loops between class instances. Fortunately, these situations can be easily resolved with weak or undirected 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
Copy the code

As shown in the above example, if we don’t use weak references, then objects will strongly refer to each other, forming circular references, so that even if you set individual Pointers to nil, they won’t be released (deinit won’t be called at all). This is a very basic example, but the real question is when do I need to use weak, unowned, or strong? 🤔

I hate to say “it depends,” so I want to point you in the right direction. If you look closely at the official documentation with closed packages, you’ll see which ones capture values:

  • A global function is a closure that has a name and does not capture any value.
  • A nested function is a closure with a name that captures a value from its enclosing function.
  • A closure expression is an unnamed closure written in lightweight syntax that can capture values from its context.

As you can see, the global (static function) does not increment the reference counter. Nested functions, on the other hand, capture values, which is also true for closure expressions and unnamed closures, but it’s a little more complicated. To learn more about closures and value capture, I recommend the following two articles:

  • You don’t need [weak self]
  • Weak, strong, unowned, my god!

Long story short, circular references suck, but in most cases they can be avoided by using the right keywords. Behind the scenes, ARC does a great job, except in some cases where you have to break the reference loop. Swift is designed to be a memory-safe programming language. The language ensures that each object will be initialized before it can be used, and that objects in memory that are no longer referenced will be automatically freed. The array index is also checked for out-of-bounds errors. This gives us an extra layer of security unless we’re writing insecure Swift code… 🤓

In short, this is the memory layout in Swift.

If you find any errors in the translation or other areas that need improvement, you are welcome to revise and PR the translation in the Gold Translation program, and you can also get corresponding bonus points. The permanent link to this article at the beginning of this article is the MarkDown link to this article on GitHub.


Diggings translation project is a community for translating quality Internet technical articles from diggings English sharing articles. The content covers the fields of Android, iOS, front end, back end, blockchain, products, design, artificial intelligence and so on. For more high-quality translations, please keep paying attention to The Translation Project, official weibo and zhihu column.