Performance disadvantages of structs?

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!

1 Like

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.

4 Likes

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


1 Like

Probably you do not even need special structure or structures. Just use named tuples like
(x = 1, y = [1, 1]) and pass them around.

1 Like

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.

You can still mutate arrays inside a tuple.

5 Likes

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.

1 Like

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:

julia> using BenchmarkTools

julia> struct tmp
         tmp1::Vector{Int}
       end

julia> tmpa=tmp([1,2,3])
tmp([1, 2, 3])

julia> @benchmark tmpb = $tmpa.tmp1
BenchmarkTools.Trial: 
  memory estimate:  0 bytes
  allocs estimate:  0
  --------------
  minimum time:     1.412 ns (0.00% GC)
  median time:      1.419 ns (0.00% GC)
  mean time:        1.457 ns (0.00% GC)
  maximum time:     8.787 ns (0.00% GC)
  --------------
  samples:          10000
  evals/sample:     1000
2 Likes

@rdeits Ah, ok. Thanks!
@DNF @Skoffer Any advantages, performance-wise, in using named tuples vs structs vs mutable strucs?

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

3 Likes

And are immutable structs somehow better than mutable structs?

Yes, they are much better :slight_smile: 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.

7 Likes

Oh, wow. Thanks a lot for the thorough examples.
Immutable structs it is!

1 Like

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.

2 Likes

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!

1 Like

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.

2 Likes

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.