# Mutable struct versus Base.RefValue

For structures that contain among other things a mutable scalar, I see (at least) two approaches:

1. Make the structure mutable
``````mutable struct S1
vector1::Vector{Float64}
vector2::Vector{Float64}
matrix::Matrix{Float64}
scalar::Float64
end
set_scalar!(a::S1, x) = a.scalar = x
get_scalar(a::S1) = a.scalar
``````
1. Use `Base.RefValue`
``````struct S2
vector1::Vector{Float64}
vector2::Vector{Float64}
matrix::Matrix{Float64}
scalar::Base.RefValue{Float64}
end
set_scalar!(a::S2, x) = a.scalar[] = x
get_scalar(a::S2) = a.scalar[]
``````

The first approach looks a bit nicer to read (even though I would have kept the structure immutable without the scalar). But based on the following silly benchmark, the second approach seems a tiny bit faster.

``````f(a) = get_scalar(a) * sum(a.vector1) + sum(a.vector2) + sum(a.matrix)
g!(a, s) = a.vector1 .*= s
function do_something(a)
b = prod(a.vector1) * prod(a.vector2)
b += get_scalar(a)
set_scalar!(a, .6)
g!(a, .4)
b *= get_scalar(a) + f(a)
g!(a, 1/.4)
set_scalar!(a, .4)
b
end

x1 = S1([1., 2.], [3., 4.], [1. 2.; 5. 0.], .4)
x2 = S2([1., 2.], [3., 4.], [1. 2.; 5. 0.], Ref(.4))
using BenchmarkTools
julia> @btime do_something(\$x1)
25.502 ns (0 allocations: 0 bytes)
398.20799999999997
julia> @btime do_something(\$x2)
22.462 ns (0 allocations: 0 bytes)
398.20799999999997
``````

What is your favourite approach for such structures? Should I expect the performance penalty of the first approach (`mutable struct`) to be very small most of the time?

1 Like

My worry is that 3ns could be caused by you computer doing other random things and the wrong time. You might want to run the benchmarks multiple times just to see if they are consistent or fluctuate by a small amount.

My general view is first write the code so it makes sense and is maintainable first. Then worry about improving the performance. So in that case I would go with the mutable structure. I also think in the long run itâ€™s not going to make much different. In the first case you will probably end up with an allocation in the heap for the whole structure, in the second you end up with a (smaller) allocation for just the Float64. I itâ€™s probably 6 of one half dozen of the otherâ€¦

4 Likes

The main difference should comes from whether the compiler can assume the field value never change and avoid loading them again. Since this really mainly happen when thereâ€™s non-inlined function calls that generally means that thereâ€™s something more expensive going on it should not matter much in general.

The difference could be significant when you use it in a loop that requires extensive compiler optimizations to vectorize. There are tricks that essentially amount to manually or helping the compiler doing LICM but those are very much case dependent at that point.

3 Likes

I donâ€™t know how it performs with your use case but thereâ€™s a package for â€ťmutatingâ€ť otherwise immutable structs.

1 Like