SE-0453: Vector, a fixed size array

How does literal initialization of a Vector work with overload resolution?

Could we add an overload to Sequence<>.flatMap where the transform closure returns a Vector without breaking source or ABI?

extension Sequence {
  func flatMap<let segmentCountOfResult: Int, SegmentElementOfResult >(
    _ transform: (Element) throws -> Vector<segmentCountOfResult, SegmentElementOfResult>
  ) rethrows -> [SegmentElementOfResult] { ... }
}

This overload could allocate a more reasonable amount of memory by multiplying the static Vector count segmentCountOfResult with underestimatedCount of the Sequence.

That sounds like a very interesting idea. Iterating a Range and afterwards indexing into a Vector currently has the problem of double bounds checking. First in the Range subscript because iterating a Range uses the IndexingIterator. Afterwards again in the subscript of the Vector. I'm not sure if we can get rid of the bounds checking in the Range<>.subscript (WDYT @lorentey?).

RangeVector<count, Int> (or maybe VectorIndices?) could likely completely eliminate bounds checking if we cary the static information all the way through i.e.:

struct VectorIndices<let count: Int>: RandomAccessCollection {
   typealias Element = VectorIndex<count>
   ...
}
// index between 0 ..< upperBound
struct VectorIndex<let upperBound: Int> {
    let index: Int
}
extension Vector {
   subscript(position: VectorIndex<count>) -> Element {
      // bounds check can be skipped because index is guaranteed to be 
      // between 0 ..< count
   }
}

This would also allow to iterating a pair of vectors with equal lengths without bounds checking.

This can potentially be extended in the future if we can express smaller than or equal relations with integer generics e.g.:

extension Vector {
   subscript<let upperBound: Int>(
       position: VectorIndex<upperBound>
   ) -> Element where upperBound <= count {
      ...
   }
}

Not sure how important this is in practice though.

This would mean iterating over a vector of 512 elements would require allocating and filling an entire 4KB of integers.

Perhaps there’s some confusion here in what integer generics enable. The compiler can already do constant-value propagation and optimizations based on that such as loop unrolling. I believe that actually happens at the LLVM layer, well below the layer of the Swift type system.

Integer generics allow the compiler to reckon about the size of generic types, which unlocks new forms of code generation that don’t necessarily require abstraction behind pointers. This is a much higher level concept which surfaces in the visible language semantics that affect what kind of data structures and algorithms are possible to express in Swift.

3 Likes

Is Vector<3, Int> a subtype of Vector<2, Int>?

This is on my mind because I just found myself limited by the type system in modeling a certain situation and I believe that if a given generic parameter pack were a subtype of all other parameter packs that share the same prefix (really the prefix would only need to be a subtype of the other prefix) then that would have gone most of the way toward solving my problem.

Brief description of why I needed this...

I had to forego associatedtype Input in one of my protocols and resort to dynamic type casting because I also needed to model entities that take no input, and it was not good enough to model them with Input = Void because I have extensions with generic constraints that require that the Input types of two different conforming types are equal.

What I would really need is something like the following:

extension PredicateExpressions.Equal: MyPredicateExpression
where LHS: MyPredicateExpression,
      RHS: MyPredicateExpression,
      LHS.Input <=> RHS.Input {

    typealias Input = max(LHS.Input, RHS.Input)
}

where <=> means "have a subtype relation" (one is a subtype of the other), and max(LHS.Input, RHS.Input) means "the more specific of the two types". Come to think of it, these two tools would have solved my situation without the need for the subtyping relationships between variadics, because my no-input type could just use Input = Any, but if at some point I wanted to generalize this such that each type is allowed a variadic Input type then I would need the variadic subtyping.

No, because the upcast would lose information. Which C++ is fine with (it’s what happens with value types that use inheritance when you upcast), but not Swift (this behavior of C++ is usually considered a pitfall).

2 Likes

Does this also mean that there is no way that longer parameter packs could ever be passed in places that are expecting only the prefix?

I think you would need explicit syntax for that, yes, but a parameter pack isn’t a first-class value, so it could get different rules in theory. I don’t think Vector is similar enough to be relevant here; even if we eventually have “known-length parameter packs with a common element type” that you could get in or out of a Vector, it would still be a distinct language concept.

Yep :slightly_smiling_face:

In your example, indices is known to run the loop 8 iterations. After multiple passes of inlining, it will essentially boil down to the same generated instructions.

For what it's worth, the bounds check of a vector subscript is already eliminated now when 1. the index is known at compile time and when 2. the vector's count is known at compile time. The compiler notices that the precondition value is always false so it just zaps the it altogether.

Currently it always prefers the array overload (as it should). E.g.

func something(_: [Int]) {}

func something(_: Vector<2, Int>) {}

func main() {
  something([1, 2]) // takes the array 'something'
  something([1, 2, 3]) // takes the array 'something'
  something([1, 2] as Vector) // takes the vector 'something'
}
3 Likes

I think there is a clear value in the indices property returning the common Range type; I do not see a need for introducing a new set of fixed-size Range/ClosedRange variants. (And I certainly wouldn't want us to roll a custom range type that's specifically tied to Vector.)

Swift represents integer ranges using Range<Int>; the indices of a Vector form an integer range; therefore the Vector.indices property must be of type Range<Int>.

(As we discussed above, the Vector.Indices typealias is a remnant of a failed Collection conformance; we should remove it from the proposal. The Vector.indices property should stay, though, to be consistent with Span. This property will be useful for iteration, at least until we become able to directly iterate over noncopyable/nonescapable container types like Vector.)

As you describe, Range/ClosedRange rely on the default IndexingIterator, and indeed this means that a trivial loop like for i in v.indices { foo(v[i]) } technically performs two unnecessary rounds of bound checking in each iteration: one in Range.index(after:), followed by another in Vector.subscript. The compiler is often able to eliminate one or both of these, but this is not a strict guarantee that can be safely relied on. (Especially not in unspecialized generic code, where regular inlining heuristics do not apply.)

This issue is not new with Vector -- it lies within the preexisting Range/ClosedRange types. For example, for-in loops over an array's indices are subject to the same unnecessary bounds checking behavior, and so are loops over manually constructed ranges, as in for i in 0 ..< array.count. These are quite common in Swift, and they generally work well enough.

I don't think the range types are broken enough for us to consider replacing them. Rather, we should find a way to improve for i in range loops even though Range.Iterator is forever stuck with IndexingIterator.

There are several ways we can go about this, ranging from narrow tweaks like encouraging existing optimizer phases to trigger by carefully force-inlining specific entry points (such as Range._failEarlyRangeCheck), all the way to major changes to compiler magic, such as tweaking the implementation of for-in loops if the subject happens to be a range type. (Either by manually special casing these types in the compiler, or by adding a new Sequence requirement (or child protocol) that opportunistically opts in to some new for-in implementation.)

All this is useful and important work, but it is almost entirely orthogonal to the proposal at hand. The range types are extremely commonly used throughout pretty much all of Swift, and therefore even slight changes to their behavior will inevitably risk causing widespread breakage. We also know that for-in loops over ranges generally work well enough: the bounds checks do often happen to be optimized away, as expected.

So I expect for i in vector.indices will be a fine stopgap solution for vector iteration, until we are able to provide direct borrowing for-in loops over (potentially) noncopyable container types. If there are important optimization opportunities we aren't taking, I trust we'll be able to patch them by carefully applying some elbow grease.

I expect this issue will be entirely eliminated with the advent of borrowing for-in loops enabled by the upcoming container protocols. These are designed to get rid of iteration bottlenecks of all sorts, including bounds checking overhead. Because these new protocols will be (necessarily) distinct from Sequence/Collection, they are free to establish their own patterns. This gives us a chance to make a different set of exciting mistakes than before, this time optimizing for performance over expressivity.

In-memory containers hold their elements as a series of contiguous memory regions -- this suggests a model for borrowing iteration that is based on exposing borrow access to these storage chunks using the nonescapable Span type. We do not have the means to implement this model in Swift yet, as the protocols will be built on language features that do not exist yet. But here is a glimpse of roughly where I want us to go: (The magic wand :magic_wand: marks some of the missing pieces. This is just a rough draft; none of the API names or precise signatures/shapes that I present here are final.)

public protocol BorrowingIteratorProtocol: ~Escapable {
  associatedtype Element: ~Copyable // đŸȘ„noncopyable associated types 

  // đŸȘ„protocol member requirements with nonescapable return types
  mutating func nextChunk(maximumCount: Int) -> Span<Element>
}

protocol Container: ~Copyable, ~Escapable {
  associatedtype Element: ~Copyable // đŸȘ„noncopyable associated types 
  associatedtype BorrowingIterator: BorrowingIteratorProtocol, ~Escapable
  where BorrowingIterator.Element == Element // đŸȘ„nonescapable associated types 

  // đŸȘ„protocol member requirements with nonescapable return types
  borrowing func startBorrowingIteration() -> BorrowingIterator
  ...
}

extension Span: BorrowingIterator where Element: ~Copyable {
  mutating func nextChunk(maximumCount: Int) -> Span<Element> {
    let c = Swift.max(0, Swift.min(maximumCount, count))
    defer { self = self.extracting(unchecked: c...) }
    return self.extracting(unchecked: ..<c)
  }
}

extension Vector: Container where Element: ~Copyable {
  var storage: Span<Element> { // đŸȘ„ Nonescapable computed properties
    get { ... }
  }

  typealias BorrowingIterator = Span<Element>
  func startBorrowingIteration() -> BorrowingIterator {
    self.storage
  }
}

let items: Vector = [0, 2, 4, 6, 8]

// This loop:
for borrow item in items { // đŸȘ„ borrowing for-in loop syntax
  foo(item)
}
// Turns into something like this:
var it = items.startBorrowingIteration()
while true {
  let chunk = it.nextChunk(maximumCount: Int.max)
  guard chunk.count > 0 else { break }
  var i = 0
  while i < chunk.count {
    defer { i &+= 1 }
    borrow item = chunk[unchecked: i] // đŸȘ„ borrow bindings
    foo(item)
  }
}

Of course, this raises many more questions than it answers. But it hopefully illustrates how this approach will enable for-in loops to efficiently provide direct borrowing access to the contents of in-memory containers. Unnecessary per-iteration bounds checks are either eliminated entirely, or at least they are limited to be within nextChunk calls that are invoked once per storage chunk, rather than once per element. In the case of contiguous containers like Vector or Array, there is only a single storage chunk, so the outer loop will iterate exactly once.

Some notes on unresolved and/or curious details
  1. The for-in expansion will have some interesting mechanical details, as a break statement in the loop body needs to break out of the outer loop in the expansion, while continue statements need to only apply to the inner loop.

  2. The semantics of member requirements that return nonescapable types are intentionally left unspecified above, as they're currently still unclear -- nonescapable types (like Span itself) want to conform to Container, too, and that raises some interesting questions about the precise lifetime requirements on the result of startBorrowingIteration. (For composability, we need the spans returned by the iterator to be directly tied to the container itself (not the iterator). But if the container is nonescapable, do we want the spans to inherit the dependencies of the container, or should they be scoped to the container itself? I.e., do we need to allow the spans returned to sometimes survive the (nonescapable) container they (ostensibly) came from?)

  3. Things get even more interesting when we consider that protocol Container should also allow nonescapable elements. Inevitably, we will need to be able to, say, collect a bunch of Span instances into some future array-like type, and then iterate over them. To do this, we need to be able to reason about lifetime dependencies of not just spans, iterators, and containers, but also the elements that are contained within. (E.g., how do lifetime dependencies of the elements relate to those of the container? It seems likely that some escapable types will want to conform to Container with a nonescapable element type. How do we describe the lifetime dependencies of the elements of such a type? Can we even model such escapable containers with the same general protocol?)

The advent of the new borrowable container protocols will resolve the question of how to iterate over the contents of a Span or Vector, but it will not directly help Range/ClosedRange, as those types will not be able to conform to the new protocols. Borrowable containers expose direct access to their contents; this means that their contents need to physically exist somewhere "within" them. Ranges only materialize their contents on demand. That is fine for Sequence/Collection, but it will definitely not be okay for Container.

However, the idea of span-based iteration can be retrofitted onto Collection, too. To support on-the-fly generation of items, it will need to use different (and far less useful) lifetime semantics, where the returned spans are tied to a borrow of the iterator, and they cannot survive until the next nextChunk call. This is a potential direction I'm investigating; its possible benefits include the possibility to give Range a new iterator type, escaping the sad shackles of its IndexingIterator. This is what I alluded to above when I talked about classic for-in loops opportunistically switching to a new implementation.

In an ideal world, we would delay doing this Collection work until we have finalized the Container protocols, to avoid unnecessary discrepancies between them. However, adding a new (escapable/copyable!) BulkIterator associated type to Collection would require fewer future language features, so it may be possible to ship it sooner. (The primary risk is that this work wouldn't directly help noncopyable types, but it would distract us from figuring out the unresolved semantic issues about protocols involving nonescapable types. What we urgently need is a workable model for iteration over noncopyable types; retrofitting bulk iteration into Collection is going to be a sideshow at best.)

12 Likes

Yep, this is a problem. Whether a swiftinterface uses this upcoming feature or whether the client uses it, if both modules don't agree on what the type of a C record field is then we run into issues.

As a workaround, we think we can resolve this by importing a C array field as two fields in the generated Swift struct. Consider the following example:

typedef struct {
  char x[4];
} my_type;

would get imported as:

struct my_type {
  var x: (CChar, CChar, CChar, CChar)
  var xVector: Vector<4, CChar>
}

We would add a new <field_name>Vector property on the generated Swift struct that would reference the same underlying C field. This actually seems to work out quite well because it doesn't affect queries like MemoryLayout<my_type>.size, it would still return the same size as if the new field didn't exist. The same could be done for global C arrays being imported as 2 separate definitions but accessing the same underlying value.

This approach would require a longer deprecation period. We'd first add this new field in one release -> in another release add the deprecation warning -> swap x (in this example) to be imported as a Vector and deprecate the xVector -> remove the xVector or something similar across these lines.

Of course, as you mention, this only really works for fields. Function parameters that are pointers to C arrays do get imported today in Swift as UnsafePointer<(CChar, CChar, ...)>, but we also suspect it to be pretty rare in general. We could choose to import such function definitions twice using a similar strategy of appending some Vector to the end of the function name to access the vector version, or we could maybe overload the function name? (this might cause source compatibility issues with constructs such as let x = my_c_function)

3 Likes

This implies that C’s char x[] cannot be imported as

struct my_type {
  var x: () // this is Void
  var xVector: Vector<0, CChar>
}

And that’s good, since C’s char x[] is just sugar for char *x.

But even outside of C imports, should Vector<0, Int> be a valid Swift declaration?

This is incorrect about char x[], this syntax indicates data trailing a struct when behind a pointer. But yes, it would not be imported as a Vector.

Defining a zero-length vector isn’t generally useful on its own, but could be useful for generic code. For example, a UsuallySmallArray type that either stores data inline or on the heap might let you choose the “small” size, and “0” would be a good choice for an array that’s usually empty. (Assume the “0” lives somewhere away from the array, or else you’d just use Array to begin with in that case.)

3 Likes

SE-0453 has been revised and is now in a second phase of review.

4 Likes