Functionality like the SAVE statement in Fortran?

I am converting some Fortran code to Julia, and I noticed the fortran code uses the SAVE statement (SAVE (FORTRAN 77 Language Reference)).

I have very little experience in Fortran, but it seems like this statement allows a variable to be preserved across function calls.

I can see how this kind of statement could be dangerous (especially in a multithreaded environment), but I think it is very useful in the context in which it is used, which is to save an initial guess for which interval a number is in (e.g. Given non-decreasing numbers t_1 < t_2 < t_3 \dots, if t_i < t < t_{i+1} then return i. Since the function (subroutine, in Fortran terms) is called many times with t only increasing slightly each call, it saves a lot of time to have the last interval used and use it as an initial guess for the next call, since most of the time the initial guess is correct.

This could also be accomplished by making the initial guess an argument to the function, but that can be burdensome when the function call is nested deeply in many other function calls, since I have to add an argument to each of the functions in the call stack. Moreover, the more “direct” the translation between Julia and Fortran, the easier the translation becomes.

Does the same functionality as Fortran’s SAVE statement exist in Julia? Is there a language feature that lets me save a variable to the function?

1 Like

Does it work like the “static” keyword in C?

No, I don’t think Julia has it. But I think the same effect can be achieved with a closure.

1 Like

What’s the difference between this and a global variable? Also, how would you want this to work with multiple dispatch?

Somehow I wasn’t aware of the “static” keyword in C, but yes, it looks like it works the same way.

I’m also not familiar with closures. The Julia docs description of them doesn’t tell me a whole lot (Performance Tips · The Julia Language), and given the example they give I don’t see how to apply a closure here.

Could you please give an example of how an effect like I described could be achieved? For example, how can I use a closure to implement an initial guess for i in the following function:

"""
`ts` is assumed to be a strictly increasing vector of floats.
`t` is assumed to be greater than `ts[1]` and less than `ts[end]`
"""
function find_interval(t::Float64, ts::Vector{Float64})
  i = 1
  while !(ts[i] < t < ts[i+1])
    if t > ts[i+1]
      i += 1
    else
      i -= 1
    end
  end
  return i
end
  
1 Like

A static variable in C is not visible outside of the function where it is defined.

4 Likes

What @ufechner7 said. A global variable could achieve the functionality I asked for, but on the other hand I don’t like that the variable could be changed by something other than the function, and I also don’t like that it would pollute the namespace of the module.

As for multiple dispatch, I don’t know. In my case, the variable I want to save is always an Int64.

Simple example of a closure:

function create_counter()
    count = 0
    return function()
        count += 1
        return count
    end
end

counter = create_counter()

Perhaps like this (untested)?

function find_interval(t::Float64, ts::Vector{Float64})
  i = 1
  return function result(t::Float64, ts::Vector{Float64})
    while !(ts[i] < t < ts[i+1])
      if t > ts[i+1]
        i += 1
      else
        i -= 1
      end
    end
  end
end
5 Likes

Thanks, that works great! I hadn’t considered using a variable outside the scope of the inner-defined function.

2 Likes

If performance is important, this must be rewritten to use a Ref wrapper:

function create_counter()
    count = Ref(0)
    return function()
        c = count[] += 1
        return c
    end
end

counter = create_counter()

Otherwise, you’re subject to the infamous closure boxing issue. (See performance of captured variables in closures · Issue #15276 · JuliaLang/julia · GitHub or just search this forum—reading material for days!)

6 Likes

Closures aren’t the only way to make function-external data that doesn’t occupy the global namespace.

Yet another (unboxed) closure capturing a local variable
julia> function makecountedcall() # closure, Ref for unboxed capture
         callcounter = Ref(0)
         function countedcall()
           callcounter[] += 1
           println(callcounter)
         end
       end
makecountedcall (generic function with 1 method)

julia> makecountedcall() # parameter shows unboxed capture
(::var"#countedcall#6"{Base.RefValue{Int64}}) (generic function with 1 method)

julia> makecountedcall().callcounter # internal captured variable
Base.RefValue{Int64}(0)
A `local`-declared variable inside a top-level `begin` block
julia> begin
         local callcounter2 = Ref(0)
         function countedcall2()
           callcounter2[] += 1
           println(callcounter2)
         end
       end
countedcall2 (generic function with 1 method)

julia> countedcall2()
Base.RefValue{Int64}(1)

julia> countedcall2()
Base.RefValue{Int64}(2)

julia> callcounter2
ERROR: UndefVarError: `callcounter2` not defined in `Main`
A `global`-declared method definition inside a local `let` block
julia> let
         callcounter3 = Ref(0)
         global function countedcall3()
           callcounter3[] += 1
           println(callcounter3)
         end
       end
countedcall3 (generic function with 1 method)

julia> countedcall3()
Base.RefValue{Int64}(1)

julia> countedcall3()
Base.RefValue{Int64}(2)

julia> callcounter3
ERROR: UndefVarError: `callcounter3` not defined in `Main`
Interpolate and assign a specific `RefValue` instance into a globally scoped method definition
julia> @eval function countedcall4()
         callcounter4 = $(Ref(0))
         callcounter4[] += 1
         println(callcounter4)
       end
countedcall4 (generic function with 1 method)

julia> countedcall4()
Base.RefValue{Int64}(1)

julia> countedcall4()
Base.RefValue{Int64}(2)

julia> callcounter4
ERROR: UndefVarError: `callcounter4` not defined in `Main`

For consistent demonstration, I made all of the external data RefValue{Int}. As stated before, the manual Ref wrapper isn’t necessary for the closure to operate, but it does help performance by evading the automatic and poorly inferred Core.Box. Meta.@lower and @code_warntype also show the same applies to the begin-local and let-global examples. Ref is necessary for the interpolated example; you can think of it as a non-existent global const variable, no automatic boxing there.

6 Likes

IMHO it’s worth the burden in the long term if the code is not a fast quick and dirty experiment. Passing an explicit state to your mutating (!) computation functions will make your code more readable and potentially amenable to thread safety in the future.

5 Likes

To complete this list:

  • Replace the function with a callable struct
    julia> mutable struct CallCounter
               count::Int
               CallCounter() = new(0)
           end
    
    julia> function (c::CallCounter)()
               c.count += 1
               println(c.count)
           end
    
    julia> callcounter = CallCounter()
    CallCounter(0)
    
    julia> callcounter()
    1
    
    julia> callcounter()
    2
    
    julia> callcounter
    CallCounter(2)
    
11 Likes

I agree on safety, but I’m not so sure about readability. As a developer it is more maintainable, but for a user it may be confusing/cumbersome.

I used the interval finding as an example, but my actual use case is that I want to evaluate bsplines at a certain point, which (in the fortran code), has an interval finding subroutine. So the value I actually want to return is the sum of all the bspline basis functions at a certain point. The interval index is not the main thing I am interested in.

If the user wants to evaluate the bsplines multiple times in succession while using good guesses for the interval index, the user has to handle the output of both a the bspline value and the updated initial guess. This puts more of a burden on the user. Providing a default method for when an initial guess is not supplied would reduce the burden on the user, but they would not get the benefit of having a good initial guess that is updated each function call.

I think you’re implying you know so already, but you can do both. Closures and callable struct instances differ from static memory in that the state can be instantiated several times independently without replacing the method; in Julia, the callable is also an input to the call and thus involved in method dispatch as much as the arguments. For example, the user can easily get to ufechner7’s create_counter or danielwe’s CallCounter, even if not deemed API. My begin-local, let-global, and @eval ... $()... examples on the other hand require replacement of the method to re-instantiate the state because the method itself is referencing an instance without an intermediary global variable, and there’s no way to make several independent states for the same method.

Is this Thread safe? Can you use it in multi-threaded situation?

function create_counter_func()
    count = Ref(0)
    return function()
        c = count[] += 1
        return c
    end
end

counter_func = create_counter_func()
1 Like

No. count[] += 1 expands to count[] = count[] + 1, which is not atomic; the read and write are separate operations. You may get data races if you call the same instance counter_func from multiple concurrent tasks. In multithreading/multitasking contexts you need atomics or locks.

The easiest way I know to make this thread-safe is to use Threads.Atomic as follows, but do note that this is a deprecated API:

function create_counter_func()
    count = Threads.Atomic{Int}(0)
    return function()
        c_old = Threads.atomic_add!(count, 1)
        # atomic_add! returns the old value;
        # add 1 to get the desired return value
        return c_old + 1
    end
end

It’s probably better to learn how to use the newer @atomic API, in which case I think it’s going to be easier to start from the CallCounter callable struct.

1 Like

One can try to reduce the effort of the user by providing a default state for the first call. What about the following toy example. where g is a solver that may be efficiently accelerated by providing some state s(e.g. last solution) when available (not at the first call):

julia> g(x,state) = (2x,state+1)
g (generic function with 2 methods)

julia> g(x) = (2x,1) #initial state is set to 1
g (generic function with 2 methods)

julia> (res,state) = g(1.2) #first call
(2.4, 1)

julia> (res,state) = g(1.2,state) #subsequent calls
(2.4, 2)

julia> (res,state) = g(1.2,state) #subsequent calls
(2.4, 3)

A while back I made the Guesser in FindFirstFunctions.jl for exactly the use case you described: FindFirstFunctions.jl · FindFirstFunctions.jl. That package also uses highly optimized algorithms to perform the search.

3 Likes

Thanks for this list and the addition of @danielwe . As this occurs so often, wouldn’t it be worth to put this list in the documentation? Somewhere below Noteworthy differences from C/C++ probably in its own section Julia ⇔ C/C++: Static Variables

1 Like

The new way would be

mutable struct AtomicRef{T} <: Ref{T}
    @atomic x::T
    AtomicRef{T}() where {T} = new{T}()
    AtomicRef(x::T) where {T} = new{T}(x)
    AtomicRef{T}(x) where {T} = new{T}(convert(T, x))
end

function create_counter_func()
    count = AtomicRef(0)
    () -> @atomic count.x += 1
end

I have no idea why the above AtomicRef or something similar isn’t provided by Base.

I’ve had to recreate it a couple times in different places so far.


EDIT: As pointed out below, the original code written above was incorrect. I have edited it to combine the load and store into one atomic op.

1 Like