Hi folks! Long-time reader, first-time caller! This is my first pitch, so in addition to feedback about the concept, feel free to send me a direct message if you have any feedback on the structure/tone/etc. of this post. Thanks!
Introduction
I've been dealing with code that needs to work with temporary sequences of various types. The types I'm thinking of are usually value types and need to be allocated and dealt with en masse. In C or Objective-C, it's easy enough to stack-allocate a buffer in which to hold these values, and the logic to switch to the heap for a larger allocation is pretty simple too. Something like this:
size_t tacoCount = ...;
Taco *tacos = NULL;
Taco stackBuffer[SOME_LIMIT];
if (tacoCount < SOME_LIMIT) {
tacos = stackBuffer;
} else {
tacos = calloc(tacoCount, sizeof(Taco));
}
// do some work here
for (size_t i = 0; i < tacoCount; i++) {
createTaco(tacos + i, ...);
}
eatTooMuch(tacos, tacoCount);
for (size_t i = 0; i < tacoCount; i++) {
destroyTaco(tacos + i);
}
if (buffer != stackBuffer) {
free(buffer);
}
In C++, we can make judicious use of std::array
and std::vector
to achieve the same purpose while generally preserving memory safety.
But there's not really any way to express this sort of transient buffer usage in Swift. You can allocate an UnsafeMutableBufferPointer<Taco>
of course, but such a buffer pointer is allocated on the heap and the optimizer can only stack-promote when it can see tacoCount
at compile-time—something that's not always feasible in production code.
The Pitch
I'd like to propose a new inlinable function in the standard library that allocates a buffer of a specified type and capacity, provides that buffer to a closure, and then deallocates the buffer. The buffer would be passed to the closure in an uninitialized state and treated as uninitialized on closure return—that is, the closure would be responsible for initializing and deinitializing the elements in the buffer. The function would look something like this:
/// Provides scoped access to a buffer pointer to memory of the specified type
/// and with the specified capacity.
///
/// - Parameters:
/// - type: The type of the buffer pointer to allocate.
/// - capacity: The capacity of the buffer pointer to allocate.
/// - body: A closure to invoke and to which the allocated buffer pointer
/// should be passed.
///
/// - Returns: Whatever is returned by `body`.
///
/// - Throws: Whatever is thrown by `body`.
///
/// This function is useful for cheaply allocating storage for a sequence of
/// values for a brief duration. Storage may be allocated on the heap or on the
/// stack, depending on the required size and alignment.
///
/// When `body` is called, the contents of the buffer pointer passed to it are
/// in an unspecified, uninitialized state. `body` is responsible for
/// initializing the buffer pointer before it is used _and_ for deinitializing
/// it before returning. `body` does not need to deallocate the buffer pointer.
///
/// The implementation may allocate a larger buffer pointer than is strictly
/// necessary to contain `capacity` values of type `type`. The behavior of a
/// program that attempts to access any such additional storage is undefined.
///
/// The buffer pointer passed to `body` (as well as any pointers to elements in
/// the buffer) must not escape—it will be deallocated when `body` returns and
/// cannot be used afterward.
@inlinable
public func withUnsafeUninitializedMutableBufferPointer<T, R>(
of type: T.Type,
capacity: Int,
_ body: (UnsafeMutableBufferPointer<T>) throws -> R
) rethrows -> R
The implementation of withUnsafeUninitializedMutableBufferPointer(of:capacity:_:)
would check the required size of the buffer and, if small enough, stack-promote it. If too large to safely fit on the stack, it would heap-allocate (just as UnsafeMutableBufferPointer.allocate(capacity:)
does today.) The compiler can optimize such a function heavily in a way that it can't do with a heap-allocated buffer.
As a convenience, for when you just need transient storage for a single value, we can also provide a second function:
@inlinable
public func withUnsafeUninitializedMutablePointer<T, R>(
to type: T.Type,
_ body: (UnsafeMutablePointer<T>) throws -> R
) rethrows -> R {
return try withUnsafeUninitializedMutableBufferPointer(of: type, capacity: 1) { buffer in
return try body(buffer.baseAddress!)
}
}
I have a proof-of-concept implementation of these functions that I will be sharing in a "work-in-progress" branch in the swift repo in the near future; I'll comment here when that's posted. The proof-of-concept branch (still untested!) is available on my fork of the the Swift repo.
The Catch(es)
There are a few issues with this pitch:
-
To implement
withUnsafeUninitializedMutableBufferPointer(of:capacity:_:)
optimally, a newBuiltin
function would be needed, equivalent toalloca()
in C, to allocate uninitialized space on the stack and extend its lifetime to some future point automatically. Today, there is no mechanism to access such memory without dropping down to C/Objective-C/C++.My proof-of-concept implementation avoids the above constraint by using several internally-defined value types to allocate stack space. These value types exist in the Swift type system and the Swift memory model, so they need to be zero-initialized which incurs a non-zero (heh) runtime cost.
The compiler is still able to optimize away the zero-initialization when it can see that values in the buffer are untouched before the closure initializes them, but this optimization is not always applicable.
-
There is no perfect heuristic for capping the size of the stack allocation. In my proof-of-concept work, I've settled on:
IF size ≤ 1KB AND alignment ≤ natural THEN stackAllocate() ELSE heapAllocate() ENDIF
This is an easy and fast heuristic to implement but probably needs to be refined e.g. for embedded systems.
-
The set of scenarios where these functions are useful in Swift is more constrained than the set of such scenarios in C/Objective-C/C++. In Swift, a better solution is (often? usually?) one that involves creating a really good API interface. The counterpoint is that there's a lot of API out there, especially at lower layers (POSIX, Win32, etc.), that doesn't have a great overlay and probably won't get one in the near future. We still need to use those APIs, so being able to do so in Swift as efficiently as we do in C is still desireable. And, if we do someday get Swift interfaces for those APIs, they'll presumably need to allocate stack buffers under the covers—and to do so, they'll likely want
withUnsafeUninitializedMutableBufferPointer(of:capacity:_:)
!
-JG