What you’re measuring here is essentially the cost of creating a new T
instance at each iteration, a cost that is negligible for immutable structures, and large for mutable structures. But presumably you want your structure to be mutable because you want to mutate it at each iteration, instead of creating a new instance at each iteration.
Let’s take your example, modified in such a way that the structures have two fields (so that it is easier to see the differences between mutation and instance creation:
struct S_i
x :: Int
y :: Int
end
function f!(v::Vector{T}) where T
s = 0
for i in 1:10^7
v[1] = T(i, v[1].y)
s += v[1].x
end
s
end
This performs well for a vector of immutable structures, in that it does not allocate:
julia> @btime f(v_i) setup=(v_i=[S_i(1, 42)]) evals=1
2.452 ms (0 allocations: 0 bytes)
50000005000000
Of course, you can use the exact same function with a mutable struct, but it allocates one new object for each iteration, and is therefore very inefficient:
julia> mutable struct S_m
x :: Int
y :: Int
end
julia> @btime f!(v_m) setup=(v_m=[S_m(1, 42)]) evals=1
44.628 ms (10000000 allocations: 305.18 MiB)
50000005000000
Fortunately, in this case, you can mutate the objects that are already stored in the vector:
function f_mut!(v)
s = 0
for i in 1:10^7
v[1].x = i # nicer syntax: we only want to mutate `x`; `y` is left alone
s += v[1].x
end
s
end
And it again avoids all allocations:
julia> @btime f_mut!(v_m) setup=(v_m=[S_m(1, 42)]) evals=1
3.469 ms (0 allocations: 0 bytes)
50000005000000
Note that we could have had a nice enough syntax in the immutable case, where we would only have to tell what to modify, and the unchanged fields would be taken care of automatically. Accessors.jl
is one way of doing this:
using Accessors
function f_accessors!(v)
s = 0
field_x = @optic _.x
for i in 1:10^7
v[1] = set(v[1], field_x, i)
s += v[1].x
end
s
end
julia> @btime f_accessors!(v_i) setup=(v_i=[S_i(1, 42)]) evals=1
3.968 ms (0 allocations: 0 bytes)
50000005000000
Note that all non-allocating versions have performances in the same ballpark. I wouldn’t try to look too much into the small performance differences observed in this case, since we’re only manipulating a 1-element vector. Larger performance differences may (or may not, I’m not sure) appear when manipulating large vectors.