@assert alternatives?

+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

Yes, from a users perspective in current julia yes. I was talking about it from the point of view that the @assert macro in Base would work like this. For this cases I think it would be better to have a startup option with two different sysimg and precompilation directories. The downside being that you can’t toggle it in a session.

1 Like

For this cases I think it would be better to have a startup option with two different sysimg and precompilation directories.

Even the bit-polishing performance-obsessed me is thinking this could
be a bit of an overkill for @assert, XD. If we grouped all training
wheels and/or non-essentials (@assert, bound checking, TimerOutputs,
etc…), then maybe having this infrastructure to run the code with or
without them would be justified. Maybe.

Is ToggleableAsserts.jl still the state-of-the-art to use assertions in high-performance-code?

2 Likes

Check out ArgCheck.jl and OptionalArgChecks.jl.

1 Like

Very interesting. If I am in need of this kind of check again, I will
maybe try to update the package to make them work again.

Great solution.
I ended up adding something like the following to my module’s __init__() function:

@eval asserting() = get(ENV, "ENABLE_ASSERTS", "0") == "1" 

Why does that need to be in the __init__ function (and use @eval :scream:)?

1 Like