@assert alternatives?

AFAIK there is no way yet to globally disable @assert with a compiler switch, even if doing so was already considered in the past (here and here, for example). While this is not done (as I do like to believe some day someone will implement this), what is the “de facto” solution for having @assert in performance critical code? There is a package? We can redefine the behavior for a no-op in some way? Should we implement our own macro? Use ifs and a global boolean variable?

3 Likes

You can read environment variables within Julia. Let’s use “ASSERTIONS” as the environment variable that, if it exist and it is set to “TRUE”, we want to enable our assertions. Otherwise, we do not want any assertion checking to occur.

module MayAssert
    export mayassert
    active = keyexists(ENV, "ASSERTIONS") && ENV["ASSERTIONS"] == "TRUE"
    if active
        macro mayassert(test)
            @assert(test)
        end
     else
         macro mayassert(test)
         end
      end
end
using .MayAssert
@mayassert(false) 
nothing

… another session

$ export ASSERTIONS="TRUE"
$ julia
using MayAssert
@mayassert(false)
AssertionError
1 Like

This will read the value of the environment variable at precompile time and will not update when the variable changes until a new precompile is triggered for some other reason.

7 Likes

Hmm, I think that without someone implementing it in the compiler itself, an @assert macro has no way to be a guaranteed no-op and, at the same time, avoid the need to change and recompile a specific module to alternate between “no-op assert” and “working assert”.

Maybe it will need to be a module in which I manually change the value of an ‘active’ flag every time I want to change the status of @assert.

Or can I pass something to force a module to be recompiled in a specific using/import? (or, less ideally, every time it is used/imported)? This also would solve the problem (and allow for reading the ENV to make the decision).

Can’t you just turn off precompilation of the module?

@Mason

The command line flag --compiled-modules={yes. no} enables you to toggle module precompilation on and off . When Julia is started with --compiled-modules=no the serialized modules in the compile cache are ignored when loading modules and module dependencies.

(I don’t know what _precompile_(false) does anymore)

@kristoffer.carlsson
Could mayassert be written as a mutable function wrapper, wrapping
fn::Ref(Union{typeof(doassert), typeof(donotassert)}) and get assigned as the desire for assertions is updated … sure, the calling may require some touch, but would it work?

You can write active = true or active = false into deps/deps.jl (say) at build time based on ENV["ASSERTIONS"]. If it is included from src/MayAssert.jl, the whole package and all its downstreams would be re-compiled if you re-build MayAssert. But it is not ideal to re-compile packages all the time…

Alternatively, how about abusing @boundscheck?

macro mayassert(ex)
    esc(:($Base.@boundscheck $Base.@assert $ex))
end

You can then use --check-bounds={yes|no} to toggle assertion.

2 Likes

+1 for outside-the-box

we want the assertion macro to be able to dissolve in a released version
we do not want to force the package user to start with --check-bounds=no

reversing the sense of your macro forces bounds checking off when assertions are active, and that is imperfect for development

fwiw years ago I found a way that worked – would be worth more with sourcetext, and maybe it worked by using aspects of very early Julia that no longer hold.

Ah, good point. If you want to get rid of assertion in the release mode, yet another possibility is to release version x.y and x.(y - 1) of MayAssert where y is an even number such that @mayassert from version x.y is a no-op and it’s @assert for x.(y - 1) (like old Linux kernel which used odd minor version to mean dev version). This way, you can use jlm (a tool I made) to isolate the precompilation cache for “dev builds.”

Aside: even/odd versions work for a binary flag like this. But it would really be nice to have package options (Package options · Issue #38 · JuliaLang/Juleps · GitHub, Package Options · Issue #458 · JuliaLang/Pkg.jl · GitHub) so that we can, e.g., toggle only a subset of assertions.

add this to startup.jl

const Asserting = Ref( true )  # or Ref( false )

macro mayassert(test)
    :(if Asserting[]
          @assert($test)
      end)
end

then while working, to turn off assertions Asserting[] = false
to turn assertions on again Asserting[] = true

to use switchable assertions

@mayassert( precondition )
@mayassert( invariant )
@mayassert( postcondition )

if Asserting[] has some runtime const (which is totally fine in many cases).

@JeffreySarnoff, I think the Ref is not necessary, it is? The keyword const on global variables do not just mean their types is constant, not their value?

I am not sure if I understand how macros work, would it compile for the value of Asserting[] during the first time the definition is run, and then all the other times it would be no-op or an assert? Because this would work for me.

@tkf, @JeffreySarnoff, I agree that this is not ideal, but abusing --check-bounds is very tempting for me. I would disable @assert in the same cases that I would disable bounds check that is when I am confident in the code correctness and would run experiments (to put in scientific papers) and wanted the best performance possible.

no one will fault you for using a solution that effectively propels your work
no one will fault you for using a solution that propels your work effectively

(the use of Ref allows the assertions to be switched on and off from the REPL)

You can also look at taking advantage of constant propogation to get the behavior you want.

asserting() = false #making this a function results in code being invalidated and recompiled when this gets changed

macro mayassert(test)
  esc(:(if $(@__MODULE__).asserting()
    @assert($test)
   end))
end

f(x) = @mayassert x < 2 

If we use that in a function we can see it gets compile out

julia> @code_llvm f(2)

;  @ REPL[3]:1 within `f'
define void @julia_f_12238(i64) {
top:
  ret void
}

we can just as easily turn it back on.

julia> asserting() = true

julia> @code_typed f(2)
CodeInfo(
1 ─ %1 = (Base.slt_int)(x, 2)::Bool
└──      goto #3 if not %1
2 ─      return
3 ─ %4 = %new(Core.AssertionError, "x < 2")::AssertionError
│        (Base.throw)(%4)::Union{}
└──      $(Expr(:unreachable))::Union{}
) => Nothing

Note: It is worth noting that @assert statements can sometimes help the compiler generate faster code. In those cases it would be best to use a normal @assert statement then a toggle-able one.

23 Likes

This is very clever!

I do agree, I cannot decide if is elegant or hacky clever, but is indeed clever.

Clearly, if a method is changed, so all methods that depend on it also
need to be recompiled, but not one more. And if the method return the
same literal every time, it also makes very hard for the compiler to
mess something.

If nobody jumps out of a bush and bring a caveat or pitfall of this
solution (as already happened many times in this thread) by tomorrow
noon, then I can finally decide on a solution, XD.

Thanks everyone

well crafted

Perhaps this could be put in a package? I think that could be really useful.

Using the solution of julia issue #265 to toggle debug info has been discussed (it is also used in e.g https://github.com/KristofferC/TimerOutputs.jl/blob/master/README.md#overhead to do essentially the same thing).

The downside is that the more things that use it, the more things have to be recompiled when toggling.

2 Likes

The downside is that the more things that use it, the more things have to be recompiled when toggling.

I think this is a unavoidable downside. If the intention is to have a macro that sometimes do something, sometimes is a no-op, the code that uses it has to be recompiled each time it changes situation. My original question asked by an alternative while --assert-check=no does not exist. My use case, therefore, considered that I wanted to define at the start of Julia if the assert would be no-op, or not, and compile everything accordingly. To do so, it would need to recompile any pre-compiled Module that uses @assert anyway.

I am really satisfied with this solution and it seems to be a ‘de facto’ solution for the problem (already is used by a package that wants to be able to disable overhead of a non-essential part of the program: measuring how much time was spent in each section).

I will probably use an hybrid of such solution and the --check-bounds abuse to easily toggle all extra checking when doing experiments to go on a paper (instead of testing the code correctness). I will also probably become an user of this TimerOutputs Package as I was doing something like this by hand already.

2 Likes