Skip to content

Builtin vector properties #4961

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
data-man opened this issue Apr 6, 2020 · 12 comments
Open

Builtin vector properties #4961

data-man opened this issue Apr 6, 2020 · 12 comments
Labels
proposal This issue suggests modifications. If it also has the "accepted" label then it is planned.
Milestone

Comments

@data-man
Copy link
Contributor

data-man commented Apr 6, 2020

x, r == v[0]
y, g == v[1]
z, b == v[2]
w, a == v[3]

Or even user-defined:

const Color32 = @Vector(3, u32, "rgb")
const Vec2 = @Vector(2, f32, "xy")
const Vec3 = @Vector(3, f32, "xyz")
const Vec4 = @Vector(4, f32, "xyzw")
const Quaternion = @Vector(4, f32, "xyzw")
@daurnimator daurnimator added the proposal This issue suggests modifications. If it also has the "accepted" label then it is planned. label Apr 6, 2020
@ikskuh
Copy link
Contributor

ikskuh commented Apr 6, 2020

I would totally love to have vectors expose their elements as semantic fields, makes code a lot more readable.

I think the idea with user-defined vector fields is the way to go here as a color does not expose xyzw, a vector not rgba and uv coordinates do use uv instead of xy,

Maybe @Vector could either take a number as the first field or an array of field names:

const Generic = @Vector(3, u32);
const Vector3 = @Vector(.{ "x", "y", "z" }, f32);

@SpexGuy
Copy link
Contributor

SpexGuy commented Apr 6, 2020

I agree that this could makes code slightly cleaner, even ignoring aesthetics. combinedData[2] is definitely less maintainable than combinedData.velocity_x. However I have a few reservations about adding this to the language.

First, despite the name, SIMD vectors aren't extremely good at doing vector math when multiple channels are stored in a single vector. This effect is discussed more on Nathan Reed's Blog and in This talk by Andreas Fredriksson, but the short version is this:

You can see from these results that using naive SIMD didn’t help much even in the best case (64-bit world-to-camera), and actually hurt in the other cases. I’m not going to analyze the generated code closely, but basically what’s going on is that we have to spend too much time moving data around (often via memory) to get it into the right shape for SIMD vector operations to work on it. The overhead negates all the benefit from the 4-wide vector operations, and the scalar code is actually faster.

The better approach is to use SIMD to accelerate cases where, within a single vector, all channels have the same meaning. So instead of storing const Vector4 = @Vector(4, f32);, a better approach is to store values for 4 items together using something like this:

const Vector4x4 = struct {
    x: @Vector(4, f32),
    y: @Vector(4, f32),
    z: @Vector(4, f32),
    w: @Vector(4, f32),
};

Here, channel access is still important to extract the data for an individual if you need to do specialized processing, but the channels no longer need names since they all have the same meaning. Because of this, I don't feel that storing channels with separate meanings in a single SIMD vector is an important use case for the language.

Secondly, even when writing code that does have mixed-meaning, often the best path is to organize your vector code so that you never have to convert from vector->scalar->vector. This means that functions that convert vector->scalar (e.g. dot product) can usually be written better as a function that converts a vector to another vector with the scalar value across all four channels. Here's an example godbolt with dot product. I worry that making more ergonomic vector->scalar conversions will encourage more mixing of scalar and vector code, which is counter-productive if the goal of @Vector is to be a way to access SIMD functionality.

Finally, I think this feature would introduce some unnecessary complexity to the language. With this, instead of one unique vector type of 4 f32 values, a program could have a large number. Whether or not these should implicitly cast between each other is pretty analagous to the distinct types discussion (#1595), which is still quite controversial. The alternative, which I vastly prefer, is to wrap the vector in a struct that gives the correct operations. After all, @Vector is meant to be a mechanism for accessing SIMD, not a mechanism to get operator overloading in your vector math library. Something like this would be my choice:

struct Vec4 {
    simdVec: @Vector(4, f32),

    pub fn x(self: Vec4) f32 { return self.simdVec[0]; }
    pub fn y(self: Vec4) f32 { return self.simdVec[1]; }
    pub fn z(self: Vec4) f32 { return self.simdVec[2]; }
    pub fn w(self: Vec4) f32 { return self.simdVec[3]; }
};

I think my opinions here come from this distinction:

To me, @Vector is:

  • A cpu-independent mechanism to generate simd instructions
  • A way to get nicer aliasing reasoning out of the compiler

@Vector is not:

  • A color, spacial vector, or any other bundle of specialized data
  • A way to get operator overloading on your data type
  • A way to get distinct types

@data-man
Copy link
Contributor Author

data-man commented Apr 6, 2020

pub fn x(self: Vec4) f32
...

It's readonly.

Also zig zen wrote:

  • Favor reading code over writing code.
  • Minimize energy spent on coding style.

@SpexGuy
Copy link
Contributor

SpexGuy commented Apr 6, 2020

Easily remedied:

pub inline fn set_x(self: *Vec4, newValue: f32) void { self.simdVec[0] = newValue; }
pub inline fn set_y(...

I too can quote zig zen:

  • Only one obvious way to do things.
  • Communicate intent precisely.

But that's besides the point. @Vector is a base building block to me, like u32 or [4]i32. You wouldn't really want to do const FloatColor = [4]f32; in most situations, and const FloatColor = @Vector(4, f32); should be the same. If you want to attach meaning to the data fields, I think that should be done via a struct, functions, or both.

@data-man
Copy link
Contributor Author

data-man commented Apr 6, 2020

v.x += delta
vs
v.set_x(v.x + delta)
What more readable/lesswritable?

@SpexGuy
Copy link
Contributor

SpexGuy commented Apr 6, 2020

You could alternatively do this:

const SimdF4 = @Vector(4, f32);

const Vec4 = extern struct {
    x: f32 align(@alignOf(SimdF4)),
    y: f32,
    z: f32,
    w: f32,

    pub fn toSimd(self: Vec4) SimdF4 {
        return @bitCast(SimdF4, self);
    }

    pub fn fromSimd(vec: SimdF4) Vec4 {
        return @bitCast(Vec4, vec);
    }
};

Here's a godbolt showing that it generates the correct instructions. This allows you to do v.x += delta, and it has the added benefit of explicitly acknowledging when you make the switch between scalar and simd code.

@SpexGuy
Copy link
Contributor

SpexGuy commented Apr 6, 2020

Here's another example showing that the compiler is smart enough to track the value through the @bitCast and avoid memory operations: https://godbolt.org/z/AMmU5t . It also makes clear all of the hidden stuff the compiler has to generate in order to access a channel in the middle of a simd vector.

@andrewrk andrewrk added this to the 0.7.0 milestone Apr 6, 2020
@data-man
Copy link
Contributor Author

data-man commented Apr 6, 2020

Oh, I too can invent tricks :)

const std = @import("std");
const warn = std.debug.warn;

const Vec2 = struct {
    v: @Vector(2, f32),

    pub fn @"x+="(self: *@This(), delta: f32) void {
        self.v[0] += delta;
    }

    pub fn @"y+="(self: *@This(), delta: f32) void {
        self.v[1] += delta;
    }
};

pub fn main() !void {
    var v: Vec2 = undefined;

    v.@"x+="(1.0);
    v.@"y+="(1.0);

    warn("v = {}\n", .{v});
}

@BarabasGitHub
Copy link
Contributor

Now people are just being silly. As @SpexGuy explained, a vector is not a vector in the mathematical sense. And you shouldn't treat it as such. Also accessing the separate elements of a vector is not something you want to be doing anyway. It's exactly the opposite of it's purpose.

@data-man
Copy link
Contributor Author

data-man commented Apr 8, 2020

"Geniuses" should learn to add "IMHO" to each phrase.

@andrewrk
Copy link
Member

andrewrk commented Apr 8, 2020

What is going on here? Please let us keep the conversion technical and remember that we are all working towards the same goal.

@ghost ghost mentioned this issue Apr 22, 2020
@ghost
Copy link

ghost commented Apr 28, 2020

With typedef, #5132:

// fields split by `,` must have length equal to the base array length
// base type must be an array or vector of a primitive type
const Color = typedefprimitive([4]u8, .NamedArray{.fields="r,g,b,a"});
const Pos3d = typedefprimitive([@Vector(3,f64), .NamedArray{.fields="x,y,z"});

test "" {
  const color : Color = @as(Color,.{0,1,2,3});
  assert(color[0] == color.r); // syntax sugar applied at comptime
  assert(color[1] == color.g);
  assert(color[2] == color.b);
  assert(color[3] == color.a);

  const arr : [4]u8 = @as([4]u8, color); // equivalent at runtime
}

This would rely on typedef primitives (typedefprimitive) being allowed to have custom properties, in the same way that "normal" primitive types can have them. It also relies on typedef triggering comptime validation behavior. See the appropriate sub sections in #5132 for more details.

(Normal typedefs (typedef) should not be allowed to add properties, but can disable/hide properties and fields for encapsulation purposes)

With respect to coercion, I think named arrays should behave like typedef distincts. The named array auto coerces down to the base type, but going from the base to the named array requires a cast with @as(,).

const arr : [4]u8 = color; // ok. auto coerce
const color2 : Color = arr; // compile error

@andrewrk andrewrk modified the milestones: 0.7.0, 0.8.0 Oct 27, 2020
@andrewrk andrewrk modified the milestones: 0.8.0, 0.9.0 May 19, 2021
@andrewrk andrewrk modified the milestones: 0.9.0, 0.10.0 Nov 23, 2021
@andrewrk andrewrk modified the milestones: 0.10.0, 0.11.0 Apr 16, 2022
@andrewrk andrewrk modified the milestones: 0.11.0, 0.12.0 Apr 9, 2023
@andrewrk andrewrk modified the milestones: 0.13.0, 0.12.0 Jul 9, 2023
@andrewrk andrewrk removed this from the 0.14.0 milestone Feb 9, 2025
@andrewrk andrewrk added this to the 0.15.0 milestone Feb 9, 2025
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
proposal This issue suggests modifications. If it also has the "accepted" label then it is planned.
Projects
None yet
Development

No branches or pull requests

6 participants