How to omit type assertion from compiled output

Hello!

I have a struct S that contains a Union{Some{String}, Nothing} and I’m accessing the value at a time when it’s guaranteed to be a Some{String} (unless someone accesses the private fields of the struct). Can I access the wrapped String value without Julia emitting code for a type assertion?

Concrete example that already includes an assert:

struct S1
    v::Union{Some{String}, Nothing}

    S1() = new(Some("foo"))
end

function f1(s::S1)
    # By the time this function executes, v is guaranteed to contain a value
    # of type `Some{String}`.
    @assert typeof(s.v) == Some{String}
    return something(s.v)
end

# The challenge is to make Julia emit some code that doesn't contain any
# error handling for the case where v is `nothing`.
@code_llvm f1(S1())

Harder mode that’s closer to my real problem:

struct S2
    v::Vector{Union{Some{String}, Nothing}}

    S2() = new([Some("foo")])
end

function f2(s::S2)
    # By the time this function executes, v[1] is guaranteed to contain a value
    # of type `Some{String}`.
    v1 = @inbounds s.v[1]
    @assert typeof(v1) == Some{String}
    return something(v1)
end

# The challenge is to make Julia emit some code that doesn't contain any
# error handling for the cases where v[1] is `nothing` or out of bounds.
@code_llvm f2(S2())

I’m mostly asking out of curiosity, but if it is possible to omit these checks then I might be able to speed up a data structure I’ve written (though I won’t know until I do it whether the speed up will be worth the extra risk).

Thanks!

I think the type assertion could also look like this:

struct S1
    v::Union{Some{String}, Nothing}

    S1() = new(Some("foo"))
end

function f1(s::S1)
    # By the time this function executes, v is guaranteed to contain a value
    # of type `Some{String}`.
    v::Some{String} = s.v
    return something(v)
end

However I don’t know if the generated code solves your problem or not

1 Like

I don’t know the answer, but AFAIK @assert is only for “debugging” purposes, and it is possible that it is optimized away. Link to @assert docs. Based on that, I don’t think that it should be used for the purpose that you want to achieve.

1 Like

Fundamentally, the field is only known to be a union, so at least the field access itself infers as that union. Any further optimization then depends on the assignments like v::Some{String} = s.v, which do v = convert(Some{String}, s.v) in the actual code, to not throw an error at runtime, because s.v is only known as the union - it can’t really be eliminated.

So - what can be done? You can try using SumTypes.jl, which tend to emit a bit better code, but you’re not going to get rid of the type instability completely.

Thanks all. I know why the compiler won’t emit better code by default, I’m looking for a mechanism for making the compiler believe something it can’t prove.

Thanks for the link to SumTypes.jl, the readme is interesting.

Regarding assert, I’m using it both to demonstrate how this can be solved in some other languages (tell the compiler a fact with an assert or typeassert, then compile with no runtime asserts) and because the Julia compiler seems to respond to the presence of asserts (adding an assert can speed your code up sometimes).

The julia compiler does not, to my knowledge, handle @assert specially in any way. If you have a benchmark showing otherwise, I’d be willing to bet it’s the conditional check in the macro use, not the @assert, that could help the compiler.

That’s probably correct. I think the same behaviour occurs with any early termination (so throwing any error or returning early). The important thing is that the compiler seems to use the assertion condition (or the if condition from another early termination) to sometimes prove things that let it make optimisations.

From Essentials · The Julia Language :

An assert might be disabled at various optimization levels. Assert should therefore only be used as a debugging tool and not used for authentication verification (e.g., verifying passwords), nor should side effects needed for the function to work correctly be used inside of asserts.

This suggests that there may be some optimization level that may omit an assertion.

One obvious case, is if the compiler thinks your assertion will always be true. It might not actually check if that is the case.

julia> f() = @assert true
f (generic function with 1 method)

julia> @code_llvm f()
;  @ REPL[8]:1 within `f`
define void @julia_f_147() #0 {
top:
  ret void
}

One case where the compiler may assume wrongly is when you change constants in the REPL.

julia> const maybe = true
true

julia> g() = @assert maybe "Hello world"
g (generic function with 1 method)

julia> g()

julia> maybe = false
WARNING: redefinition of constant maybe. This may fail, cause incorrect answers, or produce other errors.
false

julia> g()

julia> g() = @assert maybe "Hello world"
g (generic function with 1 method)

julia> g()
ERROR: AssertionError: Hello world
Stacktrace:
 [1] g()
   @ Main ./REPL[6]:1
 [2] top-level scope
   @ REPL[7]:1

julia> maybe = true
WARNING: redefinition of constant maybe. This may fail, cause incorrect answers, or produce other errors.
true

julia> g()
ERROR: AssertionError: Hello world
Stacktrace:
 [1] g()
   @ Main ./REPL[6]:1
 [2] top-level scope
   @ REPL[9]:1

I’ll also that if you run Julia with -O0 (optimization level 0) that you get a more complicated f():

julia> f() = @assert true
f (generic function with 1 method)

julia> @code_llvm f()
;  @ REPL[9]:1 within `f`
define void @julia_f_115() #0 {
top:
  %thread_ptr = call i8* asm "movq %fs:0, $0", "=r"()
  %ppgcstack_i8 = getelementptr i8, i8* %thread_ptr, i64 -8
  %ppgcstack = bitcast i8* %ppgcstack_i8 to {}****
  %pgcstack = load {}***, {}**** %ppgcstack, align 8
  %0 = bitcast {}*** %pgcstack to {}**
  %current_task = getelementptr inbounds {}*, {}** %0, i64 -13
  %1 = bitcast {}** %current_task to i64*
  %world_age = getelementptr inbounds i64, i64* %1, i64 14
  ret void
}

It might, but currently isn’t. There is an open PR to enable removal of @assert in non-debug mode.