Swift-CowBox-Sample: Measuring the Performance of Copy-on-Write Semantics for SwiftUI

Swift-CowBox-Sample

Swift-CowBox[1] is a simple set of Swift Macros for adding easy copy-on-write semantics to Swift Structs.

Let’s see the macro in action. Suppose we define a simple Swift Struct:

public struct Person {
  public let id: String
  public var name: String
}

This struct is a Person with two stored variables: a non-mutable id and a mutable name. Let’s see how we can use the CowBox macros to give this struct copy-on-write semantics:

import CowBox

@CowBox public struct Person {
  @CowBoxNonMutating public var id: String
  @CowBoxMutating public var name: String
}

Our CowBoxNonMutating macro attaches to a stored property to indicate we synthesize a getter (we must transform the let to var before attaching an accessor). We use CowBoxMutating to indicate we synthesize a getter and a setter. Let’s expand this macro to see the code that is generated for us:

public struct Person {
  public var id: String {
    get {
      self._storage.id
    }
  }
  public var name: String {
    get {
      self._storage.name
    }
    set {
      if Swift.isKnownUniquelyReferenced(&self._storage) == false {
        self._storage = self._storage.copy()
      }
      self._storage.name = newValue
    }
  }
  
  private final class _Storage: @unchecked Sendable {
    let id: String
    var name: String
    init(id: String, name: String) {
      self.id = id
      self.name = name
    }
    func copy() -> _Storage {
      _Storage(id: self.id, name: self.name)
    }
  }
  
  private var _storage: _Storage
  
  public init(id: String, name: String) {
    self._storage = _Storage(id: id, name: name)
  }
}

extension Person: CowBox {
  public func isIdentical(to other: Person) -> Bool {
    self._storage === other._storage
  }
}

All of this boilerplate to manage and access the underlying storage object reference is provided by the macro. The macro also provides a memberwise initializer. An isIdentical function is provided for quickly confirming two struct values point to the same storage object reference.

The Swift-CowBox repo comes with a set of benchmarks for examples of how copy-on-write semantics can reduce CPU and memory usage at scale. Those benchmarks tell us one side of the story: measuring performance independent of any user interface. Many of us are going to be building (and maintaining) complex apps with complex views. How would adopting Swift-CowBox affect performance in apps built for SwiftUI?

Food Truck

Our experiments will begin with sample-food-truck[2] from Apple. This project has two important details that make it a good choice for us to use for benchmarking Swift-CowBox: the sample-food-truck app is built from SwiftUI, and the underlying data models are built on value types (as opposed to an object-oriented solution like Core Data or SwiftData).

To get started, feel free to clone the original repo and build the app locally. You can try as many platforms as you like, but we will focus on macOS for our analysis. Here is what the app looks like built for macOS:

We will spend most of our time investigating the OrdersTable. Here is what that component looks like:

The OrdersTable[3] is a SwiftUI component that reads (and displays) data from a FoodTruckModel[4] object instance. The FoodTruckModel object instance manages an Array of Order[5] value types. Our sample app from Apple launches with 24 Order instances generated from a OrderGenerator[6]. We will increase this by three orders of magnitude and measure performance as we migrate our Order struct to copy-on-write semantics.

Feel free to look around this code and investigate how things are currently architected before moving forward. We will be hacking on the sample project from Apple to collect our measurements. You can choose to follow along by hacking on the Apple repo, or you can clone the Swift-CowBox-Sample fork to see the complete project with our changes already implemented.

When choosing whether or not to adopt CowBox in a project, start by looking for data structures that would produce a measurable performance benefit from migrating to copy-on-write. Our Order value-type is complex (much more data than is needed by one pointer) and is copied many times over an app lifecycle. Instead of migrating several data structures to copy-on-write semantics all at once, let’s start just with Order (and measure our performance against our baseline).

Data

Please reference the Swift-CowBox-Sample-Data[7] repo to see the complete benchmark results (including traces from Instruments) that were collected. All measurements were taken from a MacBook Pro with Apple M2 Max and 96 GB of memory running macOS 14.4.1 and Xcode 15.3.

Conclusions

We began with a sample app from Apple built on immutable value-types. We defined a set of benchmarks that could be measured from the command-line. We also ran our app in Instruments to measure performance over the app lifecycle.

Once we measured our baseline measurements, we migrated one immutable value-type to copy-on-write semantics with the CowBox macro. We measured performance improvements: our app ran faster and used less memory.

We refactored our view component to memoize a sorted array. We confirmed that this improved performance. Pairing this refactoring in our view component layer with the refactoring in our data layer gave us the best results.

Next Steps

The Swift-CowBox macro makes it easy to add copy-on-write semantics to structs. We saw how this migration can improve performance in the Food Truck sample app from Apple. Should you migrate your own structs to Swift-CowBox? It depends.

The most impactful structs to migrate to copy-on-write semantics would be structs that are complex (a lot of data) that you expect to copy many times over the course of an app lifecycle.

Before you attempt to add Swift-CowBox, start with baseline benchmark measurements:

  • Control:
    • Confirm the memory footprint of one struct instance.
    • Define (and run) some benchmarks against the data models of your app from a command-line utility like Benchmark from Ordo One.
    • Run Instruments like os_signpost, Hangs, Core Animation Commits, and Allocations over an app lifecycle.
  • Test:
    • Migrate one data model to copy-on-write semantics with Swift-CowBox.
    • Repeat the steps from Control and compare these measurements with your baseline.

Your “control” group would be your app built from your original struct data model. Your “test” group would be your app built with your new Swift-CowBox data model.

Use your best judgement. If migrating to Swift-CowBox significantly increases app launch time, the performance improvements over the course of your app lifecycle might not be worth it. Someone else can’t make that decision for you; you will have the most context and insight when it comes to what should produce the best user experience for your own customers.

Please file a GitHub issue for any new issues or limitations you encounter when using Swift-CowBox.

Thanks!


  1. GitHub - Swift-CowBox/Swift-CowBox: Easy Copy-on-Write Semantics for Structs. ↩︎

  2. GitHub - apple/sample-food-truck: SwiftUI sample code from WWDC22 ↩︎

  3. sample-food-truck/App/Orders/OrdersTable.swift at main · apple/sample-food-truck · GitHub ↩︎

  4. sample-food-truck/FoodTruckKit/Sources/Model/FoodTruckModel.swift at main · apple/sample-food-truck · GitHub ↩︎

  5. sample-food-truck/FoodTruckKit/Sources/Order/Order.swift at main · apple/sample-food-truck · GitHub ↩︎

  6. sample-food-truck/FoodTruckKit/Sources/Order/OrderGenerator.swift at main · apple/sample-food-truck · GitHub ↩︎

  7. GitHub - Swift-CowBox/Swift-CowBox-Sample-Data ↩︎

3 Likes