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()
}

I'm not sure what's the exact problem that you're trying to solve, but you might be missting that CoW is not really the tool to avoid making copies, it's a tool to delay them until the first write (that's why Copy-on-Write). Avoiding such a copy is, I would say, more of a nice side-effect of it than the real goal.

The actual way to avoid copies is by using a reference type from the beginning. You discover in your last post that wrapping a type in a class achieves this β€” but now, if you observe, you have a reference type wrapped in a CoW value type wrapped in a reference type β€” you most likely don't need any of this nesting and could have used a reference type all along.

More specifically, by declaring a value type (whether as CoW or not), you can not ever guarantee that its storage won't be copied, so you can't hope that CoW won't be triggered. In your case, your value type gets captured into a closure, so really it's doomed to trigger CoW at some point. What you should have done from the very beginning it seems is to model your data as a reference type (a class) and provide some means to explicitly copy it if necessary.

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.

1 Like

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().

1 Like

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
Terms of Service

Privacy Policy

Cookie Policy