Performance benefits of using Array<Element> over UnsafeBufferPointer<Element>?

I generally use a lot of UnsafeBufferPointer<Element> for “performance” and really only return Swift native Arrays for user-facing functions. However I’ve been thinking a lot about this given the recent horror stories going around and I’m wondering if Arrays are actually friendlier to the Swift optimizer and I should prefer using them for reasons other than following Swift idioms. I can think of a couple reasons this might be the case:

Pro:

  • If Foundation is out of the picture, Arrays have basically the same amount of indirection as Buffers, with the slight difference that Arrays store their count and capacity inline in their backing storage, and so take up just one word on the stack. (While an equivalent vectorbuffer takes three.)

  • Swift Arrays have value semantics and their contents obey let and var making it easier for the optimizer to reason about them.

  • Arrays check the element indices and trap on out-of-bounds. I’ve written enough C and C++ to appreciate this feature and the penalty seems to be like less than 10% at worst. And it can always be circumvented using withUnsafeBufferPointer.

  • Arrays lend themselves better to functional styles of programming and so are easier vectorize and parallelize.

  • Memory-safe Buffers need to be wrapped in class types (because they need deinit) when facing users, which effectively adds a layer of storage indirection. An Array can be safely placed inside a struct type when designing APIs. (a workaround: can use unmanaged buffer with header class type,, but at that point you’re basically rewriting Array.)

Con:

  • Arrays have reference counting overhead. I don’t know if the compiler knows to optimize this away if the Array is local and never gets returned.

  • Swift doesn’t have a concept of a fixed-size array (not talking about stack-allocated arrays or contiguous tuples but certainly related). This means the count and capacity are redundant in these cases and I’m not sure the compiler can optimize based on knowledge the array never changes size the way a buffer pointer never changes size.

  • Arrays have value semantics and so you can’t opt-into shared, manually-managed storage the way you can with a Buffer. This means Arrays accumulate a lot of reference counting overhead if they move around enough, even if they never get mutated.

  • If you’re using Foundation, then you have to remember to use ContiguousArray and not Array (ugh), and on top of that, weird things happen performance-wise when you use ContiguousArray. Though this is only really relevant if you’re someone who uses a lot of class types.

Can someone who knows more about the Swift optimizer comment on this?

3 Likes

I would expect that you define APIs in terms of Arrays and drop down using withUnsafeBufferPointer in isolated cases to work around performance limitations. In the future, I'd like to see a safe, move-only, fixed-size array replace any remaining cases that you find yourself allocating your own unsafe buffers just because Array is overkill.

5 Likes

I mean that’s basically what I already do except my internal code works almost entirely in the UnsafeBufferPointer domain. I’m asking if performance (and performance potential, pending compiler improvements) could actually benefit from using Array inside the module as opposed to on the surface

I think now that arguments are passed with a +0 refcount, you can probably get away with just passing arrays and mutable slices everywhere. But honestly, the only way to know for sure is to try it.

2 Likes

hmm. i forgot about that

That was only just implemented in 4.2 though, so it won't have any effect if you're still using the 4.1 compiler.

Perhaps the High Level SIL Optimizations for Array can bring some performance benefits of using Array over UnsafeBufferPointer in certain situations?

2 Likes