Why does arrayref throw?

I’m not well-versed in LLVM, but I suppose it comes down to the fact that compiler optimizer passes are divided into interprocedural (between procedures) and intraprocedural (within a procedure). Intraprocedural passes includes optimizations that would be cost-prohibitive (in compilation time) as part of interprocedural optimization, because a lot of the problems are NP-complete, so decreasing problem size prevents an explosion of compilation time costs.

Inlining literally transforms interprocedural problems into intraprocedural problems, because what it does is include the body of a procedure into another procedure, thus enabling new optimizations. This is why inlining is called an enabling transformation.

So in this specific example, (I suppose) we need inlining to enable the dead code elimination of:

  1. the code that establishes the assumptions, isassigned(a, 1) in this case
  2. the code that we wanted to eliminate in the first place, namely the undefined reference check

I suppose the relevant optimizer passes simply don’t see the information relevant for inferring that some code may be eliminated if all relevant code is not within the same procedure.

The Julia compiler should usually be able to take care of everything more or less on its own. The aspect that’s complicating things here for Julia is that, to implement the UnsafeAssume functions, it’s necessary to use inline (LLVM) assembly: llvmcall. I think that when the Julia compiler sees an inline assembly call, the assembly call is basically opaque to the compiler. Among other things (like giving up on effect inference), Julia by default doesn’t like inlining llvmcall calls. You can see that in the UnsafeAssume readme, in the sqrt example section:

julia> Base.print_statement_costs(stdout, unsafe_sqrt, Tuple{Float64})
unsafe_sqrt(x) @ Main REPL[2]:1
    0 1 ─ %1  = Base.lt_float(_2, 0.0)::Bool
    0 │   %2  = Base.not_int(%1)::Bool
    0 └──       goto #3 if not %2
    0 2 ─       goto #4
 1000 3 ─       (Core.Intrinsics.llvmcall)("unreachable", UnsafeAssume.Cvoid, Tuple{})::Nothing

The numbers on the left are the inlining “costs”, which is how the Julia compiler models what should be inlined and what shouldn’t, by default. You can see that llvmcall has a huge inlining cost, so the Julia authors wanted to prevent inlining llvmcall by default.

It might make sense for something like unsafe_assume_condition to be included into Julia as an intrinsic. This could presumably make using it less finicky, and it could also, hypothetically, enable Julia-level optimizations, in addition to LLVM-level optimizations. I suppose the Julia authors would only consider including an intrinsic like that into the Julia implementation if the UnsafeAssume package proves to be useful and popular, to justify their costs.

An alternative approach may be to teach Julia about LLVM instructions and intrinsics like unreachable and llvm.assume when they appear in inline assembly.

5 Likes