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