Avoiding branching in loops with macros

metaprogramming

#1

Is there a common pattern in Julia (perhaps with some kind of a macro) for the following case?
I have a function that runs a model through several thousands of time steps, and I want to have two versions:

  • one where each time-step is saved,
  • just a sum over all time steps is computed
    As the parameters of this model will be optimized using Optim.jl on many sets, I would like to keep the functions as performant as possible and avoid conditions inside the loop, yet without copy-pasting a large part of code (only two lines are different).

#2

You can use @static if e.g.

const SAVE_TIMESTEP = false

function f()
    @static if SAVE_TIMESTEP
        ....
    end
    ....
end

but if the conditional is statically known, it is likely to get optimized away without need of @static.

Have you benchmarked to see that the conditionals are actually expensive. CPUs are very good at predicting branches.


#3
function foo(x,v::Val{T}) where T
       if T 
          return x
       else 
         return -x
       end
end

julia> @code_llvm foo(12, Val(true));

define i64 @julia_foo_66629(i64) #0 !dbg !5 {
top:
  ret i64 %0
}

julia> @code_llvm foo(12, Val(false));

define i64 @julia_foo_66663(i64) #0 !dbg !5 {
top:
  %1 = sub i64 0, %0
  ret i64 %1
}

#4

@kristoffer.carlsson @foobar_lv2 thank you for the fast responses!

I was hanging on too much on my lessons from my early computer science courses which assumed very little cleverness on the part of the compiler.

Is the following then a good pattern to handle multiple dispatch of optional parameters within the function body?

function foo(x, y::T=nothing) where T
    if T == Nothing
        sum = zero(eltype(x))
    end
    for i in eachindex(x)
        if T == Nothing
            sum += x[i]
        else
            y[i] = x[i]+1
        end
    end
    if T == Nothing
        return sum
    else
        return y
    end
end

#5

You don’t even need the type parameter — just doing if y isa Nothing has enough static information that those branches will be compiled away.


#6

I think the official way is something like

struct Verbose_T end
const verbose = Verbose_T()
struct Debug_T end
const debug = Debug_T()

function foo(x, loglevel=nothing)
    if loglevel==debug
       @show x
    elseif loglevel == verbose
       println("entering foo")
    else
       #we need no default branch today
    end
    return x   
end

Feel free to use more reasonable / julian type names. I like to use intentionally lenghty/nonclashing names for stuff that really is just internals.

edit: You call by

foo(12)
foo(12, debug)
foo(12, verbose)

Also, my suggestions are slightly outdated on current master. I think nowadays keyword parameters are the way to go.


#7

Thanks for the replies! I suppose I was aiming at making types explicit, since the function is going to be run many times inside the optimization function and I wanted to avoid type instability, but I see now that it is not necessary.