Copy On Write and the Swift-CowBox Macro

Published: October 15, 2024
Written by:
Natan Rolnik
Natan Rolnik

You might have heard the concept of Copy On Write before, and asked yourself what it means. In a first moment it might sound cryptic, and even abstract and hard to visualize. In this post, you’ll understand what it means, how to implement Copy On Write in Swift, and also meet a great Swift macro that wraps the copy on write logic for your code.

But first, a quick recap on the difference between classes and structs to refresh our memories!

Reference Vs. Value Types

One of the biggest changes from the Objective C days to the Swift era is the extensive - and in many cases, recommended - use of structs over classes. While Objective-C could have C-based structs, they had many limitations, such as having a class inside a struct, and other memory-related differences. For that reason, even simple data structures would inherit from NSObject.

The biggest difference, in practice, is how these objects are shared throughout a process. Classes are reference types, so when passing an object around (NSString, UIImage or UIView), for example to another method, only a new pointer is created - and the memory address itself is shared between the two references. Changes performed on the copied reference will change the original object.

Structs, however, are value types and work in a distinct way, and are usually considered safer. Whenever a new struct is created, behind the scenes a new space in the memory is allocated. Any changes you perform to a second instance will not be applied to the first one, guaranteeing data isolation and favoring immutability.

This difference can be summarized in this brilliant and famous GIF:

Classes are passed as references, structs values are copied
Classes are passed as references, structs values are copied

Visualized in code:

class Person {
    var name: String
    // init(name: String) ...
}

let p1 = Person(
    name: "Richard"
)
let p2 = p1
p2.name = "Jian Yang"
print(p1.name)
// prints Jian Yang
struct Person {
    var name: String
}

let p1 = Person(
    name: "Richard"
)
var p2 = p1
p2.name = "Jian Yang"
print(p1.name)
// prints Richard

The Downside of Structs

Struct have many advantages: besides data isolation, structs have better performance in general, being faster to access. Additionally, as only the copy is being changed, and there’s no shared state between different threads, so they’re by nature safer, without requiring locks or other synchronization mechanisms.

Structs have many advantages, but also can be overused Structs have many advantages, but also can be overused
Structs have many advantages, but also can be overused

But using structs extensively might come with a price, especially on those with many properties. Think about the following code:

struct Coordinate {
    var latitude: Double
    var longitude: Double
}

let hackerHouse = Coordinate(latitude: 34.165, longitude: -118.568)
bookmarkLocation(hackerHouse)

When bookmarkLocation is called, what happens behind the scenes is that the coordinate is copied, and this copy is what the function actually receives. In contexts like this, it is totally fine, for two reasons:

  1. The struct itself is small. As Double takes 8 bytes (64 bits), the Coordinate struct memory size is 16 bytes
  2. The first instance is discarded right at the end, so there’s no real overhead

Large Structs

However, when having structs that and more complex and hold more data, having multiple copies can lead to higher memory usages.

Imagine a large struct, with 10 integers:

struct Startup {
    let employeesCount: Int
    let products: Int
    let seed: Int
    let roundA: Int
    let roundB: Int
    let roundC: Int
    let valuation: Int
    let revenue: Int
    let burnRate: Int
    let foundationYear: Int
}

Can you tell how much memory this struct takes? As mentioned above, an Int takes 8 bytes in a 64-bit platform. You can check this with the following code:

print(MemoryLayout<Int>.stride) // prints 8

As it has 10 properties, its size is 80 bytes. When copying this struct, the extra memory required will be another 80 bytes. If it would be a class, the extra memory would be 8 bytes - because that’s the size a pointer takes.

Now, imagine that instead of one copy, your application holds 1,000 copies. What’s the memory price you pay? It’s 1,000 x 80 bytes, which equals to 80kb. That’s not much in today’s devices. But do you know how much memory that would take would take with a class? It would be one instance (80 bytes), and one thousand pointers (1,000 x 8 bytes), totaling 8,080 bytes (around 8kb). That’s a difference of 10 times!.

We could continue with the math, but the point is clear: the more copies of a struct you have, and the larger it is, the higher is the memory consumption.

Copy On Write: the Best of Both Worlds

Now that you understand when the abuse of structs might harm the performance of your app, you might be asking yourself: is there any way to get the best of both worlds? Wouldn’t it be nice to have the performance of reference types, while keeping the safety of structs? Can we get struct-like behavior, without the performance penalty of copying large data structures?

You guessed it right - there is! And this is just where things are starting to get interesting! Wasn’t all this introduction worth a like in this video?

This pattern is called Copy On Write (or COW). What it means? As the name itself suggests, a copy only happens whenever there’s the need to write, to perform a change in the content - behaving as a value type. Otherwise, if the content is only shared but not modified, its reference is shared and it behaves like a class.

To enable this, each data structure must contain an internal data storage, it will either be shared or copied. More on that soon.

COW in Swift

To perform a copy on write in Swift, there’s a method that is essential to allow this logic: isKnownUniquelyReferenced. According to the docs in the method itself, in the standard library:

Returns a Boolean value indicating whether the given object is known to have a single strong reference. (…) useful for implementing the copy-on-write optimization for the deep storage of value types.

In libraries by Apple itself, such as SwiftNIO, this pattern is commonly used.

Implementing COW

And here’s how you could convert a struct from having the default value type behavior, to have COW in one of its properties. Its important to emphasize that COW optimizations aren’t valuable in short data structures - but in large ones, or more complex ones, such as arrays or dictionaries, they can be very valuable. This example is a simple one just to help with the explanation.

If we wanted to add support for Copy On Write to the person struct above, this is how it could be achieved:

struct Person {
    //1
    var name: String {
        get { storage.name }
        
        //2
        set {
            if isKnownUniquelyReferenced(&storage) == false {
                storage = storage.copy()
            }

            storage.name = newValue
        }
    }

    //3
    private var storage: Storage
    
    //4
    private class Storage {
        var name: String
        
        init(name: String) {
            self.name = name
        }
        
        //5
        func copy() -> Storage {
            Storage(name: name)
        }
    }    
}

This code is a bit long, but breaking it down makes it easier to understand it:

  1. What used to be a regular property, now has a getter and a setter. They both access the storage (more on that soon). The getter just returns the same property in the storage.
  2. The setter is slightly more elaborate. It uses the isKnownUniquelyReferenced method to check if there’s only a single strong reference to the storage. If not, meaning more than one copy of this struct relies on this storage, then copy the storage before modifying (therefore Copy On Write!).
  3. Declare a private property to hold the storage.
  4. Create a Storage box as a class (for having it as a reference type)
  5. Allow creating a copy of it, by returning a new Storage with the same name.

While this is a simplification example, this is more or less the same pattern used by Swift itself!

Now, to another problem of scalability: imagine having multiple properties in this structure. That would be a lot of getters, setters, and properties added to the storage! And imagine wanting to apply this pattern to multiple types. It would be a huge pain!

Luckily, someone thought about this previously and created a macro that does the heavy lifting for you!

The CowBox Macro

Macros exist to avoid writing boilerplate code, and this is exactly what Swift-CowBox, by Rick Van Voorden does: it helps having types that support Copy On Write, but without the overhead of writing them for every property in every type.

Imagine a struct that represents a person, holding their name and age:

struct Person {
    let name: String
    var age: String
}

To add Copy On Write support for it, the 3 macros that Swift-CowBox offers need to be used:

  • @CowBox: this is the macro that should be attached to the type itself, not its properties
  • @CowBoxNonMutating: because macros attached to properties require the property to be var instead of let, immutability is enforced at the macro level
  • @CowBoxMutating: this is the macro used on mutable properties

Applying these 3 items to the person struct, we have this:

import CowBox

@CowBox struct Person {
    @CowBoxNonMutating var name: String
    @CowBoxMutating var age: Int
}

And the compiler will do its magic. When clicking on Expand Macro on the CowBox macro, this is what Xcode shows us:

Notice how the macro automatically adds the private _Storage, its initializer and the copy method. When it is used? Clicking on Expand Macro on the property macros will reveal:

Pay attention to how the two properties are different: the immutable name has only a getter, while the mutable age has a setter, that calls the isKnownUniquelyReferenced method described earlier. And notice how both are only wrappers that access the storage.

With a single keyword in the type and in the properties, your models can get COW for free. How cool is that?!

Protocol Implementations

A very common use case is to add protocol conformances to models. Some of the most common ones are Equatable, Hashable, and of course Codable.

Swift-CowBox supports them out of the box!

When adding Equatable to the Person struct, the macro automatically implements the method:

Equatable implementation by SwiftCowBox Equatable implementation by SwiftCowBox
Equatable implementation by SwiftCowBox

An extra feature that worth highlighting, in this Equatable implementation, is the optimization to check for equality by identity before equality by value. This can have big performance savings in runtime!

Automatic implementation of a protocol also happens when adding Hashable and Codable:

Hashable is also supported Hashable is also supported
Hashable is also supported
And our dear Codable as well! And our dear Codable as well!
And our dear Codable as well!

Benchmark & Considerations

How does Swift-CowBox perform in a real world scenario, and what impact does it have?

In case you want to check out an in depth benchmark of how Swift-CowBox performs, you can take a look at its benchmark repository. It is Apple’s FoodTruck sample code, with and without the macro applied on the structs. There, you can find measurements and comparisons for memory allocation, Core Animation commits, and UI hangs.

Although you can apply COW on every property, it doesn’t mean you have to: a struct might have regular properties alongside with properties with COW. It is totally fine to keep one property “out” of copy on write for performance reasons, as seen in this SwiftNIO struct.

Finally, there might be perfomance issues in some situations. One case could happen with nested types with Copy On Write. A more complicated one is having COW on a collection, which might lead to accidental quadratic behavior: when modifying a collection inside a loop, and each modification triggers a copy due to COW, you can accidentally end up with a performance issue. More on that in the links in the Explore Further section below.

Explore Further

This topic is not a simple one, so congrats if you made it here and learned a new concept!

Copy On Write can evolve to more advanced areas, and in case you’re interest in learning more, here are some useful links. Special thanks to Rick Van Voorden, who not only wrote this helpful macro, but also helped reviewing this article and suggesting the links below.

If you liked this article and have comments or suggestions, don’t hesitate to ping SwiftToolkit at X or Mastodon.

See you at the next post. Have a good one!
Swift, Xcode, the Swift Package Manager, iOS, macOS, watchOS and Mac are trademarks of Apple Inc., registered in the U.S. and other countries.

© Swift Toolkit, 2024-2025