Suppose that I'm trying to implement EnvironmentValues
, from SwiftUI myself. I'm not, but for the purposes of this question, my needs are almost identical: given some sort of key, which always tries to store the same type of data, I want to be able to retrieve it with minimal overhead, ideally as close as possible to if it was a stored property of a struct. Let's assume that I can establish ahead of time exactly how much memory I might need in total, or that I don't care about allocating too much.
What I've experimented with so far
An obvious way to do this might be
protocol Keying {
associatedtype Data
}
extension Keying {
static var asKey: String {
// idk how SwiftUI uses the type as a key, this is almost certainly
// the wrong approach.
String(reflecting: self)
}
}
final class EnvironmentValues {
private var storage: [String: Any] = [:]
func value<Key>(for key: Key.Type) -> Key.Data? where Key: Keying {
storage[key.asKey] as? Key.Data
}
func update<Key>(
_ key: Key.Type,
to value: Key.Data?
) where Key: Keying {
storage[key.asKey] = value
}
}
This is fine, but it probably isn't very fast, between the dynamic cast and the boxing and unboxing of values. We "know" that the type is always the same, if it's present.
Unfortunately, switching to unsafeBitcast
in a bid to increase performance fails, because the box is the wrong size.
We can get the unsafeBitcast if we switch to storing something that's always the same size:
class AnyBox {
}
final class Box<T>: AnyBox {
init(wrapped: T) {
self.wrapped = wrapped
}
var wrapped: T
}
// inside our impl:
func value<Key>(for key: Key.Type) -> Key.Data? where Key: Keying {
unsafeBitCast(
storage[key.asKey],
to: Box<Key.Data>?.self
)?
.wrapped
}
func update<Key>(
_ key: Key.Type,
to value: Key.Data?
) where Key: Keying {
storage[key.asKey] = Box(wrapped: value)
}
But now we're doing an allocation and reference counting. This is probably faster, but it seems like we can do better.
We could safely store AnyObject
s by using Unmanaged
and the like to correctly increment and decrement reference counts, but I'm not sure how we'd take ownership of a non-trivial value type like an Array
.
In Array.swift in the standard library, it looks like we can do this using UnsafePointer.initialize
:
(_buffer.mutableFirstElementAddress + oldCount).initialize(to: newElement)
But the documentation for that function states that " The destination memory must be uninitialized or the pointer’s Pointee
must be a trivial type". So now I'm confused about how Array.append()
is using the function, and why it's usage does not contradict the documentation.
That said, assuming this is the right function to call, it seems like I should be able to allocate a buffer that's the right size for my storage needs, and store the instances as they're set in the appropriate places. Presumably I would also need to figure out how to align them appropriately, or would need to settle for doing an individual allocation for each one.
Does this seem reasonable? Have I missed an even faster approach?