Copy-on-write types (and SwiftUI)

Hello,

I am implementing a copy-on-write type and using it in SwiftUI. I was hoping to have a single instance so that expensive copy is minimized. In the below example, the expensive copy and deinit occur every time a tap occurs. Is this a limitation of SwiftUI or is there an error or better way to implement my COW type? Thank you for any insights or hints you might have! :slight_smile:

import SwiftUI

struct CowType {

  init() {
    self.detail = Detail(value: 0)
  }

  private var detail: Detail

  var value: Int { detail.value }

  mutating func setValue(_ value: Int) {
    if isKnownUniquelyReferenced(&detail) {
      detail.value = value
    } else {
      print("performing expensive copy")
      detail = Detail(value: value)
    }
  }

  private final class Detail {
    init(value: Int) {
      self.value = value
    }

    deinit {
       print("calling deinit")
    }

    var value: Int
  }
}

struct ContentView: View {

  @State var state = CowType()

  var body: some View {
    Text("Hello, world! \(state.value)")
      .padding()
      .onTapGesture {
        state.setValue(Int.random(in: 1...100))
      }
  }
}

struct ContentView_Previews: PreviewProvider {
  static var previews: some View {
    ContentView()
  }
}

@State var state: [Int] does not appear to have an exponential behavior problem, so it must be doing something fancy for CoW? :thinking:

This runs without any slowdown:

struct ContentView: View {

  @State var state = Array(repeating: 0, count: 1000000)

  var body: some View {
    Text("Hello, world! \(state.count)")
      .padding()
      .gesture(DragGesture().onChanged { _ in
        state.append(Int.random(in: 1...100))
      })
  }
}

If I add a few more zeros, it slows way down. So maybe they do, in fact, have the same behavior.

Answering my own question here, it does seem to be a SwiftUI thing.

I found that I can avoid expensive copies if I wrap my CoW type in an observable object and forward the methods to that:

final class Container: ObservableObject {
  private var cow = CowType()
  var values: [Int] { cow.values }
  func append(_ value: Int) {
    objectWillChange.send()
    cow.append(value)
  }
}

Interestingly, you still get the expensive copy on every append if you use the published property attribute.

final class Container: ObservableObject {
  @Published var cow = CowType()
}

Have you tried making your CoW type Equatable? Does that make a difference?

1 Like

@State is a struct, so I think this should create copy of the state content in the closure:

      .onTapGesture {
        state.setValue(Int.random(in: 1...100))
      }

The function isKnownUniquelyReferenced is available for classes also, so using the class instead of the struct may solve this problem.

class CowType {

  init() {
    self.detail = Detail(value: 0)
  }

  private var detail: Detail

  var value: Int { detail.value }

  func setValue(_ value: Int) {
    if isKnownUniquelyReferenced(&detail) {
      detail.value = value
    } else {
      print("performing expensive copy")
      detail = Detail(value: value)
    }
  }

  private final class Detail {
    init(value: Int) {
      self.value = value
    }

    deinit {
       print("calling deinit")
    }

    var value: Int
  }
}

Thanks for your insights, Nikita.

I want a type that has value semantics but dynamic internal storage. Much like a Swift standard container type. Conceptually, the UI has only one of these objects and the copy operation is expensive so I want to avoid the copy as much as possible. In a unit test if I put the mutation in a loop, no problem, one copy is made and it is mutated in place.

In SwiftUI, it happens for every single drag event. Internally, SwiftUI must be doing something like this:

state.insert(...)
// becomes
var secretCopy = state
secretCopy.insert(....)

My solution seems to work although it requires an extra level of wrapping. (I don't want my CowType to know anything about SwiftUI.)

It is an interesting thought, thank you. If I make CowType a class, SwiftUI doesn't get message saying that the state has changed and doesn't re-render the body.

On second inspection, it doesn't fix the issue. (I had removed the print statement from my code.). Thank you anyway for the idea.

That is not how CoW is meant to work. The original example is the right way to implement it.

I only quickly scanned the thread and the issue is that State is probably capturing a reference to the cached storage which essentially makes the private reference to Detail no longer unique, which then would cause a copy on every tap.

Donβ€˜t do that, State is not meant to be used with classes, this breaks the contract with the API and you will run into undefined behaviors.

An alternative, non-Cow approach would be by creating a class which conforms to ObservableObject and use StateObject instead.

2 Likes

I'm also afraid State won't help you here. To avoid the expensive copy you'd need either a reference type using StateObject/ObservedObject or – for value semantics – a persistent data structure with inexpensive copies.

The behaviour you're observing has to do with State<CowType> storing a copy of the last rendered CowType value in its value type layout, as you can tell by adding dump(_state) in your code:

      .onTapGesture {
        let value = Int.random(in: 1...100)
        print("changing value from \(state.value) to \(value)")
        state.setValue(value)
        dump(_state)
      }

Output:

changing value from 95 to 39
performing expensive copy
β–Ώ SwiftUI.State<Example.CowType>
  β–Ώ _value: Example.CowType
    β–Ώ detail: Example.CowType.(unknown context at $101fee6e4).Detail #0
      - value: 95
  β–Ώ _location: Optional(SwiftUI.StoredLocation<Example.CowType>)
    [skipping 181 lines of _location details]
calling deinit

I believe this is necessary for the way SwiftUI's handling of state and view rendering works (although hard to tell without seeing source code). The _value is probably updated only just before the view's body is evaluated by SwiftUI, as is explained in the documentation for State.update().

2 Likes

If there are parts of your type that are expensive to copy, but those parts aren't the parts that you need to be modifying frequently, consider using a hybrid approach where those parts are stored inline and only the expensive parts are out-of-line.

2 Likes