zig icon indicating copy to clipboard operation
zig copied to clipboard

SIMD vector type syntax: [|N|]T

Open andrewrk opened this issue 3 years ago • 44 comments

Currently we have @Vector for this, however, see #5207 and #6209.

Array syntax is [N]T. This is a proposal for SIMD vector syntax to be [|N|]T instead of @Vector(N, T). For example, a vector of four 32-bit integers would be [|4|]i32.

The main motivation for this would be that the compiler needs to be able to talk about primitive types in type names and in compile errors. Without syntax for this primitive type, in order to do this the compiler would introduce a dependency on the std lib such as std.meta.Vector(4, i32) which is verbose and can make compile errors and types more difficult to read at a glance, or it would have to do something like @Type(.{.Vector = .{.len = 4, .child = i32}}) which is even more verbose, making people wonder whether simd vectors really are first-class types in zig after all.

I chose | because it is already associated with bitwise operations, and because it looks OK when symmetrically positioned against the [ and ].

Related:

  • Add SIMD Support #903

andrewrk avatar Oct 22 '20 23:10 andrewrk

The main motivation for this would be that the compiler needs to be able to talk about primitive types in type names and in compile errors.

Note @typeInfo(@typeInfo(@TypeOf(%s)).Fn.return_type.?).ErrorUnion.error_set is already used for inferred error sets.

I think syntax like v4f32 or f32x4 is easier to read and much better for the common case of non-pointer vectors. @Type/std.meta.Vector is available for any others.

tadeokondrak avatar Oct 23 '20 00:10 tadeokondrak

I think syntax like v4f32 or f32x4 is easier to read and much better for the common case of non-pointer vectors.

I like the v<N><Type> syntax, it feels a natural extension of the usual scalar type syntax. On top of that we should offer a set of "sane" combinations of N and T in std.simd that make sense for the hardware and avoid people running face-first into a (performance) wall.

LLVM covers your ass when working on non-canonical (where T is not i/u{1,8,16,32,64,128}) but every single load/store/op done on such vectors is slow AF since the hardware has no native support for such wonky-sized vectors and the generated code scalarizes, performs the requested operation, masks off the unwanted bits and then re-packs the vector.

LemonBoy avatar Oct 23 '20 08:10 LemonBoy

I also think we should add a native syntax, although i prefer TxN, so f32x4 is more readable imho, as it still conveys type information first (it's a f32, but 4 of them). But i can understand why v4f32 is preferrable, as it follows the zig array decl: [4]f32 and v4f32 are similar.

another option would be <4>f32, but this would introduce ambiguities

ikskuh avatar Oct 23 '20 08:10 ikskuh

I think that f32x4 is more readable and easier to write.

michal-z avatar Oct 23 '20 11:10 michal-z

The problem with v4xf32 and f32x4 though is that the compiler still needs to generate calls to std.meta.Vector (or a long chain of builtin calls like with error set returns).

Snektron avatar Oct 23 '20 11:10 Snektron

After some discussion with @Snektron we came up with another idea, utilizing already existing features and make people remember less syntax (assimg @Vector(4, f32)):

  • ~~[|4|]*f32~~
  • ~~*f32x4~~
  • ~~v4*f32~~
  • ~~<4> *i32~~
  • ~~4 ** *i32~~
  • *i32 ** 4

Just use the ** operator for comptime repetition to also lift types from scalar to vector type:

T ** N == @Vector(N, T) == std.meta.Vector(N, T)

ikskuh avatar Oct 23 '20 12:10 ikskuh

The problem with v4xf32 and f32x4 though is that the compiler still needs to generate calls to std.meta.Vector (or a long chain of builtin calls like with error set returns).

Why? Once that syntax is adopted for all the Vector types the compiler is free to use that syntax as well. std.meta.Vector returns v4f32 (or f32v4) vectors with this proposal.

LemonBoy avatar Oct 23 '20 13:10 LemonBoy

Why? Once that syntax is adopted for all the Vector types the compiler is free to use that syntax as well. std.meta.Vector returns v4f32 (or f32v4) vectors with this proposal.

This syntax doesn't allow for vectors of pointers, or vectors of some aliased type. I probably should have clarified in my original comment, sorry about that.

Snektron avatar Oct 23 '20 13:10 Snektron

This syntax doesn't allow for vectors of pointers, or vectors of some aliased type.

I always forget of the vectors of pointers, thanks for reminding me.

LemonBoy avatar Oct 23 '20 13:10 LemonBoy

I very strongly disapprove of **. Firstly, on values that's an array operator, so it's easily confused; secondly, a SIMD multiplier would then be the only postfix type modifier, and we'd have C-style spiral precedence. Packed-native format is also terrible, because it only covers a subset of valid use cases, so it doesn't actually eliminate any human memory overhead.

A strictly regular type modifier is a necessity in my eyes, and since we don't have a modifier application operator (and we absolutely should not ever add one), another variation on bracket syntax seems like the best option. Andrew's original proposal fits that, as well as being "augmented" enough that it's clear something else is going on.

ghost avatar Oct 23 '20 13:10 ghost

I dont like any of the proposals, bars, x'es, stars don't look good and also confusing, one looks like an array and others like an identifier. I'm fine with status quo, whenever i use vectors i make aliases. If we really really need syntax for vectors i guess this is the least confusing, since it similar to const array ptr: [4]vec i32

Rocknest avatar Oct 23 '20 15:10 Rocknest

I know we removed it but personally I think @Vector(N, T) is clearer than any of these.

SpexGuy avatar Oct 23 '20 17:10 SpexGuy

[4]vec i32 looks nice. Though maybe it should be [4]simd i32? To more explicitly signal that it is an array-like object that is meant specifically for SIMD processing.

I know this is a bit off topic, but "vector" is such an overloaded term in computing, and usually little to do with the original mathematical term to boot.

zzyxyzz avatar Oct 23 '20 17:10 zzyxyzz

[N]vec T is inconsistent with the array syntax, here vec applies to the whole thing while other modifiers such as const affect the type. If you want to stretch this syntax you could use something like [N]lane T that makes sense from the simd point of view.

LemonBoy avatar Oct 23 '20 18:10 LemonBoy

@LemonBoy, could you clarify? I was thinking of [N]vec as an atomic modifier just like const, [N:0] or anything else.

zzyxyzz avatar Oct 23 '20 18:10 zzyxyzz

Some more syntax variants on a slightly more complex example:

[w][h][4]vec f32

[w][h][4v]f32

[w][h][4 simd]f32

[w][h][|4|]f32

[w][h]@Vector(4, f32)

All of the bracket-based variants have the disadvantage that they only make sense on the inner-most array (you can't really have [w][|4|][h]f32), which is a bit inconsistent. In light of that, I would agree with @SpexGuy that the old @Vector syntax is still best in many cases.

zzyxyzz avatar Oct 23 '20 18:10 zzyxyzz

@LemonBoy, could you clarify? I was thinking of [N]vec as an atomic modifier just like const, [N:0] or anything else.

[N]const T is a N-element array of const T values, the whole array is transitively constant too. *const T is a pointer to a constant value. Following this logic [N]vec T is a N-element array of vector T (??), hence my suggestion to use the term lane as [N]lane T means a bundle of N lanes of width equal to the one of T.

LemonBoy avatar Oct 23 '20 18:10 LemonBoy

Yeah, I guess the associativity is backwards in this case :smile:

zzyxyzz avatar Oct 23 '20 18:10 zzyxyzz

Following this reasoning the modifier could simply be placed right of the array: simd [N]T

Snektron avatar Oct 23 '20 19:10 Snektron

[4x]T anyone?

zzyxyzz avatar Oct 23 '20 19:10 zzyxyzz

Delurking for a minute.

Is there any projected impact on Zig's use of SIMD vectors from things like Arm's SVE? The examples I have seen of what compilers can do to automatically vectorize normal arrays using tools like SVE and RISC-V's V extension are quite impressive.

kyle-github avatar Oct 23 '20 20:10 kyle-github

Interesting question. From my superficial understanding of ARM-SVE, it represents a very significant departure from the SIMD paradigm. It is designed to operate directly on large arrays with runtime-known length, rather than manually partitioned fixed-sized chunks. In particular, array length does not need to be a multiple of the native vector size, thanks to the ability to load and operate on incomplete vectors. SVE also relies heavily on a separate bank of predicate registers that don't have a direct counterpart in traditional SIMD.

My cautious conclusion would be that SVE is not urgently relevant to the present bikeshedding session, since we are discussing syntax sugar for a fixed-width SIMD data type. It should also be kept in mind that the availability of SVE-supporting commodity hardware is still pretty much zero (I'm not going to count the Fujitsu A64FX), so introducing special syntax for it may be premature. All in all, it would probably be best to extract this question into a separate issue.

zzyxyzz avatar Oct 24 '20 12:10 zzyxyzz

Availability, yeah, that is an issue today, but probably not within a year or so. Even availability of Arm servers has gone from zero to lots with AWS being so cheap for Graviton instances. Arm is clearly pushing (we'll see what NVidia does) SVE/Helium everywhere in their next generations of cores. Everything is going to have some form of VLA (variable length array) support.

It was precisely these facts that made me wonder a bit if Zig was skating to where the puck is today and not where it will be in a few years:

  • VLA extensions toss out a lot (most?) of the pain of dealing with SIMD.
  • Arm is used in supercomputers, cloud instances (AWS), and will be in mainstream laptops shortly (Apple), as well as the usual area of phones where it has more than 99% market share.
  • RISC-V is not mainstream yet, but gaining surprising wins and there is only one accepted SIMD extension and that is VLA too.

Where is x86 in this? No idea but with Arm now entering into the Supercomputer 500 list due to SVE... There are so many, many advantages to VLA support.

But I agree that this is a different discussion point. Sorry for the diversion! I am really excited about VLA support in CPUs because of the ability to write code once that just works across a large range of hardware and it means far less support for Intel's idiotic market segmentation by ISA version (try to figure out which AVX512 instructions are supported on which processor!).

I'll go back to lurking :smiley:

kyle-github avatar Oct 24 '20 15:10 kyle-github

@andrewrk There is an ambiguity in the proposed syntax: if the length is the result of a bitwise or, the lexer will need to look ahead to know that the pipe does not pair with a close bracket. We don't have this problem with captures because they can only be alphanumeric, but integers can be arbitrary expressions. We could potentially make use of the unused #, $ sigils, but that would be ugly.

Re: VLA, I think our fixed-length paradigm can be adapted, if we relax the requirement of corresponding strictly to hardware SIMD, like we already do with integers. So, we have a vector corresponding to the size of our problem, which we can make as big as we like, and then the compiler is free to split it up into appropriately-sized chunks. Lane predication could be handled by vectors of bool and overloading of index syntax.

ghost avatar Dec 05 '20 08:12 ghost

There is an ambiguity in the proposed syntax: if the length is the result of a bitwise or, the lexer will need to look ahead to know that the pipe does not pair with a close bracket. We don't have this problem with captures because they can only be alphanumeric, but integers can be arbitrary expressions. We could potentially make use of the unused #, $ sigils, but that would be ugly.

Adding a separate token for [| and |] would solve that.

Snektron avatar Dec 05 '20 16:12 Snektron

Both this and #1974 are talking about the same issue: numbers formats hold a lot of information. We kind of want some sort of compositional syntax that's easy to read and easy to type, if only for ease of standard communication about boilerplate.

While I have no proposal for the optimal arrangements of symbols, the most obvious solution is to just literally describe the data in a way akin to the formatter syntax. For integers, something like i/u|integerbits|.|fractionalbits|x|lanes|, i.e. i32.32x4 For floats, something like f|signbit|_|exponent|_|fraction|x|lanes|, i.e. f1_8_23x4

This makes nobody happy, and I'm just going to use the equivalent of const Worldspace = FixedPoint(.{ .signed = true, .integer_bits= 16, .fractional_bits = 16, .simd_width = 8}); anyway, but I feel this commonality between issues is worth pointing out.

ghost avatar Dec 16 '20 10:12 ghost

It's strange, but when I think about it, if all the builtin number type aliases were removed I don't think I'd miss anything.

ghost avatar Dec 16 '20 10:12 ghost

What about [[N]]T ?

tealsnow avatar Jan 13 '21 10:01 tealsnow

The main objection against the otherwise popular f32x4 seems to be that it cannot express vectors of pointers. But since this is the only "non-standard" case that needs to be supported, maybe we could simply special-case it? E.g. with f32p4 or f32x4p.

zzyxyzz avatar Feb 15 '21 16:02 zzyxyzz

The question becomes then what the range of pointee types is that a vector can consist of. If its the same range as the regular primitives, then i suppose that could work. Consider a hypothetical instruction that simply performs a gather though, then that could also be used on regular types:

const T = struct { a: i32, b: i32 };
fn gatherBs(vec: Vector(N, *T)) Vector(N, i32) {
  return vec.b;
}

Snektron avatar Feb 16 '21 11:02 Snektron

I'm maybe a bit late on the subject, but I was totally fine with @Vector (except for the potential confusion with std.Vector). I don't think it is really worth to have a "symbol" syntax for it, but if I had to chose, I would go for something like [vec 4] u32 or [% 4] u32.

I would advise against [~4] as ~ is a unary operator on integers so it might appear on slice declarations at comptime.

We could have convenience aliases like u32x4 or v4u32 but those should not be the sole way to access them because of metaprog. I want to be able to create a SIMD type of N u32 where N is a comptime value, but not a literal.

All in all, my most wanted syntax is @simd(u32, 4) which is explicit, unambiguous, clear and rather small.


Side note on SVE. The size of SVE registers is not dynamic: all SVE registers have exactly the same size for at least the whole execution of the process. It is a runtime constant. I have explained a bit more SVE on this comment

We could probably say that @simd(u32) (or [vec] u32) is a SVE-like SIMD type. But I'm also fine with just having access to SVE via intrinsics.

lemaitre avatar Apr 16 '21 08:04 lemaitre

Regardless of what syntax is chosen, I think that it is imperative that a solution is chosen. Andrew's comment in the OP is spot-on: as it stands, vectors are a second-class type. Vectors are an exception to the rule,"use the appropriate type-specific syntax to declare a specific type" (better phrasing welcomed). "If you're declaring a vector, use an intrinsic; otherwise, use the appropriate syntax" is inherently problematic, and it makes the language feel less polished overall.

Personally, I think [|N|] is already a reasonable syntax. [] in Zig does not refer to a specific type; we use them for all multivalue-types, at present, even pointers-to-many!

[*] pointer-to-many
[] slice
[N] array
[|N|] vector

This may not be perfect, but it is consistent. Compare it to this list:

[*] pointer-to-many
[] slice
[N] array
@Vector(T, N)

Even if [|N|] isn't perfect, it's still a major improvement.

pixelherodev avatar Jun 27 '21 09:06 pixelherodev

  1. I think [|N|] hurts readability, since [| are 2 nearly completely vertical characters. Depending on font and editor configuration, this is very bad to read.
  2. Further this does not give visually information of what could be meant. An array with a number of elements |N| would be the intuitive meaning for me.

To fix both shortcomings I would propose something with [-N-]:

  1. It uses more horizontal space next to a vertical spaced character for optimal readability. One does not need a font with large horizontal space between characters or big character margins. And one does not need code highlighting to grasp meaning, when quickly checking some code.
  2. One can intuitive grasp meaning from this: [] indicates that the memory region is continuous. -type- indicates that the type makes up the whole line (continuous memory). So [-type-] must be "the type that must be operated at once".

~~What I am less confident about is whether 4[-i32-] or [-4-]i32 or even 4x[-i32-] is more readable and intuitive. Having (number of rows, number of columns) is intuitive and I would favor making "all columns must be processed at once" clear. So I would favor 4[-i32-].~~ @pixelherodev convinced me that its worth to favor consistency, so [-N-]T which translates to [-4-]i32 sounds better.

However:

  1. This could have unintended consequences on array slicing extensions like here.
  2. A special symbol after the number might also be simpler in the long-term, when SIMD or array operations become more complex.

So all in all I would prefer a decision, once the supported surface of array operations can be made to prevent any unintended side effects for usability. And once it is decided (ideally from experience) if and what further requirements are necessary to annotate on SIMD types.

matu3ba avatar Jun 27 '21 10:06 matu3ba

I'd go with

[-N-]T

personally, for the same reasons of consistency I outlined earlier.

[*]T pointer-to-many
[]T slice
[N]T array
[-N-]T vector

vs

[*]T pointer-to-many
[]T slice
[N]T array
N[-T-] 

pixelherodev avatar Jun 27 '21 10:06 pixelherodev

I would like to highlight an argument against consistency.

Pointers and slices are "decorators" of any types, even user defined ones. However, SIMD should most likely be restrained to primitive types (uX, iX, fX, and maybe to pointers and slices). So giving a syntax close to pointers and slices could make people believe they can use it for any types, even their own. Giving a totally different syntax (like @simd(T, N) or i8x16) will make it explicit that it cannot be used by custom types.

The reason that SIMD should most likely not be defined on custom types is what would be the meanings of methods on those. Methods could be forbidden, but it would make the resulting type a bit useless. Methods could be kept, but with what semantics?


All in all, I'm not saying we should stay away from a consistent syntax, just that we need to be careful with it because of this distinction.

lemaitre avatar Jun 28 '21 09:06 lemaitre

That is a good point. However, I think the advantage of consistency is more important, regardless. If I attempt to, say, make a vector of a structure, the compiler will reject it, and it will be clear that is not allowed. Moreover, anyone using vectors should by necessity understand how they work anyways - the documentation should render "vectors can only be made of primitives" clear, so it shouldn't be a concern.

That said, making it more immediately obvious has clear benefits as well. If a different syntax is desired, that is reasonable - however, using builtins is still a horrid solution, since it continues to leave vectors as second-class types.

pixelherodev avatar Jun 29 '21 00:06 pixelherodev

We're definitely going to have SIMD vector syntax, and get rid of std.meta.Vector as well as @Vector. The only question is what color the bikeshed should be.

andrewrk avatar Dec 19 '21 04:12 andrewrk

how about [^N]T

haoyu234 avatar Dec 20 '21 23:12 haoyu234

Is this open to more bikeshedding? If so, then I'll throw mine out there: [simd N]T.

InKryption avatar Dec 21 '21 01:12 InKryption

I suggest Vect N T like Idris2 :D

kenaryn avatar Dec 22 '21 17:12 kenaryn

I like @Vector(T, N) or [[N]]T

nektro avatar Dec 25 '21 20:12 nektro

One interesting thought:

Layout-wise, vectors are what you get when you pack arrays without padding and store them in integers. In this sense, they are just like integer-backed structs (#5049). Maybe packed(u256) [8]f32, or simply packed [8]f32?

Notice for both packed [N]T and packed struct:

  • Elements are laid out contiguously from LSB to MSB
  • They can be expected to be held in machine registers (if small enough)
  • They have increased alignment, corresponding to their backing integer
  • They can bitcast to/from their backing integer type
  • They cross C ABI boundaries like their respective backing integer

To my knowledge, all of this is already true about @Vector(N,T) (except for bitcasts for which Zig is overly strict right now, and reversed indexing on big-endian systems). It's just not at all obvious from the existing syntax.

topolarity avatar Jul 14 '22 22:07 topolarity

(except for bitcasts for which Zig is overly strict right now)

Note that @ptrCast()ing can be used to work around this and is currently the only way I know of to get from e.g. a @Vector(16, bool) to a often more useful u16. I'm not sure if this is 100% intended in the Zig language design but the stage1 generated LLVM IR is valid and does what I want.

ifreund avatar Jul 14 '22 22:07 ifreund

A natural extension would be to allow packed [N]T in a packed struct

That would give us back arrays in packed structs, which are currently unsupported under #5049

topolarity avatar Jul 15 '22 00:07 topolarity