Understanding weird @code_warntype warning

While testing one of my codes for type instability, I stumbled upon what feels like a strange behavior, emphasized by the example below.

julia> test(filepath::String) = @assert ispath(filepath) "File path $filepath is not valid, please verify you input"
test (generic function with 1 method)

julia> test("toto.dat")
ERROR: AssertionError: File path toto.dat is not valid, please verify you input
Stacktrace:
 [1] test(filepath::String)
   @ Main .\REPL[1]:1
 [2] top-level scope
   @ REPL[2]:1

julia> @code_warntype test("toto.dat")
MethodInstance for test(::String)
  from test(filepath::String) in Main at REPL[1]:1
Arguments
  #self#::Core.Const(test)
  filepath::String
Body::Nothing
1 ─ %1 = Main.ispath(filepath)::Bool
└──      goto #3 if not %1
2 ─      return nothing
3 ─ %4 = Main.Base::Core.Const(Base)
β”‚   %5 = Base.getproperty(%4, :string)::Any
β”‚   %6 = Base.string("File path ", filepath, " is not valid, please verify you input")::Any
β”‚   %7 = (%5)(%6)::Any
β”‚   %8 = Base.AssertionError(%7)::Any
β”‚        Base.throw(%8)
└──      Core.Const(:(return %9))

Basically, I did not expect @assert to result in unstable type since among other things AssertionError is a subtype of Exception while @code_warntype returns an Any.

Can somebody clarify this behavior ?

My configuration:

Julia Version 1.7.0-rc2
Commit f23fc0d27a (2021-10-20 12:45 UTC)
Platform Info:
OS: Windows (x86_64-w64-mingw32)
CPU: 11th Gen Intel(R) Coreβ„’ i7-1185G7 @ 3.00GHz
WORD_SIZE: 64
LIBM: libopenlibm
LLVM: libLLVM-12.0.1 (ORCJIT, tigerlake)
Environment:
JULIA_EDITOR = code
JULIA_NUM_THREADS = 6

My understanding is that exceptions only impact performance when they are triggered. Thus the Any you see is not bad. But you should test this with a suitable setup.

Excuse my lack of understanding, but what do you mean by test this with a suitable setup ?

Test whether you see bad performance on the code-path which does not hit the exception.

test_assert(filepath::String) = @assert ispath(filepath) "File path $filepath is not valid, please verify you input"
function test_noassert(filepath::String)
if ispath(filepath)
return 1
else
return 2
end
end

then test with @btime from benchmarktools.jl whether the two functions are equally fast when the file exists. This is indeed the case. Thus the Any does not impact performance of the no-throw code-path.

2 Likes

I understand now, thanks for the explanation. However, do you think the Any results from a design choice ? I always thought that this was the last resort in code_warntype, hence my surprise seeing this for a basic type as the AssertionError.

The code in your example is actually returning Nothing, as shown by this line:

We can see why this happens by using the macro @macro_expand:

julia> @macroexpand @assert ispath(filepath)
:(if ispath(filepath)
      nothing
  else
      Base.throw(Base.AssertionError("ispath(filepath)"))
  end)

which shows us that if the condition passed to @assert returns true, then the macro returns nothing.

We can see this in the @code_warntype output:

1 ─ %1 = Main.ispath(filepath)::Bool
└──      goto #3 if not %1
2 ─      return nothing
3 ─ %4 = Main.Base::Core.Const(Base)
...

If the line indicated by %1 is false, then the code goes to the section of code indicated by 3, which is the exception being raised. Otherwise, nothing is returned.

Therefore the function is type stable - it either returns nothing or errors.

I’m not sure why Base.AssertionError has the type of Any, but from a performance point of view this doesn’t matter, as this instability is only ever encountered when raising the exception.

3 Likes

Thanks for the additional information !

1 Like