I have a pretty standard(?) linked-list implementation that looks like this:
struct Node {
struct Raw {
let key: UInt
var value: UInt
var next: Node?
}
let pointer: UnsafeMutablePointer<Raw>
var raw: Raw {
get { pointer.pointee }
nonmutating set { pointer.pointee = newValue }
}
init(key: UInt, value: UInt) {
pointer = .allocate(capacity: 1)
pointer.initialize(to: Raw(key: key, value: value))
}
func deinitialize() {
pointer.deinitialize(count: 1)
pointer.deallocate()
}
}
where Node
is basically just an abstraction over a pointer. I was profiling iteration of a linked list of Node
s, and noticed that looping over the list had much worse performance than I was expecting.
Investigating further, I narrowed it down to my usage of one property in particular: raw
. Specifically, when I mutated Raw
through an inout
parameter...
func mutateValue(of node: Node, using mutate: (inout UInt) -> Void) {
mutate(&node.raw.value)
}
performance was worse than when I skipped raw
and used pointer
directly:
func mutateValue(of node: Node, using mutate: (inout UInt) -> Void) {
mutate(&node.pointer.pointee.value)
}
My naive expectation was that Swift would basically treat these two functions as identical, "see through" what raw
is doing, and treat it identically to the second case.
Looking at the generated assembly in Godbolt it appears to me that Swift appears is be treating raw
literally — copying the value, mutating it, then copying it back, instead of mutating it in-place, like using pointer
directly does. (I partly assumed this because I got used to seeing Swift implicitly generate modify
accessors when it notices a particular kind of get/set pattern.)
Though slightly tangential, a second odd thing is...
..that these two functions generate identical assembly but don't get merged:
func testInlineMutationThroughPointer() {
node.pointer.pointee.value = 300
}
func testInlineMutationThroughComputedProperty() {
node.raw.value = 300
}
Two questions:
- Is there a way I can indicate to Swift that it should treat
&raw...
the same as&pointer.pointee...
? - Would it break anything if Swift did treat these two cases identically, or is this a potential "free" optimization?