-
Notifications
You must be signed in to change notification settings - Fork 148
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
Extend getproperty
and setproperty!
with swizzling for vectors
#1221
Conversation
Note that performance should remain unaffected, so long as the julia> v4 = @SVector [1, 2, 3, 4]
4-element SVector{4, Int64} with indices SOneTo(4):
1
2
3
4
julia> @btime (v -> v.zx .+ v.zz .+ v.xx .+ v.y .+ v.yz .+ v.xx)($v4)
1.711 ns (0 allocations: 0 bytes)
2-element SVector{2, Int64} with indices SOneTo(2):
12
11
julia> @btime getproperty($v4, x) setup = x = :wwzy
89.658 ns (1 allocation: 48 bytes)
4-element SVector{4, Int64} with indices SOneTo(4):
4
4
3
2 The only thing I'm not so sure about is whether to keep the |
Any feedback on this, in favor or against this feature and/or regarding implementation details? |
I understand swizzling is common in some areas, but someone may want to avoid swizzling because they prefer to [w,x,y,z] instead of [x,y,z,w]. See JuliaGeometry/Rotations.jl#210 (comment) for example. I'm not sure how big the impact of this change is, and I would like to wait for some other reviews. |
Are there any situations which you really need |
Actually, the issue you linked seems to be a case that may have benefited from swizzling. The order
TL;DR: even if I could use another type to represent colors, it would harm performance not to use the efficient built-in vector type to do so. So what we'd end up with is reusing swizzling in the spatial domain like When writing shaders, one is expected to interact with most data using built-in types, and notably vectors, instead of user-defined structs. The reason for this is that built-in vectors are fast and have dedicated hardware operations. In an effort to write GPU shaders using Julia code, I replicated such built-in vector types here: https://github.com/serenity4/SPIRV.jl/blob/44de86dde135b1f172bbdcf9741af7e6b0b225a0/src/frontend/types/vector.jl along with operations that are found in most GLSL/HLSL shaders, including swizzling. The That works well and all, but it does not compose with the Julia ecosystem. Say I want to use a geometry processing library such as Meshes.jl to express a line-line intersection on the GPU. Meshes uses StaticArrays under the hood, such that shader code providing As StaticArrays are used in so many libraries nowadays, composing with code that uses them would greatly improve code reuse for numerical computations to be performed within GPU shaders. Therefore, I'd like to not introduce a new
I'd also wait for confirmation that this won't have an impact on the ecosystem, given how many dependents there are. We can reduce the likelihood of performance impacts by disallowing repeated components in swizzling, such as julia> using StaticArrays
julia> v4 = @MVector [10.0, 20.0, 30.0, 40.0];
julia> @code_typed getproperty(v4, :xyz)
[truncated]
⋮
15651 ┄ %36877 = $(Expr(:gc_preserve_begin, Core.Argument(2)))
│ %36878 = $(Expr(:foreigncall, :(:jl_value_ptr), Ptr{Nothing}, svec(Any), 0, :(:ccall), Core.Argument(2)))::Ptr{Nothing}
│ %36879 = Base.bitcast(Ptr{Int64}, %36878)::Ptr{Int64}
│ %36880 = Base.pointerref(%36879, 1, 1)::Int64
│ $(Expr(:gc_preserve_end, :(%36877)))
└────── goto #15652
15652 ─ goto #15653
15653 ─ return %36880
) => Union{Int64, NTuple{4, Int64}, MArray{S, Int64, 1} where S<:Tuple} whereas without repeated components this drops down to 5059 lines: julia> @code_typed getproperty(v4, :xyz)
[truncated]
⋮
2183 ┄ %5052 = $(Expr(:gc_preserve_begin, Core.Argument(2)))
│ %5053 = $(Expr(:foreigncall, :(:jl_value_ptr), Ptr{Nothing}, svec(Any), 0, :(:ccall), Core.Argument(2)))::Ptr{Nothing}
│ %5054 = Base.bitcast(Ptr{Int64}, %5053)::Ptr{Int64}
│ %5055 = Base.pointerref(%5054, 1, 1)::Int64
│ $(Expr(:gc_preserve_end, :(%5052)))
└───── goto #2184
2184 ─ goto #2185
2185 ─ return %5055
) => Union{Int64, NTuple{4, Int64}, MArray{S, Int64, 1} where S<:Tuple} but that's still a lot and we're very far from the smaller change of #980. If it may cause any issues in existing code, I'm ready to give up on this feature and try to find a compromise for my use case. However, if there is no reason to expect a noticeable impact due to the ability to const-propagate and optimize away all of the branches, then that would be great to have. For example, direct field access is optimized away like so: julia> @code_typed (x -> x.x)(v4)
julia> @code_typed debuginfo=:source (x -> x.x)(v4)
CodeInfo(
@ REPL[16]:1 within `#21`
┌ @ /home/serenity4/.julia/dev/StaticArrays/src/MVector.jl:73 within `getproperty`
│┌ @ /home/serenity4/.julia/dev/StaticArrays/src/MVector.jl:28 within `swizzle`
││┌ @ /home/serenity4/.julia/dev/StaticArrays/src/MArray.jl:21 within `getindex`
1 ─│││ %1 = $(Expr(:boundscheck, true))::Bool
└──│││ goto #6 if not %1
│││┌ @ abstractarray.jl:697 within `checkbounds`
2 ─││││ %3 = Core.tuple(1)::Tuple{Int64}
│ ││││ @ abstractarray.jl:699 within `checkbounds` @ abstractarray.jl:684
│ ││││┌ @ abstractarray.jl:758 within `checkindex`
│ │││││┌ @ int.jl:514 within `<=`
│ ││││││ %4 = Base.sle_int(1, 1)::Bool
│ ││││││ %5 = Base.sle_int(1, 4)::Bool
│ │││││└
│ │││││┌ @ bool.jl:38 within `&`
│ ││││││ %6 = Base.and_int(%4, %5)::Bool
│ ││││└└
│ ││││ @ abstractarray.jl:699 within `checkbounds`
└──││││ goto #4 if not %6
││││ @ abstractarray.jl:700 within `checkbounds`
3 ─││││ goto #5
││││ @ abstractarray.jl:699 within `checkbounds`
4 ─││││ invoke Base.throw_boundserror(x::MVector{4, Int64}, %3::Tuple{Int64})::Union{}
└──││││ unreachable
5 ─││││ nothing::Nothing
│││└
│││ @ /home/serenity4/.julia/dev/StaticArrays/src/MArray.jl:25 within `getindex`
6 ┄│││ %12 = $(Expr(:gc_preserve_begin, Core.Argument(2)))
│ │││┌ @ pointer.jl:270 within `pointer_from_objref`
│ ││││ %13 = $(Expr(:foreigncall, :(:jl_value_ptr), Ptr{Nothing}, svec(Any), 0, :(:ccall), Core.Argument(2)))::Ptr{Nothing}
│ │││└
│ │││┌ @ essentials.jl:555 within `unsafe_convert`
│ ││││┌ @ pointer.jl:30 within `convert`
│ │││││ %14 = Base.bitcast(Ptr{Int64}, %13)::Ptr{Int64}
│ │││└└
│ │││┌ @ pointer.jl:119 within `unsafe_load`
│ ││││ %15 = Base.pointerref(%14, 1, 1)::Int64
│ │││└
│ │││ $(Expr(:gc_preserve_end, :(%12)))
└──│││ goto #7
││└
7 ─││ goto #8
│└
8 ─│ goto #9
└
9 ─ return %15
) => Int64 which, although it seems fairly large, is actually the code for a single What I would therefore like to know for certain is whether this may be expected in general or if the large code size would, in certain circumstances, prevent such optimizations from being applied (such circumstances being for example that a parent function being compiled also has a large code size in a way that affects inlining decisions). |
Hey 👋 This is cool and I did think about supporting swizzling a long time ago (the ancient history of this library was in fact influenced by computer graphics short-vector libraries!). My main reservation is that swizzling is very domain-specific to computer graphics; perhaps too much so to be built into a general purpose library like However, implementing this as a The usage might look like this: u = SVector(1,2,3)
t = SVector(1,0,1,0)
@swizzle begin
v = u.zy + u.xy
e = t.arb
end The u = SVector(1,2,3)
t = SVector(1,0,1,0)
begin
v = swizzle(u, 3, 2) + swizzle(u, 1, 2)
e = swizzle(t, 4, 1, 3)
end This largely sidesteps the problem of a "standard name to index mapping" which @hyrodium mentions, because the ordering is opt-in via the use of |
Another interesting property of With that in mind it could even be in a separate library. But I've got some sympathy for having it here :) |
That's a fantastic idea, thank you @c42f! I wish I had thought of it before making this PR :) I think such functionality would fit better in an external package, so I just made https://github.com/serenity4/Swizzles.jl. I plan to have it registered soon when documentation is complete. Feel free to post any comments here or in an issue on that repo! |
What is implemented there is slightly different than your proposal above, in that EDIT: This is no longer the case as of serenity4/Swizzles.jl@1324794 (see commit message for more details). |
@serenity4 What a lovely little package! Very nice ❤️ |
Closes #1159, also adding appropriate
setproperty!
methods to be in line with theirgetproperty
counterparts.This improves the use of small static vectors in the context of computer graphics, where these swizzles are very common and super cheap on the GPU (which has dedicated hardware for that). Defining a
swizzle
method separately fromgetproperty
will allow GPU compilers such as SPIRV.jl to overlay that method with a specialized one that maps to a GPU intrinsic, e.g.OpVectorShuffle
in SPIR-V.Not only does expressing swizzles enable better performance, it also makes it easier for graphics programmers to code where many algorithms are written with swizzles of the sort, bizarre as that may seem. See this Reddit thread for a more in-depth explanation of why swizzling is used at all.
The color accessors (
.r
,.rgb
,.rgba
etc) are also useful where vectors refer to colors, also very common in computer graphics, where vectors are used for pretty much everything given how first-class they are in GPU shaders. Mixing spatial and color accessors doesn't really make sense, so things like.xgb
are disallowed. I would argue that these color accessors are a nice feature to have, although if there is strong resistance on that front this is less important as being able to swizzle at all (e.g. only using spatial accessors).Any comments welcome!