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?
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)
}
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?
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).