This intrigued me, so I looked up how to do it:
assume_unreachable() = Core.Intrinsics.llvmcall("unreachable", Cvoid, Tuple{})
function assume_condition(c::Bool)
# This could also be implemented using LLVM's `llvm.assume`
# intrinsic directly:
# https://llvm.org/docs/LangRef.html#llvm-assume-intrinsic
c || assume_unreachable()
nothing
end
function test0(a)
@inbounds a[1]
end
function test1(a)
assume_condition(isassigned(a, 1))
@inbounds a[1]
end
julia> a = [Int[]]
1-element Vector{Vector{Int64}}:
[]
julia> @code_llvm test0(a)
; Function Signature: test0(Array{Array{Int64, 1}, 1})
; @ REPL[3]:1 within `test0`
define nonnull {}* @julia_test0_9569({}* noundef nonnull align 16 dereferenceable(40) %"a::Array") #0 {
top:
; @ REPL[3]:2 within `test0`
; ┌ @ essentials.jl:13 within `getindex`
%0 = bitcast {}* %"a::Array" to {}***
%.data1 = load {}**, {}*** %0, align 8
%.ref = load {}*, {}** %.data1, align 8
%.not = icmp eq {}* %.ref, null
br i1 %.not, label %fail, label %pass
fail: ; preds = %top
%jl_undefref_exception = load {}*, {}** @jl_undefref_exception, align 8
call void @ijl_throw({}* %jl_undefref_exception)
unreachable
pass: ; preds = %top
; └
ret {}* %.ref
}
julia> @code_llvm test1(a)
; Function Signature: test1(Array{Array{Int64, 1}, 1})
; @ REPL[4]:1 within `test1`
define nonnull {}* @julia_test1_9742({}* noundef nonnull align 16 dereferenceable(40) %"a::Array") #0 {
top:
; @ REPL[4]:2 within `test1`
; ┌ @ array.jl:268 within `isassigned`
; │┌ @ abstractarray.jl:684 within `checkbounds`
; ││┌ @ abstractarray.jl:386 within `eachindex`
; │││┌ @ abstractarray.jl:134 within `axes1`
; ││││┌ @ abstractarray.jl:98 within `axes`
; │││││┌ @ array.jl:191 within `size`
%0 = bitcast {}* %"a::Array" to { i8*, i64, i16, i16, i32 }*
%.length_ptr = getelementptr inbounds { i8*, i64, i16, i16, i32 }, { i8*, i64, i16, i16, i32 }* %0, i64 0, i32 1
%.length = load i64, i64* %.length_ptr, align 8
; ││└└└└
; ││┌ @ abstractarray.jl:760 within `checkindex`
; │││┌ @ int.jl:513 within `<`
%.not = icmp ne i64 %.length, 0
; │└└└
call void @llvm.assume(i1 %.not)
; │ @ array.jl:270 within `isassigned`
%1 = bitcast {}* %"a::Array" to {}***
%.data5 = load {}**, {}*** %1, align 8
%array_slot = load atomic {}*, {}** %.data5 unordered, align 8
; └
; @ REPL[4]:3 within `test1`
ret {}* %array_slot
}
I’m not sure whether test1
, which uses llvm.assume
, compiles to better code, though. For some reason there’s an atomic load, which seems suboptimal. Why is that? EDIT: actually, the atomic load seems to be part of isassigned
? It’s a bit disappointing that the isassigned
call isn’t optimized away, actually?