How does struct memory layout work in Swift?

Explain `MemoryLayout`, padding, alignment, and stride; show why stored-property order changes a struct's footprint; and connect it to arrays, performance, and low-level interop

Answer

The core idea

Struct memory layout is the compiler's in-memory arrangement of a struct's stored properties. In Swift, the main tools for reasoning about it are MemoryLayout's three numbers:

  • size: the contiguous bytes occupied by the value itself
  • alignment: the byte boundary the value must start on
  • stride: the distance from one value to the next in contiguous storage such as an Array

The short interview answer is: Swift lays out a struct by placing stored properties in declaration order, inserting padding when a property needs stricter alignment, and then rounding the final footprint up to the struct's alignment.

One subtle but important point: MemoryLayout describes the inline representation of the value. For types like String or Array, that does not include any extra heap storage the value may reference.

1. What size, stride, and alignment mean

This first example uses only fixed-width types so the numbers stay stable across common 64-bit platforms:

swift
struct Point {
    let x: Double
    let y: Double
    let isFilled: Bool
}

print("size:", MemoryLayout<Point>.size)
print("stride:", MemoryLayout<Point>.stride)
print("alignment:", MemoryLayout<Point>.alignment)

Run compiles and executes on the server; output shows below.

This prints:

  • size: 17 because the stored properties use 8 + 8 + 1 bytes
  • alignment: 8 because Double needs 8-byte alignment
  • stride: 24 because arrays and other contiguous storage must leave enough room so the next Point also starts on an 8-byte boundary

That last number is the one candidates often miss. If you store four Point values back-to-back, you do not need 4 * 17 bytes. You need 4 * 24.

Reference table:

TypeSizeStrideAlignment
Bool111
Int8 / UInt8111
Int16 / UInt16222
Int32 / UInt32444
Int64 / UInt64888
Int / UInt8 on 64-bit8 on 64-bit8 on 64-bit
Float444
Double888
Point17248

On 32-bit platforms, Int and UInt are 4 / 4 / 4 instead.

2. How Swift decides the layout

Swift currently lays out stored properties in declaration order. As it walks the fields, it inserts padding whenever the next property would otherwise land at an address that violates that property's alignment requirement.

This means the order of the same fields can change the final footprint:

swift
struct EfficientHeader {
    let messageID: Int64
    let retryCount: Int32
    let isCompressed: Bool
    let isEncrypted: Bool
}

struct PaddedHeader {
    let isCompressed: Bool
    let messageID: Int64
    let retryCount: Int32
    let isEncrypted: Bool
}

func printLayout<T>(_ type: T.Type, name: String) {
    print(name)
    print("  size:", MemoryLayout<T>.size)
    print("  stride:", MemoryLayout<T>.stride)
    print("  alignment:", MemoryLayout<T>.alignment)
}

printLayout(EfficientHeader.self, name: "EfficientHeader")
print("  offsets:",
      MemoryLayout<EfficientHeader>.offset(of: \.messageID)!,
      MemoryLayout<EfficientHeader>.offset(of: \.retryCount)!,
      MemoryLayout<EfficientHeader>.offset(of: \.isCompressed)!,
      MemoryLayout<EfficientHeader>.offset(of: \.isEncrypted)!)

printLayout(PaddedHeader.self, name: "PaddedHeader")
print("  offsets:",
      MemoryLayout<PaddedHeader>.offset(of: \.isCompressed)!,
      MemoryLayout<PaddedHeader>.offset(of: \.messageID)!,
      MemoryLayout<PaddedHeader>.offset(of: \.retryCount)!,
      MemoryLayout<PaddedHeader>.offset(of: \.isEncrypted)!)

Run compiles and executes on the server; output shows below.

On a typical 64-bit build, the important difference is:

  • EfficientHeader has size == 14 and stride == 16
  • PaddedHeader has size == 21 and stride == 24

Why? messageID needs 8-byte alignment. In PaddedHeader, the leading Bool uses only 1 byte, so Swift must insert 7 padding bytes before messageID can start at offset 8. That pushes the rest of the layout out, and the final stride becomes 24 instead of 16.

That is the interview takeaway: put larger-alignment fields first and smaller fields later when you care about packing.

3. Where this is applied in real code

The most useful places to care are:

  1. Large collections of value types. Arrays of structs use stride, so wasted padding multiplies quickly when the app holds many values in memory.
  2. Copy-heavy value types. Larger structs cost more to copy, move, and compare. Trailing padding is usually less harmful than internal padding because internal holes may still travel with bulk copies.
  3. Unsafe APIs and C interop. If code allocates raw memory, binds pointers, or maps C structs, size, stride, alignment, and field offsets become correctness issues, not just optimization details.

This is much less important for small, infrequently created app models. A struct that exists in tiny numbers usually should stay readable unless profiling shows a real problem.

4. Common pitfalls

size is not the same as array footprint

When allocating space for multiple values or reasoning about contiguous memory, use stride, not size. Apple's MemoryLayout documentation calls this out explicitly: arrays store each next element stride bytes after the previous one, not size bytes after it.

Bool is a byte, not a bit

Swift does not generally bit-pack multiple Bool stored properties inside a struct. Several flags can still add up, and they can also trigger padding when mixed with larger aligned fields.

Int is platform-dependent

Int follows the machine word size, so it is 8 bytes on 64-bit platforms and 4 bytes on 32-bit platforms. For interview examples, Int64, Int32, and UInt8 are easier because they make the math explicit.

Interview angle

A strong answer sounds like this: MemoryLayout tells you a type's size, stride, and alignment. For structs, Swift currently walks stored properties in declaration order, adds padding when a property's alignment requires it, and rounds the final result up to the struct's alignment. That means field order can change the stride, which is why arrays of poorly ordered structs can waste memory. Optimize it when the type is stored in large numbers or used with unsafe memory, and use the official MemoryLayout docs plus Wade Tregaskis's A brief introduction to type memory layout in Swift as the canonical references.