Jens
1
Let's say I have some data structure S that allocates some memory to a pointer. It's a struct because I want to avoid double indirection when accessing the data pointed to by its pointer, ie:
struct S {
var somePtr: UnsafeMutableRawPointer
init() {
somePtr = UnsafeMutableRawPointer.allocate(byteCount: 1024, alignment: 16)
print("Allocated", somePtr)
}
func doSomething() { print("Does something") }
func deallocate() {
print("Dellocating", somePtr)
somePtr.deallocate()
}
}
When using S, I have to remember to call deallocate():
let s = S()
s.doSomething()
s.deallocate()
or:
let s = S()
defer { s.deallocate() }
s.doSomething()
Now, and here's the thing I'm wondering about, AFAICS the deallocation can be done implicitly at the end of an S-instance's lifetime, like so:
struct S {
var somePtr: UnsafeMutableRawPointer
private final class Deinitializer {
var block: () -> Void
init(_ block: @escaping () -> Void) { self.block = block }
deinit { block() }
}
private let deinitializer: Deinitializer
init() {
somePtr = UnsafeMutableRawPointer.allocate(byteCount: 1024, alignment: 16)
print("Allocated", somePtr)
deinitializer = Deinitializer { [somePtr] in
print("Dellocating", somePtr)
somePtr.deallocate()
}
}
func doSomething() { print("Does something") }
}
func test() {
let s = S()
s.doSomething()
}
test()
Is this a reasonable thing to do for types like S?
The only downsides I can see are:
- It adds 8 bytes of storage to the struct.
- It has to allocate and deallocate the class instance, and call deallocate via indirection. This is only a problem if
S instances are created and destroyed with high frequency, in which case allocating and deallocating somePtr would already be a problem.
- No explicit control over deallocation, which seems like it's not a big deal, because the same control is still available via
do { ... }.
Are there more?
2 Likes
Karl
(👑🦆)
2
Sure. In fact, it's even used in the standard library.
It doesn't have to call deallocate via indirection. You could replace the closure with a copy of the pointer, like this:
private final class Deinitializer {
let ptr: UnsafeMutableRawPointer
init(_ ptr: UnsafeMutableRawPointer) { self.ptr = ptr }
deinit { ptr.deallocate() }
}
Now what you've created is an "owner" for the memory pointed to by ptr. The fact that you're storing a copy of that pointer value alongside it in a (pointer, owner) pair to avoid double indirection doesn't matter.
A similar pattern is used for shared Strings (which, even though not publicly exposed, are baked in to the ABI). Take a look at implementation of __SharedStringStorage (and the draft implementation to see how it is used). It does a similar thing with (pointer, owner) pairs:
final internal class __SharedStringStorage {
internal var _owner: AnyObject?
internal var start: UnsafePointer<UInt8>
...
}
Here, as far as the String is concerned, _owner is just "some object I need to retain to keep the memory alive" (and, by implication, something it should release when it no longer needs the memory).
Of course, ManagedBuffer is the preferred way to allocate storage while avoiding double-indirection, but if you have a use-case which can't be modelled with that type, storing a (pointer, owner) pair is a perfectly reasonable implementation strategy IMO.
8 Likes