Why is throw(Exception) less efficient than throw(String) when unreachable?

Why does not g() yield as efficient code as f() in this example:

julia> f() = 2 < 1 ? throw("This Should Never Happen") : 42
f (generic function with 1 method)

julia> g() = 2 < 1 ? throw(ErrorException("This Should Never Happen")) : 42
g (generic function with 1 method)

julia> (f(), g())
(42, 42)

julia> code_llvm(f, ())

define i64 @julia_f_67922() #0 !dbg !5 {
top:
  ret i64 42
}

julia> code_llvm(g, ())

define i64 @julia_g_67923() #0 !dbg !5 {
top:
  %0 = call i8**** @jl_get_ptls_states() #5
  %1 = alloca [4 x i8**], align 8
  %.sub = getelementptr inbounds [4 x i8**], [4 x i8**]* %1, i64 0, i64 0
  %2 = getelementptr [4 x i8**], [4 x i8**]* %1, i64 0, i64 2
  %3 = bitcast [4 x i8**]* %1 to i64*
  %4 = bitcast i8*** %2 to i8*
  call void @llvm.memset.p0i8.i64(i8* %4, i8 0, i64 16, i32 8, i1 false)
  store i64 4, i64* %3, align 8
  %5 = bitcast i8**** %0 to i64*
  %6 = load i64, i64* %5, align 8
  %7 = getelementptr [4 x i8**], [4 x i8**]* %1, i64 0, i64 1
  %8 = bitcast i8*** %7 to i64*
  store i64 %6, i64* %8, align 8
  store i8*** %.sub, i8**** %0, align 8
  %9 = load i64, i64* %8, align 8
  store i64 %9, i64* %5, align 8
  ret i64 42
}

(Background: I’m writing code that will run in a tight loop, and I need it to be fast, but I would still like to check some compile-time constants (types, etc). My thought was that this would work fine, since the tests will be done at compile time, and therefore not generate any extra work at run-time. However, when I look at the output of code_llvm or code_native, it seems that tests that could throw certain exceptions still generate extra code, even though the exception can never be thrown.)

I’d like to throw exceptions, rather than strings, because that seems to be the correct coding style, but I will not do so at the expense of slower functions.

The example was tested on julia 0.5.1 and 0.6.0-pre.alpha.76.

Maybe put the exception throwing part in a separate function and annotate it with @noinline. I think this has to do with GC roots being created.

Thanks, that worked!

julia> @noinline throwerror(x) = throw(ErrorException(x))
throwerror (generic function with 1 method)

julia> g() = 2 < 1 ? throwerror("This Should Never Happen") : 42
g (generic function with 1 method)

julia> code_llvm(g,())

define i64 @julia_g_68071() #0 !dbg !5 {
top:
  ret i64 42
}
1 Like

A follow-up question: Is the creation of unnecessary GC roots considered an issue with julia itself (should I file a bug-report somewhere) or is it the programmer’s responsibility to watch out for these things?

For the time being, I’m going to write a creates_GC_root function that captures the output of code_llvm and scans for the GC root code, so that my unit tests can warn me when this happens.

It’s a known issue.

See https://github.com/JuliaLang/julia/issues/12152, https://github.com/JuliaLang/julia/pull/9693

Interesting. Maybe julia needs some kind of @static_object macro that causes objects to be created at compile-time and persist, rather than being created at runtime and garbage-collected.

julia> macro static_object(x)
       s = gensym()
       eval(current_module(),  :( const $s = $x ))
       s
       end
@static_object (macro with 1 method)

julia> g() = 2 < 1 ? throw(@static_object(ErrorException("This Should Never Happen"))) : 42
g (generic function with 1 method)

julia> code_llvm(g, ())

define i64 @julia_g_67879() #0 !dbg !5 {
top:
  ret i64 42
}

The way this has often been written when needed in Base is:

@eval g() = 2 < 1 ? throw($(ErrorException("This Should Never Happen")))) : 42
2 Likes