Non-const globals in a macro and performances on the function calling the macro

I am trying to implement some “fixed stochasticity” in individual functions of my model (called once), where I have a global seed and then this is re-seed on each needed function based on the name of the function and the global seed, such that different functions are uncorrelated but the same function provides at each call the same outcomes, without passing RNGs around.

Anyhow, my issue is that in order to achieve this behaviour I need to use a non-const global (at module level) on the macro, and I wonder if this has a performance effect on the function calling the macro, or the macro implies a barrier effect.

This is a snippet of what I am trying to achieve:

cd(@__DIR__)
module Foo

import Random
export random_seed

global_random_seed = 123 

macro random_seed!()
    return quote
        st = stacktrace(backtrace())
        myf = ""
        for frm in st
            funcname = frm.func
            if frm.func != :backtrace && frm.func!= Symbol("macro expansion")
                myf = frm.func
                break
            end
        end
        m1 = $("$(__module__)")
        s = m1 * "$(myf)"
        Random.seed!(hash("$s",UInt64(global_random_seed)))
        @info "Random seeded with hash of \"$s\" and $(global_random_seed)"
    end
end

function init()
    Foo.global_random_seed = parse(Int64,readline("test_seed.txt"))  #just 125 in test_seed.txt
end

module FooFoo
import ..Foo
import ..Foo:@random_seed!, global_random_seed


function foo()
    @random_seed!()
    println(rand())
    println(rand())
end

function goo()
    @random_seed!()
    println(rand())
    println(rand())
end

end

end

Foo.init()
Foo.FooFoo.foo() # Julia v1.11: 0.008282138701719233 0.9042476148934772
Foo.FooFoo.foo() # Julia v1.11: 0.008282138701719233 0.9042476148934772
Foo.FooFoo.goo() # Julia v1.11: 0.4430332457748505   0.7426054431450675
Foo.FooFoo.goo() # Julia v1.11: 0.4430332457748505   0.7426054431450675
Foo.FooFoo.foo() # Julia v1.11: 0.008282138701719233 0.9042476148934772

The macro has no arguments or computation besides instantiating the return expression, so it’s just pasting that with generated local variables into macro call sites. global_random_seed is just a symbol in the expression, so it’s going to work like any global variable in a macro-less method. The bad type inference isn’t saved by the UInt64 call alone because the language doesn’t mandate type constructors return their own type; it’s saved by the only hash(::String, ...) method returning UInt.

Could do global_random_seed::Int for inherent type stability (or why not ::UInt64 if that’s all you’ll use it for), though non-const global variables require assignment checks and is thus implemented by a reference to a reference the last time I checked. Indexing and mutating const global_random_seed = Ref(123) would halve the work, though there is the risk of reading garbage values “assigned” to an uninitialized Ref{Int}(). The performance gain was actually dubious then, not sure how CPUs handled things.

Thank you. I am not concerned by the performances of the macro, but of the function where the macro is called.

These are themselves high-level functions that are called only once, but may contain for loops and run for days. My understanding is that compiling time is then treasurable, right ?

I don’t know what you mean by “compiling time” if you’re concerned about runtime performance. Annotating the global variable with a type shouldn’t stress the compiler any more than typical type stability practices, and whether it significantly affects runtime depends on how much of the runtime handles global_random_seed::Any. If it’s just the one UInt64 call that restores type stability in a days-long run, it’s negligible. If it’s making many short calls in your hot loop type-unstable, then it’s worth doing global_random_seed:Int.