Reminiscence structure of worth varieties in Swift
Reminiscence is only a bunch of `1`s and `0`s, merely referred to as bits (binary digits). If we group the circulate of bits into teams of 8, we will name this new unit byte (eight bit is a byte, e.g. binary 10010110 is hex 96). We are able to additionally visualize these bytes in a hexadecimal type (e.g. 96 A6 6D 74 B2 4C 4A 15 and many others). Now if we put these hexa representations into teams of 8, we’ll get a brand new unit referred to as phrase.
This 64bit reminiscence (a phrase represents 64bit) structure is the essential basis of our trendy x64 CPU structure. Every phrase is related to a digital reminiscence handle which can be represented by a (normally 64bit) hexadecimal quantity. Earlier than the x86-64 period the x32 ABI used 32bit lengthy addresses, with a most reminiscence limitation of 4GiB. Happily we use x64 these days. 💪
So how will we retailer our information varieties on this digital reminiscence handle house? Nicely, lengthy story quick, we allocate simply the correct amount of house for every information sort and write the hex illustration of our values into the reminiscence. It is magic, offered by the working system and it simply works.
We may additionally begin speaking about reminiscence segmentation, paging, and different low stage stuff, however actually talking I actually do not know the way these issues work simply but. As I am digging deeper and deeper into low stage stuff like this I am studying loads about how computer systems work beneath the hood.
One vital factor is that I already know and I wish to share with you. It’s all about reminiscence entry on varied architectures. For instance if a CPU’s bus width is 32bit meaning the CPU can solely learn 32bit phrases from the reminiscence beneath 1 learn cycle. Now if we merely write each object to the reminiscence with out correct information separation that may trigger some hassle.
┌──────────────────────────┬──────┬───────────────────────────┐
│ ... │ 4b │ ... │
├──────────────────────────┴───┬──┴───────────────────────────┤
│ 32 bytes │ 32 bytes │
└──────────────────────────────┴──────────────────────────────┘
As you’ll be able to see if our reminiscence information is misaligned, the primary 32bit learn cycle can solely learn the very first a part of our 4bit information object. It will take 2 learn cycles to get again our information from the given reminiscence house. That is very inefficient and in addition harmful, that is why a lot of the techniques will not permit you unaligned entry and this system will merely crash. So how does our reminiscence structure seems to be like in Swift? Let’s take a fast take a look at our information varieties utilizing the built-in MemoryLayout enum sort.
print(MemoryLayout<Bool>.measurement)
print(MemoryLayout<Bool>.stride)
print(MemoryLayout<Bool>.alignment)
print(MemoryLayout<Int>.measurement)
print(MemoryLayout<Int>.stride)
print(MemoryLayout<Int>.alignment)
As you’ll be able to see Swift shops a Bool worth utilizing 1 byte and (on 64bit techniques) Int will probably be saved utilizing 8 bytes. So, what the heck is the distinction between measurement, stride and alignment?
The alignment will let you know how a lot reminiscence is required (a number of of the alignment worth) to save lots of issues completely aligned on a reminiscence buffer. Measurement is the variety of bytes required to truly retailer that sort. Stride will let you know in regards to the distance between two components on the buffer. Don’t fret in case you do not perceive a phrase about these casual definitions, it’s going to all make sense simply in a second.
struct Instance {
let foo: Int
let bar: Bool
}
print(MemoryLayout<Instance>.measurement)
print(MemoryLayout<Instance>.stride)
print(MemoryLayout<Instance>.alignment)
When establishing new information varieties, a struct in our case (courses work completely different), we will calculate the reminiscence structure properties, primarily based on the reminiscence structure attributes of the taking part variables.
┌─────────────────────────────────────┬─────────────────────────────────────┐
│ 16 bytes stride (8x2) │ 16 bytes stride (8x2) │
├──────────────────┬──────┬───────────┼──────────────────┬──────┬───────────┤
│ 8 bytes │ 1b │ 7 bytes │ 8 bytes │ 1b │ 7 bytes │
├──────────────────┴──────┼───────────┼──────────────────┴──────┼───────────┤
│ 9 bytes measurement (8+1) │ padding │ 9 bytes measurement (8+1) │ padding │
└─────────────────────────┴───────────┴─────────────────────────┴───────────┘
In Swift, easy varieties have the identical alignment worth measurement as their measurement. For those who retailer customary Swift information varieties on a contiguous reminiscence buffer there is not any padding wanted, so each stride will probably be equal with the alignment for these varieties.
When working with compound varieties, such because the Instance
struct is, the reminiscence alignment worth for that sort will probably be chosen utilizing the utmost worth (8) of the properties alignments. Measurement would be the sum of the properties (8 + 1) and stride may be calculated by rounding up the scale to the subsequent the subsequent a number of of the alignment. Is that this true in each case? Nicely, not precisely…
struct Instance {
let bar: Bool
let foo: Int
}
print(MemoryLayout<Instance>.measurement)
print(MemoryLayout<Instance>.stride)
print(MemoryLayout<Instance>.alignment)
What the heck occurred right here? Why did the scale improve? Measurement is hard, as a result of if the padding is available in between the saved variables, then it’s going to improve the general measurement of our sort. You possibly can’t begin with 1 byte then put 8 extra bytes subsequent to it, since you’d misalign the integer sort, so that you want 1 byte, then 7 bytes of padding and at last the 8 bypes to retailer the integer worth.
┌─────────────────────────────────────┬─────────────────────────────────────┐
│ 16 bytes stride (8x2) │ 16 bytes stride (8x2) │
├──────────────────┬───────────┬──────┼──────────────────┬───────────┬──────┤
│ 8 bytes │ 7 bytes │ 1b │ 8 bytes │ 7 bytes │ 1b │
└──────────────────┼───────────┼──────┴──────────────────┼───────────┼──────┘
│ padding │ │ padding │
┌──────────────────┴───────────┴──────┬──────────────────┴───────────┴──────┐
│ 16 bytes measurement (1+7+8) │ 16 bytes measurement (1+7+8) │
└─────────────────────────────────────┴─────────────────────────────────────┘
That is the principle purpose why the second instance struct has a barely elevated measurement worth. Be at liberty to create different varieties and apply by drawing the reminiscence structure for them, you’ll be able to all the time verify in case you have been appropriate or not by printing the reminiscence structure at runtime utilizing Swift. 💡
This complete downside is actual properly defined on the [swift unboxed] weblog. I’d additionally prefer to advocate this text by Steven Curtis and there may be another nice publish about Unsafe Swift: A highway to reminiscence. These writings helped me loads to grasp reminiscence structure in Swift. 🙏
Reference varieties and reminiscence structure in Swift
I discussed earlier that courses behave fairly completely different that is as a result of they’re reference varieties. Let me change the Instance
sort to a category and see what occurs with the reminiscence structure.
class Instance {
let bar: Bool = true
let foo: Int = 0
}
print(MemoryLayout<Instance>.measurement)
print(MemoryLayout<Instance>.stride)
print(MemoryLayout<Instance>.alignment)
What, why? We have been speaking about reminiscence reserved within the stack, till now. The stack reminiscence is reserved for static reminiscence allocation and there is an different factor referred to as heap for dynamic reminiscence allocation. We may merely say, that worth varieties (struct, Int, Bool, Float, and many others.) dwell within the stack and reference varieties (courses) are allotted within the heap, which isn’t 100% true. Swift is wise sufficient to carry out further reminiscence optimizations, however for the sake of “simplicity” let’s simply cease right here.
You would possibly ask the query: why is there a stack and a heap? The reply is that they’re fairly completely different. The stack may be quicker, as a result of reminiscence allocation occurs utilizing push / pop operations, however you’ll be able to solely add or take away objects to / from it. The stack measurement can be restricted, have you ever ever seen a stack overflow error? The heap permits random reminiscence allocations and it’s a must to just be sure you additionally deallocate what you have reserved. The opposite draw back is that the allocation course of has some overhead, however there isn’t any measurement limitation, besides the bodily quantity of RAM. The stack and the heap is kind of completely different, however they’re each extraordinarily helpful reminiscence storages. 👍
Again to the subject, how did we get 8 for each worth (measurement, stride, alignment) right here? We are able to calculate the actual measurement (in bytes) of an object on the heap through the use of the class_getInstanceSize
technique. A category all the time has a 16 bytes of metadata (simply print the scale of an empty class utilizing the get occasion measurement technique) plus the calculated measurement for the occasion variables.
class Empty {}
print(class_getInstanceSize(Empty.self))
class Instance {
let bar: Bool = true
let foo: Int = 0
}
print(class_getInstanceSize(Instance.self))
The reminiscence structure of a category is all the time 8 byte, however the precise measurement that it will take from the heap is determined by the occasion variable varieties. The opposite 16 byte comes from the “is a” pointer and the reference rely. If you recognize in regards to the Goal-C runtime a bit then this will sound acquainted, but when not, then don’t be concerned an excessive amount of about ISA pointers for now. We’ll discuss them subsequent time. 😅
Swift makes use of Automated Reference Counting (ARC) to trace and handle your app’s reminiscence utilization. In a lot of the instances you do not have to fret about guide reminiscence administration, due to ARC. You simply must just be sure you do not create robust reference cycles between class cases. Happily these instances may be resolved simply with weak or unowned references. 🔄
class Creator {
let identify: String
weak var publish: Publish?
init(identify: String) { self.identify = identify }
deinit { print("Creator deinit") }
}
class Publish {
let title: String
var writer: Creator?
init(title: String) { self.title = title }
deinit { print("Publish deinit") }
}
var writer: Creator? = Creator(identify: "John Doe")
var publish: Publish? = Publish(title: "Lorem ipsum dolor sit amet")
publish?.writer = writer
writer?.publish = publish
publish = nil
writer = nil
As you’ll be able to see within the instance above if we do not use a weak reference then objects will reference one another strongly, this creates a reference cycle and so they will not be deallocated (deinit will not be referred to as in any respect) even in case you set particular person tips that could nil. It is a very fundamental instance, however the actual query is when do I’ve to make use of weak, unowned or robust? 🤔
I do not prefer to say “it relies upon”, so as an alternative, I might prefer to level you into the appropriate course. For those who take a more in-depth take a look at the official documentation about Closures, you may see what captures values:
- World capabilities are closures which have a reputation and don’t seize any values.
- Nested capabilities are closures which have a reputation and might seize values from their enclosing perform.
- Closure expressions are unnamed closures written in a light-weight syntax that may seize values from their surrounding context.
As you’ll be able to see international (static capabilities) do not increment reference counters. Nested capabilities however will seize values, identical factor applies to closure expressions and unnamed closures, however it is a bit extra sophisticated. I might prefer to advocate the next two articles to grasp extra about closures and capturing values:
Lengthy story quick, retain cycles suck, however in a lot of the instances you’ll be able to keep away from them simply through the use of simply the appropriate key phrase. Below the hood, ARC does an ideal job, besides just a few edge instances when it’s a must to break the cycle. Swift is a memory-safe programming language by design. The language ensures that each object will probably be initialized earlier than you could possibly use them, and objects residing within the reminiscence that are not referenced anymore will probably be deallocated robotically. Array indices are additionally checked for out-of-bounds errors. This provides us an additional layer of security, besides in case you write unsafe Swift code… 🤓
Anyway, in a nutshell, that is how the reminiscence structure seems to be like within the Swift programming language.