Home Behind the Curtain - How Swift Structs Are Stored in Memory
Post
Cancel

Behind the Curtain - How Swift Structs Are Stored in Memory

Background

Coder Orangu has always had the perception that structs are stored on the heap and are faster to access. This belief was strengthened by the buzzword that structs are value types.

One day, Coder Orangu went for an interview. During the discussion, the interviewer asked a seemingly simple question:

“How is the following struct stored in memory”?

1
2
3
4
 struct Person {
    var houseNumber: Int
    var name: String
 }

He started, “Since Person is a struct, memory for it is allocated on the stack. That’s why structs are faster — they don’t involve heap allocation like classes do.”

Then the interviewer pointed out, “But it has a String? Do you think that String also lives on the stack?”

Coder Orangu realised that his perception wasn’t quite right — it was time to revisit and truly understand how Swift structs are stored in memory.

Memory Layout Basics

After the interview, Coder Orangu decided to start from the basics. He wanted to truly understand what happens behind the curtain — how Swift manages memory for structs, and what “value type” really means.

Let’s explore step by step what happens under the hood for the simple struct with only primitive types:

1
2
3
4
 struct Person {
    var houseNumber: Int
    var isMale: Bool
 }

When you declare a Swift struct, the compiler generates several low-level data structures that the Swift runtime uses.
One of the most important is the instance memory layout, which defines how the struct’s stored properties are arranged in memory and how they connect with the type’s metadata. For a simple struct like Person, the compiler will generate an instance layout similar to this:

Components of the Memory Layout

    Kind:
Every Swift type at runtime has a field called Kind. This small identifier tells the runtime what kind of type it’s dealing with — a struct, a class, or an enum — and guides how the rest of the metadata should be interpreted.

    Size:
Size represents the overall memory required to store one instance of the struct. It includes the combined space for all stored properties as well as any padding bytes the compiler adds to maintain proper alignment.

    Alignment:
Alignment defines how each property inside a struct is placed in memory. This ensures that its properties are accessed efficiently and safely, following the CPU’s natural alignment rules. Misaligned data can cause slower access, or in some architectures, even result in memory access errors — so the Swift compiler automatically aligns each type to an appropriate boundary. The alignment of a struct is determined by the largest alignment requirement among all its properties.

In the Person struct, the Int property requires 8-byte alignment, which becomes the alignment for the entire struct. The compiler then adds 7 bytes of padding after the Bool property to maintain this 8-byte boundary. This ensures every property is stored and accessed in a way that matches the CPU’s preferred memory layout.

    Stride:
Stride defines how many bytes you need to move forward in memory to reach the next instance of the same type when stored contiguously, such as in an array. It always accounts for both the size of the struct and any extra padding required to maintain alignment between instances. You can think of it as the “distance” between two consecutive objects of the same type in memory.

    Value Witness Table (VWT):
The Value Witness Table defines how basic operations are performed on a value type at runtime. It tells the Swift runtime how to copy, destroy, assign, and initialise values.
The VWT also plays a key role in protocol conformance and generic functions. When a value type conforms to a protocol or is passed to a generic function, the compiler uses its Value Witness Table to invoke the correct operations dynamically — ensuring that every value behaves correctly according to its type.
However, the compiler may optimise away these dynamic calls when the concrete type is known at compile time, allowing operations like copy or destroy to be inlined directly. This means you get the flexibility of runtime abstraction without sacrificing performance for most use cases.

    Nominal Type Descriptor:
The Nominal Type Descriptor provides descriptive information about a type itself.
It stores metadata such as the type name, the number of fields, the names and offsets of those fields, and references to the metadata of each field’s type.
This allows the Swift runtime and tools like Mirror to perform reflection — dynamically inspecting the structure and properties of a type at runtime.
For example, in the Person struct, the Nominal Type Descriptor includes details such as the name "Person", the two fields houseNumber and isMale, and links to their respective metadata (Int and Bool).
These details form the foundation of how Swift understands and represents types beyond just their raw memory layout.

Conceptually, you can think of the compiler laying out the type’s metadata as if it were generating a C-style struct.
This isn’t the precise layout Swift uses internally, but it’s a helpful mental model to understand how Swift stores all the essential information about a type — its size, alignment, stride, and how instances are managed at runtime.

1
2
3
4
5
6
7
8
struct PersonTypeMetadata {
    int kind;                    // Identifies it as a struct
    int size;                    // Total memory required per instance
    int alignment;               // Required memory boundary
    int stride;                  // Distance between consecutive instances
    void* valueWitnessTable;     // How to copy, destroy, assign, etc.
    void* nominalTypeDescriptor; // Blueprint for building an instance
};

With this foundation, we’re ready to move to the next layer — the Nominal Type Descriptor — where Swift keeps track of what the object is and how it behaves, not just how it’s laid out in memory.

Nominal Type Descriptor

Another key data structure generated by the compiler is the Type Description. It acts as a blueprint, describing a type’s name, its stored properties, and their types.
Swift’s runtime uses this information to know how to create, organise, and understand an instance in memory.

Conceptually, the following structure represents how Swift’s compiler records essential information about a type — its name, stored properties, and the context in which it’s defined:

1
2
3
4
5
6
7
8
struct NominalTypeDescriptor {
    char* typeName             // Name of the type, e.g. "Person"
    int numberOfFields         // Number of stored properties
    void* fieldDescriptor      // Pointer to a FieldDescriptor describing each field
    void* parent               // Pointer to parent ContextDescriptor (e.g. module or enclosing type)
    int flags                  // Bitfield describing type characteristics
}

Major Components — Nominal Type Descriptor

    Type Name:
The name of the Swift type stored as a string (e.g., “Person”). Coder Orangu now understands that when the type’s name appears during debugging or reflection, it’s coming directly from this field.

    Number of Fields:
As the name suggests, this is an integer that indicates how many stored properties the type contains. For example, a struct like Person with two properties (houseNumber and isMale ) will have this value set to 2.

    Field Descriptor:
A reference to another piece of metadata, the FieldDescriptor, which holds detailed information about each stored property—its name, type, and field-level flags. This is where the runtime learns the fine details of every property within the type. The FieldDescriptor itself further contains individual FieldRecords, each representing a single stored property.

FieldDescriptorFieldRecords

    Parent (ContextDescriptor):
A pointer to the ContextDescriptor that shows which module or parent structure this belongs to. For instance, if Person is defined inside a module named Storage, this pointer connects the type’s metadata back to that module’s context.

    Flags:
Bit flags that hold extra information about the type for the runtime..

Reference Types In Struct

Coder Orangu now comes to the core problem — the point where he went wrong in the interview. He confidently explained that structs are value types, yet overlooked that a struct can also enclose reference types within it.

1
2
3
4
 struct Person {
    var houseNumber: Int
    var name: String
 }

At first glance, everything here looks like simple value types — Int and String. But Coder Orangu forgets that String is not a pure value type in Swift — It actually stores its content on the heap, even though the String variable itself lives on the stack.

In Swift, very short strings are stored directly inside the String struct itself — avoiding heap allocation.
This optimization is known as Small String Optimization (SSO).

Heap Behaviour: Struct Enclosing Reference

Coder Orangu is now curious about how Swift manages memory when a struct holds a custom reference type and is then copied to another variable.

1
2
3
4
5
6
7
8
9
10
11
12
class Address {
    var city: String
    init(city: String) { self.city = city }
}

struct Person {
    var name: String
    var address: Address
}

var personA = Person(name: "Saurav", address: Address(city: "Delhi"))
var personB = personA   // Struct copied

Here, the Address instance lives on the heap, and both personA and personB hold references to the same memory location. When the struct is copied, the compiler doesn’t duplicate the Address object — it simply increments the retain count of the shared instance, meaning both structs now reference the same heap object.
Any mutation to that shared object will therefore be visible across all references pointing to it.

When a struct contains multiple reference-type stored properties, Swift will perform a separate retain/release for each one whenever the struct is assigned or copied. So if your struct contains many reference-heavy fields, copying it frequently can become expensive:

When Coder Orangu writes the code below, Person contains two reference-type properties (address and job). So when personA is assigned to personB, the struct is copied, and the compiler inserts two retain calls—one for each reference stored inside the struct.

1
2
3
4
5
6
7
8
9
10
11
12
13
 class Address {
    var city: String
 }

 class Job {
    var title: String
 }

 struct Person {
    var houseNumber: Int        // Value type (no ARC)
    var address: Address        // Reference type (ARC applies)
    var job: Job                // Reference type (ARC applies)
 }
Coder Orangu Code Compiler (illustration only)
 let address = Address(city: "GK, Delhi")
 let job = Job(title: "Engineer")

 let personA = Person(houseNumber: 24,
                     address: address,
                     job: job)

 // Copying personA → personB (value copy)
 let personB = personA
 let address = Address(city: "GK, Delhi")
 let job = Job(title: "Engineer")

 let personA = Person(houseNumber: 24,
                     address: address,
                     job: job)

 // Copying personA → personB (value copy)
 let personB = personA
 retain(personB.address)   // reference counted individually
 retain(personB.job)       // each reference incurs ARC cost
  // Use PersonB
 release(personB.address)
 release(personB.job)

Structs are cheap when they store values. They become costly when they store multiple references. This is a real performance factor when choosing between struct and class.

Takeaway

Coder Orangu now understands that ‘value type’ does not mean ‘everything lives on the stack.’

  Structs with only value-type properties are copied directly, producing a new independent instance each time.
  Structs are going to be paying reference counting overhead proportional to the number of references that they contain.
  String is a special case: Swift uses Small String Optimisation (SSO) — short strings are stored directly inside the struct (no heap allocation), while longer strings are stored on the heap and managed via reference counting.
  String and collection types are value types, but real copying happens only on mutation.

Final Thought

Coder Orangu began with the common impression that “structs live on the stack and are always faster.” Now, he understands that choosing between struct and class is not about slogans or trends — it is about the nature of the data and how it behaves in memory.

One of the key criteria in choosing between them is whether the type holds mostly value-only data or multiple shared references. If most of the stored properties are simple values, a struct is efficient and predictable. But if the type contains several reference-type properties that would cause frequent retain/release operations when copied, a class may be a more suitable and performant choice.

This post is licensed under CC BY 4.0 by the author.

Swift Dispatch Demystified — What Really Happens Under the Hood.

-

Comments powered by Disqus.