@debug allocate even when not in DEBUG mode

Note that you can also just write your own @debug-like macro.

Parse-time version

When should_print_pt is set to false, println will not be put into the Julia code.

const should_print_pt = false

macro debug_pt(ex)
    should_print_pt ? esc(:(println($ex))) : nothing
end

function foo_debug_pt()
    @debug_pt "Hello"
    return 0
end

function loop_foo_debug_pt()
    for _ in 1:1000
        foo_debug_pt()
    end
end
julia> @code_lowered foo_debug_pt()
CodeInfo(
1 ─     return 0
)

julia> @btime loop_foo_debug_pt()  # (No idea why we don't get 1.2 ns like in the loop_foo case on my machine)
  16.316 ns (0 allocations: 0 bytes)

julia> @code_llvm loop_foo_debug_pt()
; Function Signature: loop_foo_debug_pt()
;  @ REPL[4]:1 within `loop_foo_debug_pt`
; Function Attrs: uwtable
define void @julia_loop_foo_debug_pt_7305() #0 {
top:
;  @ REPL[4]:4 within `loop_foo_debug_pt`
  ret void
}

The downside is that this is not flexible at all: to set should_print_pt to true you have to reload the code.

julia> const should_print_pt = true
WARNING: redefinition of constant Main.should_print_pt. This may fail, cause incorrect answers, or produce other errors.
true

julia> foo_debug_pt()  # Does not print
0

julia> macro debug_pt(ex)
           should_print_pt ? esc(:(println($ex))) : nothing
       end;

julia> function foo_debug_pt()
           @debug_pt "Hello"
           return 0
       end;

julia> foo_debug_pt()
Hello
0
Compile-time version

When should_print_ct is the false function, println will be put into the Julia code, but gets compiled away.

should_print_ct() = false

macro debug_ct(ex)
    esc(:((should_print_ct() && println($ex)); nothing))
end

function foo_debug_ct()
    @debug_ct "Hello"
    return 0
end

function loop_foo_debug_ct()
    for _ in 1:1000
        foo_debug_ct()
    end
end
julia> @code_lowered foo_debug_ct()  # Still has a println
CodeInfo(
1 ─ %1 = Main.should_print_ct()
└──      goto #3 if not %1
2 ─      Main.println("Hello")
└──      goto #3
3 ┄      Main.nothing
└──      return 0
)

julia> @btime loop_foo_debug_ct() 
  1.200 ns (0 allocations: 0 bytes)

julia> @code_llvm loop_foo_debug_ct()  # No more println
; Function Signature: loop_foo_debug_ct()
;  @ REPL[4]:1 within `loop_foo_debug_ct`
; Function Attrs: uwtable
define void @julia_loop_foo_debug_ct_7774() #0 {
top:
;  @ REPL[4]:4 within `loop_foo_debug_ct`
  ret void
}

Changing should_print_ct will automatically trigger recompilation of foo_debug_pt, making this more flexible, at the cost of recompilations.

julia> foo_debug_ct()  # We still have should_print_ct() = false
0

julia> @time begin should_print_ct() = true; foo_debug_ct() end
Hello
  0.003950 seconds (751 allocations: 36.633 KiB, 93.69% compilation time: 100% of which was recompilation)
0
Runtime version

The println will always remain in the compiled code, but will not be executed when !should_print_rt[]. This will make this version a bit slower.

const should_print_rt = Ref(false)

macro debug_rt(ex)
    esc(:((should_print_rt[] && println($ex)); nothing))
end

function foo_debug_rt()
    @debug_rt "Hello"
    return 0
end

function loop_foo_debug_rt()
    for _ in 1:1000
        foo_debug_rt()
    end
end
julia> @code_llvm foo_debug_rt()
; Function Signature: foo_debug_rt()
;  @ REPL[3]:1 within `foo_debug_rt`
; Function Attrs: uwtable
define i64 @julia_foo_debug_rt_7422() #0 {
top:
;  @ REPL[3]:2 within `foo_debug_rt`
; ┌ @ refvalue.jl:59 within `getindex`
; │┌ @ Base.jl:49 within `getproperty`
    %0 = load i8, ptr @"jl_global#7426.jit", align 16
    %1 = and i8 %0, 1
    %.x.not = icmp eq i8 %1, 0
; └└
  br i1 %.x.not, label %L5, label %L4

L4:                                               ; preds = %top
  call void @j_println_7428(ptr nonnull @"jl_global#7429.jit")
  br label %L5

L5:                                               ; preds = %L4, %top
;  @ REPL[3]:3 within `foo_debug_rt`
  ret i64 0
}

julia> @btime loop_foo_debug_rt()
  255.251 ns (0 allocations: 0 bytes)

So this is still much faster than the full @debug, mainly because we’re using a RefValue{Bool} instead of the ENV dictionary.

Altering should_print_rt[] does not have any cost, making this the most flexible.

julia> foo_debug_rt()  # should_print_rt[] == false
0

julia> @time begin should_print_rt[] = true; foo_debug_rt() end
Hello
  0.000162 seconds (5 allocations: 80 bytes)
0