Swift By Rahul

Understanding Copy-on-Write in Swift

Swift is a language that strongly encourages the use of value types, such as structs and enums. They offer significant benefits like predictable behavior, thread safety, and easier reasoning about your code. However, working with large value types can sometimes introduce performance overhead, especially when they are copied. This is where Copy-on-Write (CoW) comes to the rescue.

CoW is a clever optimization technique that Swift uses extensively for its built-in collection types (Array, Dictionary, Set) and String. It allows these value types to behave like reference types when they are merely copied, only performing a true, deep copy of their underlying storage when a modification is actually made. This article will dive deep into how CoW works, why it's beneficial, and how you can implement it for your own custom types.

The Performance Challenge with Large Value Types

Imagine you have a struct that encapsulates a significant amount of data, perhaps a large image buffer or a complex data model.

struct ImageData {
    var pixels: [UInt8] // Imagine this array holds millions of bytes
    var width: Int
    var height: Int

    init(width: Int, height: Int) {
        self.width = width
        self.height = height
        self.pixels = Array(repeating: 0, count: width * height * 4) // 4 bytes per pixel (RGBA)
    }

    mutating func invertColors() {
        for i in 0..<pixels.count {
            pixels[i] = 255 - pixels[i]
        }
    }
}

// Create a large image data instance
var originalImage = ImageData(width: 1920, height: 1080)

Now, consider what happens when you "copy" this originalImage:

var copiedImage = originalImage // This assignment performs a full copy

Because ImageData is a struct (a value type), Swift's default behavior is to make a complete, member-wise copy of originalImage into copiedImage. If pixels contains millions of bytes, this means allocating new memory and copying all those bytes, even if copiedImage is never modified. This can be a significant performance hit if such copies happen frequently, leading to unnecessary memory allocations and CPU cycles.

This is the problem Copy-on-Write aims to solve: delaying the expensive copy operation until it's absolutely necessary.

Comparison of Value Type Copying without and with Copy-on-Write Value Type Copying: Without CoW vs. With CoW Without Copy-on-Write var A Data Block 1 var B = A Data Block 2 Deep Copy! With Copy-on-Write var A Data Block X var B = A Shared Ref (Copy only on modification)

How Copy-on-Write Works in Swift

Swift's CoW implementation leverages the distinction between value types (structs, enums) and reference types (classes). While your Array, Dictionary, Set, or String are value types, their internal storage is often managed by a private, heap-allocated class instance (a reference type).

Here's the mechanism:

  1. Initial Assignment (Copying): When you assign one CoW-enabled value type instance to another (e.g., var newArray = originalArray), Swift does not immediately create a deep copy of the underlying data. Instead, both originalArray and newArray now internally point to the same class instance that holds the actual data. Swift's runtime keeps track of how many references point to this shared storage.
  1. Modification (Copy Trigger): If you then try to modify either originalArray or newArray, Swift performs a check. It asks: "Is this internal storage currently referenced by more than one variable?"
    • If Yes (Not Uniquely Referenced): A new, independent copy of the underlying storage is created. The variable attempting the modification is then updated to point to this new copy. The modification is applied to this new copy, leaving the original shared storage untouched for the other variable(s). This is the "copy-on-write" step.
    • If No (Uniquely Referenced): If the internal storage is only referenced by the variable attempting the modification, no copy is needed. The modification is applied directly to the existing storage. This is an optimization that avoids unnecessary copying.

This strategy means that the expensive deep copy operation only occurs when it's truly needed – when a value type instance is modified while it's still being shared. If you copy a value type and never modify it, or if you modify it but it's not shared, you pay no performance penalty for the copy.

Illustrating CoW with Swift's Built-in Types

Let's use Array to see CoW in action. While we can't directly observe the internal storage, we can infer its behavior.

var numbers = [1, 2, 3, 4, 5]
print("Original numbers: \(numbers)") // Output: Original numbers: [1, 2, 3, 4, 5]

// 'copiedNumbers' now shares the same internal storage as 'numbers'
// No deep copy of the elements has occurred yet.
var copiedNumbers = numbers
print("Copied numbers (before modification): \(copiedNumbers)") // Output: Copied numbers (before modification): [1, 2, 3, 4, 5]

// Now, modify 'copiedNumbers'.
// At this point, Swift detects that 'numbers' also refers to the same storage.
// Therefore, a *copy* of the underlying storage is made for 'copiedNumbers'.
// The modification then applies to this new, independent storage.
copiedNumbers[0] = 100
print("Copied numbers (after modification): \(copiedNumbers)") // Output: Copied numbers (after modification): [100, 2, 3, 4, 5]

// 'numbers' remains unchanged because 'copiedNumbers' now has its own copy.
print("Original numbers (after copiedNumbers modification): \(numbers)") // Output: Original numbers (after copiedNumbers modification): [1, 2, 3, 4, 5]

If we were to modify numbers after copiedNumbers was modified, numbers would also trigger a copy if it was still sharing. But in this case, copiedNumbers already has its own copy, so numbers remains uniquely referenced to its original copy.

Initial State:
┌───────────┐    ┌─────────────┐
│  numbers  │───▶│ [1, 2, 3, 4, 5] │
└───────────┘    └─────────────┘

After `copiedNumbers = numbers`:
┌───────────┐    ┌─────────────┐
│  numbers  │───▶│ [1, 2, 3, 4, 5] │
├───────────┤    ▲─────────────┘
│copiedNumbers│───┘
└───────────┘
  (Shared Storage)

After `copiedNumbers[0] = 100` (CoW triggered):
┌───────────┐    ┌─────────────┐
│  numbers  │───▶│ [1, 2, 3, 4, 5] │
└───────────┘    └─────────────┘

┌───────────┐    ┌─────────────┐
│copiedNumbers│───▶│ [100, 2, 3, 4, 5] │
└───────────┘    └─────────────┘
  (Separate Storage)

This ASCII diagram clearly illustrates how the shared storage is broken when a modification occurs, ensuring value semantics are preserved without unnecessary early copying.

Performance Implications of Copy-on-Write

CoW is a powerful optimization, but it's important to understand its performance characteristics:

  • Benefit: Reduced memory allocation and copying overhead. If you copy a large collection and never modify it, or modify it only when it's not shared, you get the performance of reference semantics (just copying a pointer) while retaining the safety and predictability of value semantics.
  • Cost:
    • Reference Counting Overhead: Swift's runtime needs to track the number of references to the internal storage. This adds a small overhead to each assignment and deallocation.
    • Uniqueness Check Overhead: Before modification, a check (isKnownUniquelyReferenced) is performed.
    • Copy Penalty: If a modification does trigger a copy, you pay the full price of a deep copy at that moment. This can be less predictable than always copying immediately.

For most common use cases with Array, Dictionary, Set, and String, the benefits of CoW far outweigh these costs. Swift's standard library implementations are highly optimized.

Implementing Custom Copy-on-Write for Your Own Types

While Swift provides CoW for its built-in collections, you might have custom value types that manage large amounts of data and could benefit from this optimization. For instance, our ImageData struct from earlier.

To implement custom CoW, you'll typically follow this pattern:

  1. Create a Reference Type (Class) for Storage: This class will hold the actual mutable data.
  2. Create a Value Type (Struct) as the Public API: This struct will wrap an instance of your storage class.
  3. Use isKnownUniquelyReferenced(_:): Inside the struct's mutating methods or computed properties, check if the wrapped storage class instance is uniquely referenced. If not, create a new copy of the storage before applying changes.

Let's refactor our ImageData struct to use CoW:

// 1. Reference Type for Storage
final class ImageDataStorage {
    var pixels: [UInt8]
    let width: Int
    let height: Int

    init(width: Int, height: Int) {
        self.width = width
        self.height = height
        self.pixels = Array(repeating: 0, count: width * height * 4)
    }

    // A convenience initializer for copying existing storage
    init(copying storage: ImageDataStorage) {
        self.width = storage.width
        self.height = storage.height
        self.pixels = storage.pixels // Deep copy of the array elements
    }
}

// 2. Value Type as the Public API
struct CoWImageData {
    private var storage: ImageDataStorage // Holds a reference to the storage class

    init(width: Int, height: Int) {
        self.storage = ImageDataStorage(width: width, height: height)
    }

    // Private method to ensure unique storage before modification
    private mutating func ensureUniqueStorage() {
        if !isKnownUniquelyReferenced(&storage) {
            // If not unique, create a new copy of the storage
            storage = ImageDataStorage(copying: storage)
            print("--- CoW triggered: Made a new copy of ImageDataStorage ---")
        } else {
            print("--- Storage is unique: Modifying in place ---")
        }
    }

    // Public properties and methods that trigger CoW on write
    var width: Int { storage.width }
    var height: Int { storage.height }

    subscript(index: Int) -> UInt8 {
        get {
            precondition(index >= 0 && index < storage.pixels.count, "Index out of bounds")
            return storage.pixels[index]
        }
        set {
            precondition(index >= 0 && index < storage.pixels.count, "Index out of bounds")
            ensureUniqueStorage() // Check for uniqueness before modification
            storage.pixels[index] = newValue
        }
    }

    mutating func invertColors() {
        ensureUniqueStorage() // Check for uniqueness before modification
        for i in 0..<storage.pixels.count {
            storage.pixels[i] = 255 - storage.pixels[i]
        }
    }
}

Let's test our custom CoWImageData:

print("--- Creating original image ---")
var originalCoWImage = CoWImageData(width: 10, height: 10) // Small for demo
originalCoWImage[0] = 50 // Storage is unique, modify in place
print("OriginalCoWImage[0]: \(originalCoWImage[0])")

print("\n--- Copying original image ---")
var copiedCoWImage = originalCoWImage // Shares storage with originalCoWImage
print("CopiedCoWImage[0] (before modification): \(copiedCoWImage[0])")

print("\n--- Modifying copied image ---")
// This will trigger CoW because 'originalCoWImage' also points to the same storage
copiedCoWImage[0] = 150
print("CopiedCoWImage[0] (after modification): \(copiedCoWImage[0])")
print("OriginalCoWImage[0] (after copiedCoWImage modification): \(originalCoWImage[0])") // Remains 50

print("\n--- Modifying original image (now unique) ---")
originalCoWImage[1] = 200 // Storage is now unique, modify in place
print("OriginalCoWImage[1]: \(originalCoWImage[1])")

Output:

--- Creating original image ---
--- Storage is unique: Modifying in place ---
OriginalCoWImage[0]: 50

--- Copying original image ---
CopiedCoWImage[0] (before modification): 50

--- Modifying copied image ---
--- CoW triggered: Made a new copy of ImageDataStorage ---
CopiedCoWImage[0] (after modification): 150
OriginalCoWImage[0] (after copiedCoWImage modification): 50

--- Modifying original image (now unique) ---
--- Storage is unique: Modifying in place ---
OriginalCoWImage[1]: 200

This output clearly demonstrates ensureUniqueStorage() and isKnownUniquelyReferenced at work, triggering a copy only when necessary.

Custom Copy-on-Write Implementation Flow Diagram Custom Copy-on-Write Flow MyCoWStruct (Value Type) StorageClass (Reference Type) Holds reference to Attempt to Modify `isKnownUniquelyReferenced`? YES Modify Storage Directly NO Create New Storage Copy & Modify

When to Use (and Not Use) Custom CoW

Implementing custom CoW adds complexity to your code, so it's not a universal solution.

You should consider implementing custom CoW when:

  • Your value type wraps a large amount of data: The cost of copying this data directly is significant.
  • Copies of your value type are frequent: If your type is passed around, assigned, or stored in collections often.
  • Modifications after copying are relatively infrequent: The CoW mechanism shines when copies are common, but modifications are rare or happen only when the instance is no longer shared.
  • You need strict value semantics: You want your type to behave like a struct, where each instance is independent, but you need the performance optimization of deferred copying.

Avoid implementing custom CoW when:

  • Your value type is small and cheap to copy: The overhead of isKnownUniquelyReferenced and the indirection through a class might outweigh the benefits.
  • Modifications after copying are always expected: If every copy of your type is immediately modified, you'll always incur the copy cost, plus the CoW overhead. In such cases, a simple struct or even a class might be more straightforward.
  • You actually desire reference semantics: If multiple variables should refer to the same mutable data and see each other's changes, then a class is the appropriate choice.

Summary

Copy-on-Write is a fundamental optimization in Swift that allows value types like Array, Dictionary, Set, and String to offer excellent performance for large data structures while maintaining their predictable value semantics. By delaying expensive deep copy operations until a modification occurs on a shared instance, CoW strikes a balance between performance and correctness.

For your own custom large value types, you can implement CoW using a private class for storage and the isKnownUniquelyReferenced(_:) function. However, always weigh the benefits against the added complexity and overhead to ensure it's the right solution for your specific performance needs.

Happy Swifting!