SE-0229 — SIMD Vectors


(Jordan Rose) #104

SIMDVectorizable has underscored requirements without defaults, so this doesn't really seem to be the case. Which makes me sad, because I think Vector4<CGFloat> (for example) might make sense for some users.


(Jordan Rose) #105
public protocol SIMDVectorStorage {
  /// The number of elements in the vector.
  var elementCount: Int { get }

What's the motivation behind making this an instance property rather than a type property?


(Lukas Stabe 🙃) #106

Dave has expressed mild concern with using the names Vector2<T> , etc, instead of SIMDVector2<T> . I don't think that this is a significant concern because the new Vector2 implemented here should be suitably general to function as "the two-dimensional vector" of almost any type in almost any setting. It has more operations than some use cases require, but they will usually not interfere with the desired operation, and it's useful to have a common currency type available for small vectors.

Is that really true? The proposal requires elements of the vector types to be at least Hashable (which sounds like it makes sense) and SIMDVectorizable (which requires a bunch of scary underscored typealiases).

Beyond that, if these vectors should take the role of general vectors, where is Vector5 or Vector9001? You see where I'm going.

These vector types seem to be modeled very closely after simd, and I think either the name should make that clear or (preferably, I think) all this stuff should go into a SIMD module, rather than the standard library (which would also allow us to drop the prefixes on some more of the types).


(Ben Cohen) #107

There's nothing stopping you implementing these underscored requirements. So it really depends on what an underscored requirement means – and what it means is really what we chose it to mean.

To-date, underscores on things in the standard library means one of a few things:

  1. This should be an internal implementation detail, but we lack language features to make it so, so it's public but underscored instead.
  2. This is subject to change, full rights reserved to break/change it in the future. Use at your own peril.
  3. This is something you want to be able to implement on the protocol, but shouldn't be exposed to users of a type.

There's a lot less call for 1 these days, and 2 becomes vanishingly useful once we declare ABI stability.

That leaves 3 and we don't have a good way of spelling it. Another example would be Sequence._customContainsEquatableElement(_:)->Bool?, which a sequence implementation that has a better way of implementing contains can implement. It's a hook that the constrained extension on Sequence where Element: Equatable that supplies contains can call, giving you dynamic dispatch even though not all sequences contain equatable elements. We want to expose that to implementors of types that conform to Sequence, but a user should never see it on the concrete type when they're using it. So we underscore it. But you can still override it if you want to. It would maybe be better to do it in some more official way (say, with an @implementationDetail attribute or something).

The difference, like you note, is that the current things like this on Sequence have default implementations, whereas these don't. I don't know if that's a particularly critical dividing line. Ultimately you are going to need a fairly high degree of sophistication to make a type vectorizable. Having to know about these underscored customization points doesn't seem that big a deal. You will still get told about them if you try to conform to the type. Really all they hide stuff from autocomplete – which is what we want. It would be confusing for users of these types to see these things appear both as a nested type on T and as a top-level Vector3<T>


(Jordan Rose) #108

The meaning of a leading underscore isn't solely decided by the Swift project; it's also part of the context of platform headers and Apple SDKs. For an external developer, underscores mean #2, even if in practice some sops to compatibility are made. We really, really shouldn't muddy that story, which is already pretty subtle.

If "this is something you want to be able to implement on the protocol, but shouldn't be exposed to users of a type" is important, then we should come up with some better way to model this.* But it shouldn't be an underscore that developers, any developers, are expected to type.

EDIT: can't believe I forgot underscored language features, like @_specialize, which will change.

* I'll note that this is very similar to the justification for protected, and so I'll include my usual pushback: if you limit these to conformers in a strict way, then it's harder to write helper functions.


(Jordan Rose) #109

Ultimately I don't think I buy the argument that these don't belong as nested types. We have two ways to spell plenty of things, like String.SubSequence and Substring. That's just how associated types work in this language.

If we're actually worried about saturating documentation or code completion, especially for first-time users, well, I think we should address that directly (and separately). Swift style (largely inherited from Objective-C) is to add functionality using members, and that's one of the downsides. This proposal shouldn't go out of its way to work around that at the cost of violating other existing conventions.


(Steve Canon) #110

It's important to be clear that these are not another way to spell the same thing; they're a thing that users of Vector4<T>, as opposed to people trying to write their own vector types, should never need to see or think about. T._Vector4 is the storage type, with essentially no operations defined on it; Vector4<T> is the thing that provides all of the arithmetic operations for you.

I would be OK with removing the leading underscore if we explicitly named this T.Vector4Storage or similar. Calling it T.Vector4, to my mind, pollutes the user-surfaced types with a thing that almost all users shouldn't ever need to touch.


(Ben Cohen) #111

We could also nest them further, inside a SIMDStorage enum namespace.


(Steve Canon) #112

The absence of these today does not preclude their addition in the future. If we were to add them in the future, I would define them just like Vector4 is here.

There are at least three reasons to be hesitant about doing this:

  • We want to be able to use these types and operations to implement stdlib features for performance reasons.
  • If we do this, either the SIMD module would be implicitly imported any time you have a C header that uses vector types, or the imported interface of a C library would vary depend on whether or not you imported the Swift SIMD module as well.
  • There's no precedent for it. We will absolutely have distributed-with-swift-but-not-stdlib libraries at some point, but the bar to creating the first one is high.

(Steve Canon) #113

In the original proposal, this was called .count, and was an instance property, partially by analogy to Collection, even though vectors are not collections, and partially by analogy too .bitWidth, which is an instance property for FixedWidthInteger types too.

The new .elementsCount removes one of those analogies, but we also have in flight @rxwei's proposals for vector types and protocols, and I expect that we will end up with non-fixed-width vectors that need .elementsCount to be an instance property. It seems like the more consistent design looking forward.


(Richard Wei) #114

Why not use a singular prefix: elementCount?

I'd also suggest scalarCount for the following reasons:

  1. The name of the generic type parameter is Scalar. Using "scalar" would be more consistent.
  2. In multi-dimensional array APIs, "element" can refer to the subarray in a lower dimension. So, elementCount would refer to the size of the first dimension.
  3. "Element" implies that vectors consist of "elements" in some fixed basis. If we want the abstract vector space protocol to be used for general vector spaces that might not come with a canonical basis, "scalar" would be better than "element".

Regarding static/non-static, why not both? Users should be able to access the scalar count when they define a custom initializer or a static method in a protocol extension. This is what I'm thinking of:

protocol SIMDVector {
    ...
    static var scalarCount: Int { get }
    ...
}

extension VectorNumeric where Self : SIMDVector {
    var scalarCount: Int { return Self.scalarCount }
}

(Chris Lattner) #115

Awesome, this revision is a great improvement, thank you!

Here are some random questions, but I don't see any major concerns with this at all:

  • As Richard points out, the elementCount name seems inconsistent:
  /// The type of scalars in the vector space.
  associatedtype Scalar : Hashable

  /// The number of elements in the vector.
  var elementCount: Int { get }

==> I agree that it makes sense to use "Scalar" as the associated type name (after all, these are not collections with generic elements, they are vectors of scalars), but given that it seems more consistent to use scalarCount as the name, and I also think more clear.

  • Why is Hashable and CustomStringConvertible on SIMDVector? It seems like you could sink it to SIMDVectorStorage?

  • The Mask design makes great sense to me. Is there a way to convert it to a Vector or array of bools of clients that want to project the elements out? Or are the masks themselves subscriptable to get the individual bools out?

  • Super nit pick about comment:

  /// Initialize from array literal
  ///
  /// Precondition: the array must have exactly elementCount elements.
  init(arrayLiteral elements: Scalar...)

==> This should really be "initialize from a list of scalars", which is the client side view of the behavior

  • I don't have a strong opinion about this, but it would be fine to subset out all the random support form this basic proposal - can't it be added later? That said, while it is fine to subset it out, it doesn't seem controversial, and I'm +1 on adding it if no one else is concerned.

  • Not very important, by why are the elements of vector4 ordered as "x,y,z,w"? Historical reasons? consistency with vec3/vec2?

  • Does Vec3 have the lo/hi "half" accessors? Is so, what do they do? If not, great :-)

  • Have you thought about how to convert from a Vector3 <=> Vector4? it is fine to punt this to a subsequent additive proposal.

Overall, I am very strongly supportive of this proposal, great work and thank you!!

-Chris


(Richard Wei) #116

Nitpick about the nitpick:

  1. All stdlib doc comments for initializers start with "Creates ...", so this should follow.
  2. Array.init(arrayLiteral:)'s comment actually does say "array literal".

As such, "Creates a vector from the given array literal." seems to be the most consistent choice of comment. And it should end with a period.


(Robert Muckle-Jones) #117

There is precedence for the ordering "x,y,z,w" in graphics programming where the 4 element vector "x,y,z,w" is being used to represent a 3d object using 4d homogenous coordinates. Likewise, a 3 element vector "x,y,w" might be used to represent a 2d object using 3d homogenous coordinates. Homogenous coordinates are ubiquitous in graphics programming.

This raises an interesting question. Does this proposal allow for a 3 element vector with labels "x,y,w" rather than "x,y,z"? It would be awesome if the user could choose labels relevant to their algorithms.


(Erik Little) #118

I would say no. I think it's important that we spell out that this protocol's requirements are specific to SIMD, and not some other kind of vectorization.


(Matthew Johnson) #119

Would it make sense to introduce enum SIMD {} as a “namespace” for these types?


(Ben Rimmington) #120

@scanon The previous revision had a maximum vector size of 64 bytes. Is this no longer required?

I'd prefer the opposite: rename the concrete types to SIMDVector2, SIMDVector3, SIMDVector4, etc. Then the naming of protocols and structures is consistent.

The protocols (SIMDVectorStorage, SIMDVector, SIMDMaskVector, SIMDVectorizable) couldn't be nested. The Unicode "namespace" has type aliases for its top-level underscored protocols (e.g. Unicode.Encoding = _UnicodeEncoding).


(Steve Canon) #121

Correct. I discussed this with the core team, and we decided to relax this requirement. Codegen for very large vectors will be suboptimal in some cases, but (a) we can plausibly warn about it (b) it's a problem that can be improved over time so we don't think it's necessary to constrain the library types.


#122

I think this is a mistake.

Vectors are very much “collection-like”, and we should standardize on using “count” for the number of elements. After all, we don’t call the String equivalent characterCount (nor length like NSString does). From the perspective of someone *using* SIMD vectors, querying how many elements they have should look and feel the same as doing so for any other type.

We should be cognizant of Sharp Regret #7, and not make syntax extra-heavy just to call out that the new thing is different from the old thing. In a few years, the new thing will be just another old thing, but we’ll be stuck with the heavier syntax—and people will wonder why SIMD vectors don’t have a simple count property.

In this thread some people have asked what the equivalent will be for SIMD matrices, whether it should give the number scalars or the number of vectors in the matrix. Well, when the time comes we’ll simply pick one, either modeling matrices like collections-of-vectors or like collections-of-scalars.

Personally, I’d say matrices should have width and height properties, which provide the number of vectors in each dimension, thus count would refer to the number of scalar elements. But we’ll cross that bridge and make that decision when we come to it.

For the matter at hand, I strongly recommend that SIMD vectors should have their count property spelled “count”.


(Karl) #123

I would say the best way to tackle that would be to provide both useful conformances as separate "views" to the underlying data - both the flat list of scalars and as a collection of rows/columns, even if we can't decide which (if any) should be the "default" conformance. This is exactly the kind of thing zero-cost abstractions are supposed to be useful for:

func someNestedCollectionAlgorithm<C: Collection>(_: C) 
  where C.Element: Collection, C.Element.Element == Int {
// ...
}

func someFlatCollectionAlgorithm<C: Collection>(_: C) 
  where C.Element == Int {
// ...
}

let matrix = Matrix4x4<Int>.identity

// View as a flat collection.
someFlatCollectionAlgorithm(matrix.scalars) // [1, 0, 0, 1]
// View as rows.
someNestedCollectionAlgorithm(matrix.rows) // [[1, 0], [0, 1]]
someFlatCollectionAlgorithm(matrix.rows[1]) // [0, 1]

I also think such a collection view would be useful for VectorN<T>. It's fair enough that Vector is not a Collection by default (we don't want most of that functionality showing up and polluting the namespace with non-SIMD operations), but you should at least be able to pass a vector to some existing algorithm written generically over any Sequence of Ints.

This design doesn't even include a way to copy the vector contents out to an Array. So if you need to use such an algorithm, you need to subscript each element individually and append it to an Array. Yuck:

let val = SIMDVector4<Int>(/* ... */)

// I hope you don't need a Vector32<T>...
var arr = Array<Int>()
arr.append(val[0])
arr.append(val[1])
arr.append(val[2])
arr.append(val[3])
someFlatCollectionAlgorithm(arr)

Some kind of collection view would solve both of these issues:

let val = SIMDVector4<Int>(/* ... */)

// Pass directly to a generic, non-SIMD algorithm.
someFlatCollectionAlgorithm(val.scalars)

// Copy contents to an Array.
let arr = Array(val.scalars)

// New way to spell "elementCount".
assert(val.scalars.count == 4)