Hello,
I have a lot of large arrays (matrices of floats, vectors of floats, vectors of StaticArrays of floats, vectors of StaticArrays of ints) in my code and calling functions is becoming cumbersome, as I have way too many parameters.
Are there any disadvantages, performance-wise in putting these large mutable arrays into a single struct and passing the struct to functions?
Not all functions need all the arrays, in case that matters.
Also, within the function, if I want to give the struct variable an alias, say b=a.b. Is there a good way to do that while avoiding allocations?
Thanks!
Passing along arrays as fields of a struct is fine, as long as the arrays are concretely typed.
Creating an alias for an array field is also fine because that does not create a copy. It’s just a name.
There is actually a neat way to do that with the Parameters package:
julia> using Parameters
julia> @with_kw struct MyStruct
x :: Int = 1
y :: Vector{Int} = [1,1]
end
MyStruct
julia> a = MyStruct() # create instance with default values
MyStruct
x: Int64 1
y: Array{Int64}((2,)) [1, 1]
julia> @unpack x, y = a # x and y will be bound to a.x and a.y here (no need to unpack all fields)
MyStruct
x: Int64 1
y: Array{Int64}((2,)) [1, 1]
julia> x
1
julia> y
2-element Array{Int64,1}:
1
1
I need them to be mutable, though. I didn’t know about named tuples, so thanks anyway =].
Indeed if I do a=[1,2,3]; @time b=a I get zero allocations, but if I do
struct tmp
tmp1::Vector{Int}
end
tmpa=tmp([1,2,3])
@time tmpb=tmpa.tmp1
I do get an allocation. It’s still a pointer (modifying tmpb changes tmpa, which I didn’t expect since this is not a mutable array, but I guess this is just another example of Arrays being special in Julia), but it allocates memory. Any ideas as to why?
That is neat! And doesn’t seem to allocate, so that’s a nice bonus. Thanks!
This is an artifact of the benchmarking happening in global scope. There will be no allocation in ‘real code’, i.e. code wrapped in type stable functions.
Well, usually you do not need mutable structures. Arrays are mutable on their own, so usually you do not need to keep them in mutable structure. You can find multiple ways to work with immutable fields in Mutable scalar in immutable object: the best alternative? and references therein, for example wrap them in Ref. And there is always Setfield.jl, which has its problems, but still can be used to solve many tasks.
As a reward, you’ll be able to change flexibly these data containers and use as many of them as necessary.
This isn’t about the struct at all; it’s just an artifact of you accessing the non-constant global variable tmpa. Doing a = b is always a no-op in Julia and never has any performance impact whatsoever. Variables are just labels, and assigning a new label to an existing value does exactly zero work.
You can try using BenchmarkTools, which allows you to use $ to interpolate a global value into the benchmark, avoiding the issue of benchmarking with globals:
Named tuples are immutable structures, just special version of them. So there are no performance gains to structs, only convenience. They go very nicely together with UnPack.jl
Yes, they are much better They are allocated on a stack, while mutable structs are allocated in a heap. It means, that all operations like creating/reading are much slower for mutable structures
For example
using BenchmarkTools
using Setfield
mutable struct Foo
a::Int
end
struct Bar
a::Int
end
Creation operation
function f1()
map(i -> Foo(i), 1:10000)
end
function f2()
map(i -> Bar(i), 1:10000)
end
@btime f1();
# 50.576 μs (10002 allocations: 234.45 KiB)
@btime f2();
# 8.269 μs (2 allocations: 78.20 KiB)
Reading operations
function g(v)
res = 0
for el in v
res += el.a
end
res
end
vfoo = f1()
vbar = f2()
@assert g(vfoo) == g(vbar)
@btime g($vfoo)
# 5.394 μs (0 allocations: 0 bytes)
@btime g($vbar)
# 689.520 ns (0 allocations: 0 bytes)
Even updating is faster with immutable structures
function h1!(v)
for el in v
el.a *= 2
end
end
function h2!(v)
for (i, el) in pairs(v)
@set! el.a *= 2
v[i] = el
end
end
@btime h1!(v) setup=(v = deepcopy($vfoo));
# 8.399 μs (0 allocations: 0 bytes)
@btime h2!(v) setup=(v = deepcopy($vbar));
# 5.959 μs (0 allocations: 0 bytes)
It’s a little more complicated to write updating function, but as you can see, it’s worth it.
Well, to be completely honest, while immutable structures are really nice and most of the times give you performance boost, it’s good to use common sense. If you create this structure once or twice per application and use it only at non-bottleneck places, then you shouldn’t care much what type of the structure you use, just choose something which is most convenient for the task at hand. There was really good blogpost regarding this, I recommend to read it: When do micro-optimizations matter in scientific computing? - Stochastic Lifestyle
Of course in a tight loop, immutable structures should be preferred to mutable if possible.
These variables will be used in all the loop levels of a computation intensive code, so optimization is important.
I was surprised when I saw that chard that if-positive conditions were much cheaper than if-negative ones. I tested it in Julia and it doesn’t seem to be the case. Anyway, it was a good read. Thanks!
I was writing a contract tracing covid19 individual based model last year. Each individual had a state, a possible contact, and several bits of information (time until incubation, time until recovery, flags for isolating, compliance etc.), and there were 10^4 individuals, each with around 10 fields.
I tested both immutable and mutable struct versions of the model, and found the mutable version to be slightly faster. I suspect this is because the data was just a large array of structs, which stayed put after it was created, so it was cheaper to modify structs than to replace them with new ones.
So, YMMV. Be aware that immutable structs are a potential optimisation, but do test it to confirm.
Alright, so I did some basic testing. Seems mutable or not makes only a tiny difference in my case (within repeatability). However, defining one of my vectors as Vector{StaticArrays.SVector{3,Int}}
vs Vector{Union{ StaticArrays.SVector{3,Int},StaticArrays.SVector{4,Int} }}
or Array{SArray{S,Int64,1,L} where L where S<:Tuple,1}
makes quite the difference (20-30% for the first, 2x for the second). The discussion on these definitions is here Struct variables explicit setting so I got some tips already.
So other than my crappy way of defining my variables, structs seem ok (and the same as passing vectors). I’ll still have to figure out how I can have a Vector with SArrays that can be 3 or 4 elements long.
Thanks, everyone.
Either split it in two vectors, Vector{SVector{3, Int}} and Vector{SVector{4, Int}} or add some sentinel value, when you need to generate SVector{3, Int}, effectively converting them to vectors of the length 4. For example @SVector [1, 2, 3, typemax(Int)] instead of @SVector [1, 2, 3] and just ignore last value in calculations.
If I am not mistaken, that is more or less bounded by the same size limits in which StaticArrays are useful (that is: " A very rough rule of thumb is that you should consider using a normal Array for arrays larger than 100 elements.").
For data structures larger than about that copying the data in the stack starts to be as costly as accessing the data by pointers in a mutable array or structure. I suppose here it is the same.
For this case I will likely split it in two. I do have another vector which is less critical, but which is a vector of StaticArrays varying from 1 (unlikely, but possible) to around ten or more (unlikely, but possible). So now I’m bummed out that such a vector sucks. Oh well.