MemoryLayout
Size & Stride
A quick word of warning to those of you using UnsafePointer
and MemoryLayout
.
- The
size
of a type tells you how many bytes it takes to hold that type in memory. - The
stride
of a type tells you how far apart each instance of the type is in memory.
If you are doing pointer arithmetic on UnsafePointer
then Swift handles this for you.
If you are doing pointer arithmetic on UnsafeRawPointer
then make sure you step around in memory by the stride but read and write the size.
This isn't theoretical. There are two cases I know of where the size and stride vary (using x86-64 ABI rules).
Struct layout
struct Heh {
var x: UInt32
var y: UInt8
}
MemoryLayout<Heh>.size // = 5
MemoryLayout<Heh>.stride // = 8
Swift currently lays out structs in the order they are defined in the file, though as far as I know that's just an implementation detail and not a promise.
Because the struct has a 4-byte field followed by a 1-byte field, the struct ends up taking up 5 bytes of storage with three padding bytes for alignment. You couldn't place instances of this struct one after the other because the x
of the following struct would be incorrectly aligned (it wouldn't be on a 4-byte boundary).
struct OhMy {
var heh: Heh
var a: UInt8
var b: UInt16
}
MemoryLayout<OhMy>.size // == 8
MemoryLayout<OhMy>.stride // == 8
Here we can see that Swift has stuffed the storage for our extra fields into what would have been the padding for the inner struct. As long as you respect the size and stride for the inner struct you won't accidentally stomp on the fields you didn't realize were there.
Zero size types
MemoryLayout<()>.size // == 0
MemoryLayout<()>.stride // == 1
enum Never { }
MemoryLayout<Never>.size // == 0
MemoryLayout<Never>.stride // == 1
This might surprise you: the size of some types can be zero but the stride will always be at least 1 byte. Having types take up at least 1 byte means you don't need a bunch of special casing everywhere you deal with memory to avoid calling malloc(0)
. This does mean an array of 100 zero size types allocates 100 bytes that are never read or written.
struct X {
var a: ()
var b: ()
var c: ()
var d: ()
var e: ()
}
MemoryLayout<X>.size // == 0
MemoryLayout<X>.stride // == 1
And here is one of the benefits of logically separating size and stride: A struct containing a bunch of zero-sized types can collapse all zero-size representations. Adding a single UInt8
field would increase size to 1 byte while leaving stride at 1.
This blog represents my own personal opinion and is not endorsed by my employer.