Techniques for fixing inference problems

Here we assume you've dug into your code with a tool like Cthulhu, and want to know how to fix some of the problems that you discover. Below is a collection of specific cases and some tricks for handling them.

Note that there is also a tutorial on fixing inference that delves into advanced topics.

Adding type annotations

Using concrete types

Defining variables like list = [] can be convenient, but it creates a list of type Vector{Any}. This prevents inference from knowing the type of items extracted from list. Using list = String[] for a container of strings, etc., is an excellent fix. When in doubt, check the type with isconcretetype: a common mistake is to think that list_of_lists = Array{Int}[] gives you a vector-of-vectors, but

julia> isconcretetype(Array{Int})
false

reminds you that Array requires a second parameter indicating the dimensionality of the array. (Or use list_of_lists = Vector{Int}[] instead, as Vector{Int} === Array{Int, 1}.)

Many valuable tips can be found among Julia's performance tips, and readers are encouraged to consult that page.

Working with non-concrete types

In cases where invalidations occur, but you can't use concrete types (there are indeed many valid uses of Vector{Any}), you can often prevent the invalidation using some additional knowledge. One common example is extracting information from an IOContext structure, which is roughly defined as

struct IOContext{IO_t <: IO} <: AbstractPipe
    io::IO_t
    dict::ImmutableDict{Symbol, Any}
end

There are good reasons that dict uses a value-type of Any, but that makes it impossible for the compiler to infer the type of any object looked up in an IOContext. Fortunately, you can help! For example, the documentation specifies that the :color setting should be a Bool, and since it appears in documentation it's something we can safely enforce. Changing

iscolor = get(io, :color, false)

to

iscolor = get(io, :color, false)::Bool     # assert that the rhs is Bool-valued

will throw an error if it isn't a Bool, and this allows the compiler to take advantage of the type being known in subsequent operations.

If the return type is one of a small number of possibilities (generally three or fewer), you can annotate the return type with Union{...}. This is generally advantageous only when the intersection of what inference already knows about the types of a variable and the types in the Union results in an concrete type.

As a more detailed example, suppose you're writing code that parses Julia's Expr type:

julia> ex = :(Array{Float32,3})
:(Array{Float32, 3})

julia> dump(ex)
Expr
  head: Symbol curly
  args: Vector{Any(3,))
    1: Symbol Array
    2: Symbol Float32
    3: Int64 3

ex.args is a Vector{Any}. However, for a :curly expression only certain types will be found among the arguments; you could write key portions of your code as

a = ex.args[2]
if a isa Symbol
    # inside this block, Julia knows `a` is a Symbol, and so methods called on `a` will be resistant to invalidation
    foo(a)
elseif a isa Expr && length((a::Expr).args) > 2
    a::Expr         # sometimes you have to help inference by adding a type-assert
    x = bar(a)      # `bar` is now resistant to invalidation
elseif a isa Integer
    # even though you've not made this fully-inferrable, you've at least reduced the scope for invalidations
    # by limiting the subset of `foobar` methods that might be called
    y = foobar(a)
end

Other tricks include replacing broadcasting on v::Vector{Any} with Base.mapany(f, v)mapany avoids trying to narrow the type of f(v[i]) and just assumes it will be Any, thereby avoiding invalidations of many convert methods.

Adding type-assertions and fixing inference problems are the most common approaches for fixing invalidations. You can discover these manually, but using Cthulhu is highly recommended.

Inferrable field access for abstract types

When invalidations happen for methods that manipulate fields of abstract types, often there is a simple solution: create an "interface" for the abstract type specifying that certain fields must have certain types. Here's an example:

abstract type AbstractDisplay end

struct Monitor <: AbstractDisplay
    height::Int
    width::Int
    maker::String
end

struct Phone <: AbstractDisplay
    height::Int
    width::Int
    maker::Symbol
end

function Base.show(@nospecialize(d::AbstractDisplay), x)
    str = string(x)
    w = d.width
    if length(str) > w  # do we have to truncate to fit the display width?
        ...

In this show method, we've deliberately chosen to prevent specialization on the specific type of AbstractDisplay (to reduce the total number of times we have to compile this method). As a consequence, Julia's inference may not realize that d.width returns an Int.

Fortunately, you can help by defining an interface for generic AbstractDisplay objects:

function Base.getproperty(d::AbstractDisplay, name::Symbol)
    if name === :height
        return getfield(d, :height)::Int
    elseif name === :width
        return getfield(d, :width)::Int
    elseif name === :maker
        return getfield(d, :maker)::Union{String,Symbol}
    end
    return getfield(d, name)
end

Julia's constant propagation will ensure that most accesses of those fields will be determined at compile-time, so this simple change robustly fixes many inference problems.

Fixing Core.Box

Julia issue 15276 is one of the more surprising forms of inference failure; it is the most common cause of a Core.Box annotation. If other variables depend on the Boxed variable, then a single Core.Box can lead to widespread inference problems. For this reason, these are also among the first inference problems you should tackle.

Read this explanation of why this happens and what you can do to fix it. If you are directed to find Core.Box inference triggers via suggest, you may need to explore around the call site a bit– the inference trigger may be in the closure itself, but the fix needs to go in the method that creates the closure.

Use of ascend is highly recommended for fixing Core.Box inference failures.

Handling edge cases

You can sometimes get invalidations from failing to handle "formal" possibilities. For example, operations with regular expressions might return a Union{Nothing, RegexMatch}. You can sometimes get poor type inference by writing code that fails to take account of the possibility that nothing might be returned. For example, a comprehension

ms = [m.match for m in match.((rex,), my_strings)]

might be replaced with

ms = [m.match for m in match.((rex,), my_strings) if m !== nothing]

and return a better-typed result.