TemporaryArray: a stack-seeded, heap-spilling array - #683
Conversation
TemporaryArray is a ~Copyable and ~Escapable dynamic array whose initial storage is a borrowed buffer (most usefully a stack allocation vended by withTemporaryArray(of:capacity:_:)) and which transparently spills over into freshly allocated heap storage once it outgrows that seed buffer. As long as the element count stays within a seed buffer that fit on the stack, the array incurs no heap traffic at all. Because it can hold a dependency on borrowed (stack) memory, the type is non-escapable: instances cannot outlive the scope that provides their initial buffer. Contents are lifted out into an owning container with take(), which transfers the heap buffer in O(1) if the array has already spilled, or moves the elements into a fresh UniqueArray otherwise. The API mirrors UniqueArray (SE-0527), minus the pieces that don't apply to a non-escapable scratch type (DynamicContainer, MutableContainer, and RangeReplaceableContainer conformances plus the Drain-based consumer), plus members unique to its borrow-then-spill design: take(), clone(), and append(repeating:count:). The seed-buffer initializer is kept internal for now, with withTemporaryArray as the sole entry point. Equatable/Hashable and CustomStringConvertible/CustomDebugStringConvertible are implemented as methods with FIXMEs to add the conformances once those protocols support ~Copyable/~Escapable types. The Collection/RangeReplaceable surface and container-protocol conformances are gated behind the UnstableContainersPreview trait, mirroring UniqueArray; the core API builds in the default configuration. DocC coverage is added for the type and the withTemporaryArray entry point.
c6dd3e8 to
3a07388
Compare
|
As someone who had to hand-roll the same type as this in https://github.com/swift-dns/swift-idna, I'm already +1 on this. I was also trying to extract the impl to https://github.com/swift-dns/swift-tiny-sequence but haven't yet had the time to properly do the whole work (The current impl which is specialized for swift-idna is here, the other one in swift-tiny-sequence is behind). I'll later check to see if this branch's impl fits my usage (I see no reason why it shouldn't, the impls are similar, but just to be sure before the type is merged / tagged). |
|
Great, implementation should be ready for experimentation. Let me know if you have any feedback. |
|
Looks good! Quick notes:
|
|
That makes sense. |
TemporaryArrayA dynamically self-resizing,
~Copyableand~Escapablearray ofpotentially noncopyable elements. Its initial storage is a borrowed buffer,
most usefully a stack allocation vended by
withTemporaryArray, and ittransparently spills over into freshly allocated heap storage the moment it
grows beyond that initial buffer. As long as the element count stays within a
seed buffer that fit on the stack (currently up to 1KB), the array incurs no
heap traffic at all.
Because it can hold a dependency on borrowed (stack) memory,
TemporaryArrayis non-escapable: instances cannot outlive the scope that provides their
initial buffer. To keep the contents past that scope, move them into an owning
container with
take().Its API mirrors
UniqueArray(SE-0527), minus the pieces that don't apply to ascratch/non-escapable type plus a small set of members
unique to its borrow-then-spill design.
Entry point
This is how this type is mainly intended to be initialized. It allocates
capacityon the stack if the storage required is below 1KB, otherwise on the heap.
/// Provides a dynamically-resizing array that is initially backed by a stack /// allocation of the requested capacity, spilling over to the heap only if it /// grows beyond it. /// /// This is the primary way to create a `TemporaryArray`. The array passed to /// `body` starts empty with room for `capacity` elements. As long as the /// array's element count stays at or below that capacity, no heap allocation /// occurs, provided the requested storage fits within the stack budget. /// /// To keep latency predictable, the initial buffer is placed on the stack only /// if it occupies at most 1024 bytes; a larger initial `capacity` is heap /// allocated up front instead. Either way the array grows on the heap once it /// exceeds its initial capacity. /// /// The array cannot escape `body` (it is non-escapable), so when nothing is /// moved out, small inputs never touch the heap: /// /// let sum = withTemporaryArray(of: Int.self, capacity: 64) { scratch in /// for x in numbers where isHot(x) { /// scratch.append(x * x) /// } /// var total = 0 /// for i in scratch.indices { total += scratch[i] } /// return total /// } /// /// To keep the elements themselves, move them into an owning container with /// `take()`. /// /// - Parameters: /// - type: The element type of the array. /// - capacity: The number of elements to reserve up front. /// - body: A closure that receives the freshly created, empty array. /// - Returns: The result of `body`. func withTemporaryArray<Element: ~Copyable, E: Error, R: ~Copyable>( of type: Element.Type, capacity: Int, _ body: (inout TemporaryArray<Element>) throws(E) -> R ) throws(E) -> RWe could add more overloads similar to UniqueArray.init's that already copy elements into the array.
API new or different from
UniqueArrayAPI shared with
UniqueArrayThe largest part of the API is identical to
UniqueArray.Similarly, the implementation is largely identical, the main difference being
that
deinitchecks whether the storage is on the stack or the heap and onlydeallocates it if it is on the heap. Reallocation performs a similar check.
Conformances
TemporaryArrayattempts to conform to the same protocols asUniqueArray, butsome of them do not yet support
~Escapabletypes.UniqueArrayAPI omittedChecklist