structs in Julia are not mutable (for that, you want a mutable struct. Also, Julia uses Pass By Sharing, so test_function takes an s, creates a new s and returns it. Assuming TestStruct was mutable, you would want. s.a = 2 which changes the value of s.a instead of creating a new object for s
Yes, I know. Unfortunately, the mutable struct is very slow. Let me reformulate my question: Can I mutate only array in Julia via function? The problem really is that mutable struct is slow and if I want to mutate some pack of data and do not want to use return because it is additional cost.
Setfield is nice, but no faster than doing it manually, itâs just more convenient notation.
Mutable structs should not be âvery slowâ, maybe not quite as fast as an immutable struct, but if itâs âvery slowâ, I suspect something else is wrong.
Anyway, if you are mutating the array inside the struct, that is only possible because the array itself is mutable.
This is a benchmarking problem. The mutable struct gives you a million updates in 400 Îźs. Thatâs 0.4 ns per update, corresponding to 1 update per clock cycle on a 2.5 GHz processor. Those who know more details about CPU architecture can say whether this is good or bad but at least itâs in the right ballpark and can hardly count as slow.
The 24 ns for a million supposed updates of an immutable struct is, on the other hand, completely unreasonable. Nothing happens that fast on a CPU. With all certainty the compiler has been smart enough to see that the code doesnât actually produce anything and has optimized away nearly everything.
That benchmark is highly artificial and gives a false impression. It also shows why you should not write loops like that (artificial loops added for timing purposes) into benchmarks. BenchmarkTools already does that for you.
In fact, this function does zero work, because it doesnât even return the new value, so it can just be removed completely: Sorry, this was wrong. It does actually return the last value in the for loop.
The actual context is finite element grid. I am working on computational fluid dynamics project with fluid-structure interaction capability, i.e., moving grid, i.e., mutating data in struct. I was trying to find the right way to store data.
Can I say that I really dislike this implicit return behaviour? I wish explicit return statements were required, at least for âlong-form functionsâ.
Thanks, I will check BenchmarkTools.jl. I probably overthinking the problem anyway. I will stick with mutable struct for larger collections of data and struct for smaller, e.g. Point.
Sure, sorry, I just wanted to point out that the compiler will in general be able to do more optimizations with immutable types.
Concerning the original post:
A natural way to obtain what you originally wanted here is simply to return a copy of the value instead of trying to modify it:
julia> function test_function(s)
s2 = TestStruct(2,s.b)
return s2
end
test_function (generic function with 1 method)
julia> a = TestStruct(1,rand(1_000));
julia> a = test_function(a)
TestStruct(2, ...
You might think: But then I am just copying the struct! Indeed you are, but this is fast and allocation free (edit: actually not true if the field itself is mutable, which is the case here), because immutable structs are generally stored in the fast stack memory. This is no different from doing
x = 1
x = 2*x
except that your struct has more fields than x.
I understand that @set does no miracle here, it just conveniently does that copy without one having to define the structure explicitly. And there is no gain in performance:
julia> function change_vec1!(vec)
for s in vec
s = TestStruct(2*s.a,s.b)
end
end
change_vec1! (generic function with 1 method)
julia> function change_vec2!(vec)
for s in vec
@set s.a = 2*s.a
end
end
change_vec2! (generic function with 1 method)
julia> vec = [ TestStruct(rand(1:10),rand(1_000)) for i in 1:10_000 ];
julia> @btime change_vec1!(vec)
5.036 Îźs (0 allocations: 0 bytes)
julia> vec = [ TestStruct(rand(1:10),rand(1_000)) for i in 1:10_000 ];
julia> @btime change_vec2!(vec)
5.081 Îźs (0 allocations: 0 bytes)
edit: these benchmarks are wrong as pointed below. (it is true that @set is the same as copying the struct, but in these cases the vector of structs is not being modified).
Note that this does not update the value of s, it in principle copies the entire s (which is immutable). Still, that is still faster, than mutating the value a of an equivalent mutable struct (and this is because the way mutable and immutable structs are stored in memory):
julia> mutable struct MutableTestStruct
a::Int64
b::Array{Float64,1}
end
julia> function change_vec3!(vec)
for s in vec
s.a = 2*s.a
end
end
change_vec3! (generic function with 1 method)
julia> vec = [ MutableTestStruct(rand(1:10),rand(1_000)) for i in 1:10_000 ];
julia> @btime change_vec3!($vec)
8.692 Îźs (0 allocations: 0 bytes)
I said above in principle copies, because if you benchmark those toy examples, you will see that the time is independent of the size of the b field, thus clearly that field is not being copied (using @set or copying the struct explicitly). The compiler will figure out what is the best way to deal with such an array, probably.