StaticArrays and allocations

Are there are any guarantees when using StaticArrays that the array values will indeed get stack-allocated? Or is it more like a best-effort guarantee and you may still end up with heap-allocated values?

Reason I’m asking is that I’m using StaticArray’s FieldVector and see allocations happening when checking with --track-allocation=user, e.g.

- mutable struct vec3 <: FieldVector{3, Float64}
        -     x::Float64
        -     y::Float64
        -     z::Float64
        -     
  4208416     vec3() = new(0, 0, 0)
  5435456     vec3(x, y, z) = new(x, y, z)
        - end

I suspect that because I’ve marked the struct as mutable that it still causes heap-allocation?

And in general is what --track-allocation=user reports the total bytes in heap-allocated values only? Again, I suspect “allocation” in the context of Julia to mean heap-allocation and not something like alloca() in C/C++ which allocates stack space.

2 Likes

No, Julia does not make any strong guarantees about whether data is allocated on the stack or the heap. In general, immutable objects are very likely to either be stack allocated or only live in registers. This usually does not hold for mutable objects though. There are some cases where allocations of mutable objects can be elided, but in general, Julia’s analysis in this regard is currently not very advanced. This is something that is likely to improve in the future, but you should generally prefer immutable objects, if you can avoid mutation, as this typically allows for more compiler optimizations.

2 Likes

I’m wondering why you want a mutable FieldVector?

You can use Setfield.jl/Accessors.jl to update the fields of a regular struct

I was trying to port some C++ code (and I’m much more comfortable with C++ anyway) and what I was trying to achieve was to have a stack-allocated struct that is still mutable. In C++ it makes sense to have simple values such as 3-vectors of floats on the stack in case they’re frequently created/destroyed, as stack allocation there is cheap and orthogonal to mutability. In Julia those two properties appear to be more intertwined, which takes a bit of getting used to.

Is const vec3 = MVector{3, Float64} an option? That’s what I’m doing (while implementing the Vulkan tutorial, which is originally C++).

Well, technically I’m doing const Vec{S} = MVector{S, Float64}, since I don’t like having more than one definition for basically the same thing. You can then use it like Vec{3}(x, y, z) and it’ll work and be inlined in arrays etc.

1 Like

Ah yes, that is easier in C++. We have to hack around immutability a little to get the same result.

Setfield.jl can set the field or equally use setindex! syntax:

julia> using Setfield

julia> struct Point{T} <: FieldVector{3,T}
           x::T
           y::T
           z::T
       end

julia> p = Point(1, 2, 3)
3-element Point{Int64} with indices SOneTo(3):
 1
 2
 3

julia> @set p[3] = 4
3-element Point{Int64} with indices SOneTo(3):
 1
 2
 4

julia> @set p.y = 7
3-element Point{Int64} with indices SOneTo(3):
 1
 7
 3

Yuck, so this works even without marking the struct as mutable?

Yeah thats the idea. There are also BangBang.jl and Flatten.jl to do related things with immutables.

ConstructionBase.jl facilitates all this. Some complex types need a custom ConstructionBase.constructorof method. Its not the prettiest setup but it works.

Note that the macro of Setfield that “mutates” is @set!:

julia> using StaticArrays

julia> x = @SVector [1,1,1]
3-element SVector{3, Int64} with indices SOneTo(3):
 1
 1
 1

julia> using Setfield

julia> @set x[1] = 2
3-element SVector{3, Int64} with indices SOneTo(3):
 2
 1
 1

julia> x
3-element SVector{3, Int64} with indices SOneTo(3):
 1
 1
 1

julia> @set! x[1] = 2
3-element SVector{3, Int64} with indices SOneTo(3):
 2
 1
 1

julia> x
3-element SVector{3, Int64} with indices SOneTo(3):
 2
 1
 1

But, @paulmelis, it is probably useful to think about these immutable objects as single values most of the time. This makes most code very readable. For instance:

julia> positions = [ @SVector rand(3) for i in 1:1000 ];

julia> velocities = [ @SVector rand(3) for i in 1:1000];

julia> function update_positions!(x,v,Δt)
         for i in eachindex(x)
           x[i] = x[i] + v[i]*Δt
         end
       end
update_positions! (generic function with 1 method)

julia> using BenchmarkTools

julia> @btime update_positions!($positions,$velocities,0.1)
  1.300 μs (0 allocations: 0 bytes)

Or, even simpler:

julia> update_positions!(x,v,Δt) = @. x = x + v*Δt
update_positions! (generic function with 1 method)

julia> @btime update_positions!($positions,$velocities,0.1)
  1.198 μs (0 allocations: 0 bytes)

which is furthermore slightly faster because inbounds is granted.

2 Likes

Neither of them mutate, @set! is just a convenience macro to rename the variable.

2 Likes

Sure, but the user experience is different.

Yes. immutable-ness of structs or SVector doesn’t mean that e.g. + isn’t defined or that operations with them will allocate (in fact, I’m willing to bet that type stable code with SVectors in it will not allocate, unless you fall back to regular arrays somewhere along the line). It really depends on what you want to do with them - maybe @paulmelis could give use some more information about what they’re trying to do?

My point is we shouldn’t suggest that they mutate because the key point for this thread is that they are guaranteed to always live on the stack. Mutables aren’t. I think its better if users know what they do and have a clean mental model.

4 Likes

Agreeing with you, an example to reinforce what you are saying:

julia> x = @SVector [1,1,1];

julia> f!(x) = @set! x[1] = 2
f! (generic function with 1 method)

julia> f!(x)
3-element SVector{3, Int64} with indices SOneTo(3):
 2
 1
 1

julia> x
3-element SVector{3, Int64} with indices SOneTo(3):
 1
 1
 1

They “look” mutable in the scope of the function. But they aren’t.

1 Like

I’m still not clear for which of the above forms the values are guaranteed to live on the stack? Are there cases where even SVector can end up being allocated on the heap?

Are there cases where even SVector can end up being allocated on the heap?

Yeah I was too strong with “guarantee”, but your Point object will certainly be on the stack, or just registers. I do wonder what circumstances an immutable object would be heap allocated…

The short answer is none of them are guaranteed to live on the stack (or actually to even live anywhere). One major advantage julia’s compiler has over c/c++ is that it makes far fewer guarantees about how it will implement your code. C and c++ compilers are forced to compile the actual code you wrote more often, even when there was a different way that would have gotten the same algorithm faster. Julia, on the other hand gives itself more room to change the details as long as it can prove you won’t notice the difference. One example of this is that it will often not store StaticArrays anywhere in memory, and just use a few registers instead. This is an optimization c won’t often do because for it to be valid, the compiler has to prove that you haven’t done something ugly like take the address of an integer that happens to refer to the memory location of the array.

1 Like

But this is a good lack of guarantee! don’t we all want our SArrays to just use registers?

The concern about guarantees is heap allocations… is there example of an SArray being heap allocated?

I think there’s a use for guaranteed stack-allocated, size-known-at-compile-time arrays (possibly from literals only?), though I agree that it’s kind of niche if you get used to not requiring them at all. Certainly requires a different kind of thinking if you’re very used to doing this.

To go into more detail how this could be done would require knowing more about what you’re trying to do/what the code you want to port does, @paulmelis

This is certainly a double-edged sword. As you get more proficient with C/C++ you will know pretty well when to use stack-allocated versus heap-allocated values and what the performance trade-offs are. So it indeed requires more effort to learn and manage, but overall is pretty transparent as the language mandates a lot of implementation details and the effects of the different types of allocations are somewhat predictable in terms of performance, certainly intuitively. So it also becomes more natural to make those trade-offs yourself to get what you want.

Julia’s handling of how values get allocated might also not be too complex, but the interaction with type-stability and related automatic boxing/unboxing muddies the water a bit it seems. And this is were “to change the details as long as it can prove you won’t notice the difference” comes in, as performance can be greatly influenced by too many allocations, which can be influence by type-instabilities, or by use of SVector versus a normal array, or by mutable versus immutable, etc. So before you even get to the clever optimizations that the compiler does it seems there’s a slew of different higher-level choices to be made which are far more important for resulting performance, but which are not really part of the core Julia language. So here the mental modal of e.g. allocations happening in your code appears to be quite a bit more complex. Even though the Julia compiler will do a lot more clever things it also becomes more opaque and less predictable IMO. Case in point are my questions on the StaticArrays types as noted above, but looking at their docs once more I really think no guarantees are given, so the only option is to try them and see what comes out? Btw, all this certainly has to do with far less experience I have with Julia compared to C++.

2 Likes