Preferred way to initialize read-only variable using mutations

I wonder what your preferred ways of initializing a read-only variable using imperative code are. This becomes important, e.g., when there are closures involved and Julia wants to box the variable.

Something like

function g(x) = nothing # or something... who knows
function f(x::Integer, a::Integer)
    # Initialize y
    y = 1
    # let's pretend this sum can't be reformulated to something in closed form
    for i in 1:x
        y = y + i
    end

    # From this point on, y is basically constant
    # [...]
    sum(g(y + b) for b in 1:a)
end

My ideas were

function f(x, a)
    y = 1
    for i in 1:x
        y = y + i
    end

    let y = y
        sum(g(y + b) for b in 1:a)
    end
end

and

function f(x::Integer, a::Integer)
    y = begin 
        j = 1
        for i in 1:x
            j = j + i
        end
        j
    end

    sum(g(y + b) for b in 1:a)
end

I believe, those two variants should be equivalent. Although I am not completely certain. Personally I went with the first option, as it felt more concise to me.

What are your preferred ways of preventing a Box there and why?

My preference is to write a specific function for initialization if possible. Out of the two proposed, the second feels “better”, only I’d write it as

    y = let y = 1
        for i in 1:x
            y = y + i
        end
        y
    end

There’s also the “Ref trick”:

function f(x::Integer, a::Integer)
    # Initialize y
    y = Ref(1)
    for i in 1:x
        y[] = y[] + i
    end

    sum(g(y[] + b) for b in 1:a)
end

Now, there is no binding change for y, so no boxing needed.

1 Like

Interesting. I didn’t know the Ref trick.

Is Julia able to use a plain value for y? In that case, Ref(1) would be quite different from [1], right?

Julia compiler is free to decide if a struct (mutable or not) is stack- or heap-allocated. Ref(1) can be stack allocated if the compiler can prove it is safe. My guess is that arrays cannot be stack allocated because they don’t have a fixed size. But heap allocation of a Ref must be cheaper than array, too, because arrays have to store more meta information.

In this case, @btime shows no allocations with Ref for simple initialization procedures, but switches to heap allocation if there’s something complicated in the loop. So, using one of your initial solutions is a faster way, and both seem equivalent in performance to me.

But Ref is useful if y has to be mutated inside the closure or if you want to share a mutable value between multiple closures.

1 Like