Defining const inside struct

This is a follow-up of this thread, which ended with this new issue I am not sure how to solve:

https://discourse.julialang.org/t/nightly-build-ci-failing-because-rand-unitrange-changed-when-to-care/49857/10

The issue is the following: The usage of my package starts with the user defining a series of options which are organized in a struct. Now I want to add to that struct the possibility of the user defining which is the random number generator to be used. I am using Parameters, so I have default options:

using Parameters
@with_kw struct Options
   # ... other fields
   seed :: Int64 = 321
   rng = Random.MersenneTwister(seed)
end

The problem with this is that, the user will initialize these options with, in the simplest case:

opt = Options()

which leads to the situation that using rand(rng) allocates memory:

julia> @allocated rand(opt.rng)
31353

julia> @allocated rand(opt.rng)
48

If the opt instance was constant, the allocations go away:

julia> const opt2 = Options()
Options
  seed: Int64 321
  rng: Random.MersenneTwister

julia> @allocated(opt2.rng)
0

I do not want to impose to the user the use const opt = ...., this would be quite cumbersome, because then he/she would have to change the variable name every time in a script to run the package with different options.*

I am unsure how to proceed here. I need rng to be a constant, but which can be optionally defined and yet having a default value if nothing is defined by the user. It does not need to be a part of the Options structure, it can be a global variable (changing its value will be a very rare situation, actually only during package testing).

I ended up with the idea that I could pass a parameter for the module during loading, but that feature was not implemented because it seems that better solutions were found every time.

*Telling the user to wrap everything inside a let block, or a function, is also not an option here. Everything computationally demanding in this package is done by other function which receives those options as a parameter, and that is perfectly fine, fast, with no allocations, except now for the necessity of giving the user the option to change the random number generator.

Parameters output is deceiving, this is actuall abstract type putfall: https://docs.julialang.org/en/v1/manual/performance-tips/#Avoid-fields-with-abstract-type

You can avoid it by using parametrized struct

@with_kw struct Options2{T}
   # ... other fields
   seed :: Int64 = 321
   rng :: T = Random.MersenneTwister(seed)
end

and it’s better to test it somewhere where you can see that allocations do not multiply

function f(opts)
    res = 0.
    for i in 1:100
        res += rand(opts.rng)
    end
    return res
end
opt = Options()
opt2 = Options2()

julia> @allocated f(opt)
3200

julia> @allocated f(opt2)
16

Or you can verify that your code is type stable with @code_warntype

julia> @code_warntype f(opt)
Variables
  #self#::Core.Compiler.Const(f, false)
  opts::Options
  res::Any
  @_4::Union{Nothing, Tuple{Int64,Int64}}
  i::Int64

Body::Any
....

julia> @code_warntype f(opt2)
Variables
  #self#::Core.Compiler.Const(f, false)
  opts::Options2{MersenneTwister}
  res::Float64
  @_4::Union{Nothing, Tuple{Int64,Int64}}
  i::Int64

Body::Float64
2 Likes

Thank you. I tested that, but as your example shows, it still allocates (less, but still).

This is unavoidable and not related to rng or anything else. This is just REPL working with global variables.

function f(x)
    res = 0.
    for i in 1:100
        res += x
    end
    res
end

x = 10.

julia> @allocated f(x)
16

julia> function g()
           x = 10.0

           f(x)
       end
g (generic function with 1 method)

julia> @allocated g()
0
2 Likes

Uhm… I will check that out in the actual code. Of course there the calls to rand are inside functions, such that those allocations should not appear. I have the impression of having tested that, but maybe I didn’t after parameterizing the struct type. I hope that is the problem. Thanks again.

1 Like

Just for completeness. The problem I was having was that one, but with a small complicator. The Options structure above was being inherited (very likely this is not the correct word in computer science for this) by other structure, which I had to parameterize as well to get rid of all allocations. Something like this:

using Parameters
import Random

@with_kw struct Options{T}
  seed :: Int64 = 321
  rng :: T = Random.MersenneTwister(seed)
end

struct ComputeData
  opt :: Options
end

Changing ComputeData to this solved the problem completely:

struct ComputeData{T}
  opt :: Options{T}
end

Otherwise I still got some allocations in the use of rand inside the main function, although I could not track that with @code_warntype really.

2 Likes

The field of ComputeData had an abstract type, instead of the concrete type, you changed to the concrete type and solved the problem.

The word is composed (as a struct is composed of its fields, and putting a field T inside a struct instead inheriting the type T is often referred as using composition instead of inheritance).

5 Likes