Fast alternative to mutable struct

Hi, first discourse topic/question ever, please do tell if this is the wrong place for this type of question. (I’m also new to Julia, so this might be trivial.)

I have a function, with a couple of keyword arguments.
(Disclaimer: I like keyword arguments to define broadly the environment/domain of my problem, and I prefer them to default arguments, because I can call and change just some of them by name).

Anyway, I thought I’d pull default values for kw args from some kind of object/structure, but also be able to change that structure at times (with a set function; a.k.a. change the “environment”), so object had to be a mutable, so I used a mutable struct:

mutable struct Parameters{T}
	ao::T
	bo::T
end

instantiated:
params = Parameters{Float64}(1.23, 8.77)

defined the function:
f(x; a = params.ao, b = params.bo) = ...

also defined a function to change the params:

function set(; new_ao = params.ao, new_bo = params.bo)
	params.ao = new_ao
	params.bo = new_bo
end

This, of course, works, but the problem is that mutable structs are apparently slow.
If instead of a struct I use a const array:
const params = [1.23, 8.77]

and define the function as: f(x; a = params[1], b = params[2]) = ...

(and also redefine the ‘set’ function) execution is 10x faster!

This is a hack, though: in the new set function I’m allowed to do params[1] = new_ao, although the params array is const! But in some future version of Julia, const arrays will presumably be really immutable.

So, is there an obvious alternative I’m missing? some elegant and more future-proof way to do what I want, and still have fast execution times?

If all you are doing is passing around parameters, I’d probably put them in a NamedTuple. https://github.com/mauro3/Parameters.jl has the @with_kw macro that makes it easy to use. Then just pass the NamedTuple to your functions.

4 Likes

This is not true; const is not recursive. If you set const x = [1:10] then you are free to modify the elements of x; the only thing you can’t do is assign a different value to x.

Similarly, you should feel free to define

mutable struct Params
    something :: Float64
end

const params = Params()

and modify params.something = .... whenever you want. Is that what you are looking for?

2 Likes

Why? Who told you that? Fwiw, the only reason it can be slow sometimes is because it’s mutable, since that’s the property you need, you don’t have an alternative.

No that’s impossible.

2 Likes

@tkluck wow, that was fast, thanks!

My questions about the potential future immutability of const arrays comes from this - admittedly very old - discussion: https://github.com/JuliaLang/julia/issues/317
Reading the replies, it didn’t seem to me like the idea was completely shot down, so, yeah, that.

Now for the main part of your answer, I feel like an idiot. I was doing this late last night, and I’d swear I did try defining a const instance of the mutable struct, and then couldn’t set new values in the instance (with params.ao = ... )! I really really did it! And apparently I didn’t :(. What was I doing really?..

So, thank you, that was humbling enough for a first discourse!..

The difference comes because you compare setting your function parameters with a global variable params = Parameters{Float64}(1.23, 8.77) vs setting them with a constant const params = [1.23, 8.77]. If I do:

mutable struct Par{T}
	a::T
	b::T
end

const par = Par(1.23, 8.77)
f(x, a=par.a, b=par.b) = x*a/b

const arr = [1.23, 8.77]
f1(x, a=arr[1], b=arr[2]) = x*a/b

julia> using BenchmarkTools

julia> @btime f(pi)
  4.033 ns (0 allocations: 0 bytes)
0.4406110563187509

julia> @btime f1(pi)
  5.042 ns (0 allocations: 0 bytes)
0.4406110563187509

vs

var = Par(1.23, 8.77)
g(x, a=var.a, b=var.b) = x*a/b

varr = [1.23, 8.77]
g1(x, a=varr[1], b=varr[2]) = x*a/b

julia> @btime g(pi)
  141.142 ns (6 allocations: 96 bytes)
0.4406110563187509

julia> @btime g1(pi)
  67.992 ns (3 allocations: 48 bytes)
0.4406110563187509

It has nothing to do with performance of mutable structs. If you set your function parameters with a reference to a variable, the compiler needs to generate extra code.

2 Likes

@yuyichao as per my reply to tkluck, my reservations on const arrays are based on a very old discussion. I wouldn’t be surprised if real immutable arrays were pursued at some point, though; there is merit to them.

As for the performance of mutable structs, I was, as I of course realize now, comparing apples to oranges (a const array to a non-const struct). Still the array seems faster (I did some runs just now with @btime), but nowhere near 10x.

Sorry guys, I’ve been looking into Julia for maybe a week now, and in my spare time, too, so I still understand pretty much nothing! Thanks for the quick responses.

The point is that const ... = [...] will never make an otherwise normal array immutable. Immutable array type could be created, normal array could be made immutable, but the const there will play no roll in any of it.

This shouldn’t be the case either…

1 Like

hmm, I might have some leftover type instability. I’ll look into it later. Thanks.

For what it’s worth, real immutable arrays exist right now; they’re just not in the standard library:

3 Likes

params is const, the content of params is not const – this is a well defined state of affairs in Julia. Use it as you like.

julia> const params = [1.25, 2.5]
2-element Array{Float64,1}:
 1.25
 2.5

julia> params = "abc"
ERROR: invalid redefinition of constant params
Stacktrace:
 [1] top-level scope at REPL[2]:1

julia> params[1] *= 4
5.0

julia> params
2-element Array{Float64,1}:
 5.0
 2.5
1 Like