Key path mutations and reference counting

I was working with a copy-on-write wrapper type and found that when mutations were made to it via key path, it was always executing the copy path, even when the box is uniquely referenced. It appears that the act of modification via key path increments the reference count for the duration of the mutation.

Here's a simple playground reproduction:

final class Box<T> {
  var value: T
  init(_ value: T) { self.value = value }
}
struct COW {
  var int: Int {
    get { box.value }
    set {
      if isKnownUniquelyReferenced(&box) {
        print("modify")
        box.value = newValue
      } else {
        print("copy")
        box = Box(newValue)
      }
    }
  }
  var box = Box(0)
}
final class Ref {
  var cow = COW()
}
let ref = Ref()

ref.cow.int += 1              // prints "modify"
ref[keyPath: \.cow.int] += 1  // prints "copy"

Is this simply a known limitation of using key paths? Is there a way to work around the problem and eliminate these copies?

2 Likes

if you have a tolerance for not-yet-official features, i think if you swap to the coroutine getter you'll eliminate the issue, at least in this example:

struct COW {
  var int: Int {
    _read { yield box.value }
    set {
      if isKnownUniquelyReferenced(&box) {
        print("modify")
        box.value = newValue
      } else {
        print("copy")
        box = Box(newValue)
      }
    }
  }
  var box = Box(0)
}
1 Like

Huh, that does seem to work in the simplified example above, but not in a slightly more complex one:

final class Box<T> {
  var value: T
  init(_ value: T) {
    self.value = value
  }
}

struct COW<T> {
  var value: T {
    _read { yield box.value }
    set {
      if isKnownUniquelyReferenced(&box) {
        print(T.self, "modify")
        box.value = newValue
      } else {
        print(T.self, "copy")
        box = Box(newValue)
      }
    }
    _modify {
      if !isKnownUniquelyReferenced(&box) {
        print(T.self, "copy")
        box = Box(box.value)
      } else {
        print(T.self, "modify")
      }
      yield &box.value
    }
  }
  var box: Box<T>
  init(_ value: T) {
    box = Box(value)
  }
}

struct Child {
  var count: Int {
    _read { yield _count.value }
    set { _count.value = newValue }
    _modify { yield &_count.value }
  }
  var _count = COW(0)
}

struct Parent {
  var child: Child {
    _read { yield _child.value }
    set { _child.value = newValue }
    _modify { yield &_child.value }
  }
  var _child = COW(Child())
}

@dynamicMemberLookup
final class Ref<T> {
  var value: T

  init(_ value: T) {
    self.value = value
  }

  subscript<V>(dynamicMember keyPath: WritableKeyPath<T, V>) -> ProjectedRef<T, V> {
    ProjectedRef(self, keyPath: keyPath)
  }
}

final class ProjectedRef<R, V> {
  let ref: Ref<R>
  let keyPath: WritableKeyPath<R, V>

  var value: V {
    _read { yield ref.value[keyPath: keyPath] }
    set { ref.value[keyPath: keyPath] = newValue }
    _modify { yield &ref.value[keyPath: keyPath] }
  }

  init(_ ref: Ref<R>, keyPath: WritableKeyPath<R, V>) {
    self.ref = ref
    self.keyPath = keyPath
  }
}

let parent = Ref(Parent())
parent.value.child.count += 1  // prints "Int modify"

let child = parent.child
child.value.count += 1         // prints "Int copy"

Any idea how to eliminate the copies in this case?

1 Like

i've been unable to come up with anything. my current best guess is this might be an inherent current limitation if the runtime functions are needed to interact with the keypath (see this comment and the 'keepAlive'/writeback stuff). but i'm pretty much just speculating – @Joe_Groff or @Alejandro could you shed any light what may be going on here if you have a chance please?

1 Like

The current key path implementation relies on get/set interfaces for accessing computed properties, so it can't take advantage of coroutine accessors to avoid refcounting yet. If the components are all stored properties, it might be possible, but maybe the runtime introduces unnecessary copies somewhere still (though Alex has greatly improved that recently).

1 Like