Compile time conditions or dispatch removing code based on (DEBUG, LIVE) mode

Hey Julianners!
Conditional code compilation would be Crazy!
I want to make a code where I don’t have to comment out some of my code when I run my code in live version. I know I am talking about 10-20 conditions only but if certain part run millions of time it would be nice to avoid it. This would also open some chance to create more conditions to check the code with special flags, which is actually really big deal.

I saw we have @static if [condition] [code] end way to do it.
What I wanted to know if I can achieve this effect on local variables. Dispatching on Val(:SYMBOL) wasn’t actually removing code. Which is actually really interesting that I cannot use @inline with the Val to remove codes from my codebase.

The code I tried without success:

using InteractiveUtils

@inline test(::Val{:DOTEST}, v) = v== 5.2 ? println("HEY") : println("NO")
@inline test(::Val{:NOTEST}, v) = nothing
fn1(isdebug, v) = begin
	test(isdebug, v)
	@static if eisdebug test(isdebug, v) end # works but only on global scope variables? 
fn1(Val(:DOTEST), 7.2)
fn1(Val(:NOTEST), 7.2)
@code_warntype fn1(Val(:DOTEST), 7.2)
@code_warntype fn1(Val(:NOTEST), 7.2)
using Static
@inline test2(::True, v) = v== 5.2 ? println("HEY") : println("NO")
@inline test2(::False, v) = nothing
fn2(isdebug, v) = begin
	test2(isdebug, v)

fn2(static(true), 7.2)
fn2(static(false), 7.2)
@code_warntype fn2(static(false), 7.2)
@code_warntype fn2(static(true), 7.2)

So how to avoid code to compile based on LOCAL fn variables like @static. (Supposed, we know the type exactly at compile time)

So there is another problem. @static seems to has some compilation problem as changing the global variable’s value doesn’t start recompilation as local variable would at type dispatch I guess.

Not really sure if this is useful to you, but maybe you can use the type system as a trait. Here’s a simple example to get you started.

abstract type CompileModeType end
struct Debug <: CompileModeType end
struct Live <: CompileModeType end

CompileMode() = Live()

test(v) = test(CompileMode(), v)
test(::Debug, v) = v == 5.2 ? "HEY" : "NO"
test(::Live, v) = nothing

@code_warntype test(7.2)

You could add type arguments to CompileMode() for finer control.

See Holy Traits Pattern (book excerpt), or search for Holy Traits for more detailed examples.

The problem is that, it is no different than the first example with the :DOTEST and :NOTEST, but it doesn’t require to create new structures for the solution.

You get this:

MethodInstance for test(::Float64)
  from test(v) in Main at /home/hm/repo/agei/tests/playground/compilation_time_condition.jl:45
1 ─ %1 = Main.CompileMode()::Core.Const(Live())
│   %2 = Main.test(%1, v)::Core.Const(nothing)
└──      return %2

So, the point I want to make to not having these code in the code:

1 ─ %1 = Main.CompileMode()::Core.Const(Live())
│   %2 = Main.test(%1, v)::Core.Const(nothing)


Ok, just to note.
Anyone else facing with this issue.

Each of the solution listed here is good.

  1. The @static if … remove the appropriate code from the compilation.
  2. In each of the other case you have to check the optimised compiled version, where the branch will be already excluded like this:
println(@code_typed fn1(Val(:DOTEST),7.2))
@code_typed fn1(Val(:NOTEST),7.2)


1 ─ %1 = Base.eq_float(v, 5.2)::Bool
└──      goto #3 if not %1
2 ─      invoke Main.println("HEY"::String)::Nothing
└──      goto #4
3 ─      invoke Main.println("NO"::String)::Nothing
└──      goto #4
4 ┄      return nothing
) => Nothing
1 ─     return nothing
) => Nothing

So, the solution is actually fairly simple and can be chosen as preferred.

Note that you can also use the “use functions as global variables” trick.

mode() = :debug

function myfunc()
    if mode() == :debug
        # do this only in debug mode
    println("regular computation")

This gives

julia> myfunc()
regular computation

julia> @code_typed myfunc()
1 ─      goto #3 if not true
2 ─      invoke Main.println("debugging..."::String)::Any
3 ┄ %3 = invoke Main.println("regular computation"::String)::Nothing
└──      return %3
) => Nothing

Redefining, say, mode() = :live will automatically trigger recompilation of myfunc() and, via constant propagation, eliminate the debug branch entirely:

julia> mode() = :live
mode (generic function with 1 method)

julia> myfunc()
regular computation

julia> @code_typed myfunc()
1 ─      nothing::Nothing
│   %2 = invoke Main.println("regular computation"::String)::Nothing
└──      return %2
) => Nothing

One advantage of this approach compared to attempting to achieve the same with global variables is that you can’t assign variables in other modules but you can redefine functions in other modules. See GitHub - JuliaPerf/STREAMBenchmark.jl: A version of the STREAM benchmark which measures the sustainable memory bandwidth., where I tell the user to redefine STREAMBenchmark.avxt() = true to make my package STREAMBenchmark use LoopVectorization for multithreading, as an example.

1 Like

I realised @static if is actually useful if there can be structural differences or unexisting function that is called based on version or config and would cause compile time issues.

The question is whether it is better to have in precompilation time or better to have to do this job with the llvm optimizer or something like that.
It would be great to know what is better when we calculate the time but… for now we have to be satisfied with these answer I guess.

Maybe one reason to only use @static if in very specific cases as it doesn’t perform recompilation with Revise when the if condition changes and drastically lower the speed of development as we need to make manual modification to start the recompilation.

RFC: implement proper debug mode for packages (with support for re-precompilation) by KristofferC · Pull Request #37874 · JuliaLang/julia · GitHub might be relevant.