# For statement type instability

Hello all. I encountered the same problem as in the title. Sorry if this is a known problem.
`@code_warntype` warns `@_2::Union{Nothing, Tuple{Union{One, Two}, Int64}}` has type instability.
How can I solve this problem?

``````abstract type AbstractNumber end

struct One <: AbstractNumber end
struct Two <: AbstractNumber end

function test()
for number in (One(), Two())
end
end
``````
``````julia> @code_warntype test()
MethodInstance for test()
from test() @ Main ~/dev/test/test.jl:6
Arguments
#self#::Core.Const(test)
Locals
@_2::Union{Nothing, Tuple{Union{One, Two}, Int64}}
number::Union{One, Two}
Body::Nothing
1 â”€ %1  = Main.One()::Core.Const(One())
â”‚   %2  = Main.Two()::Core.Const(Two())
â”‚   %3  = Core.tuple(%1, %2)::Core.Const((One(), Two()))
â”‚         (@_2 = Base.iterate(%3))
â”‚   %5  = (@_2::Core.Const((One(), 2)) === nothing)::Core.Const(false)
â”‚   %6  = Base.not_int(%5)::Core.Const(true)
â””â”€â”€       goto #4 if not %6
2 â”„ %8  = @_2::Union{Tuple{One, Int64}, Tuple{Two, Int64}}
â”‚         (number = Core.getfield(%8, 1))
â”‚   %10 = Core.getfield(%8, 2)::Int64
â”‚         (@_2 = Base.iterate(%3, %10))
â”‚   %12 = (@_2 === nothing)::Bool
â”‚   %13 = Base.not_int(%12)::Bool
â””â”€â”€       goto #4 if not %13
3 â”€       goto #2
4 â”„       return nothing
``````

You are looping over a collect of multiple types so `number` has different types, but Iâ€™m not sure what your question is or what youâ€™re trying to achieve?

I am sorry the lack of clarity.
`@code_warntype` warns `@_2::Union{Nothing, Tuple{Union{One, Two}, Int64}}` has type instability.
How can I solve this problem?

You donâ€™t need to do anything. In this case Julia will just unroll the loop later down the compilation pipeline and you wonâ€™t notice anything from this type-instability. In any case, the number of types is also small enough for union-splitting (where the magic number is 4 IIRC).

As a general rule of thumb: Small unions of concrete types are likely fine. You donâ€™t need to fix anything. Iâ€™d say fixing â€śtype instabilitiesâ€ť just for the sake of it is rather pointless. If you want to do it for performance optimization, you need to demonstrate that this is a performance problem to begin with.

That being said, in general you should strive to avoid code like this in Julia:

1. generally avoid containers of abstract/heterogeneous types â€¦ yes, there are specialized cases like `Vector{Union{Missing,T}}` that the compiler does a good job with thanks to union-splitting, but they are the exception that proves the rule
2. be cautious about using types to encode values as in `One <: AbstractNumber`, `Two <: AbstractNumber`. This can be useful, e.g. `Ď€` has its own type, but itâ€™s a specialized tool because of point (1)
3 Likes

This code example is a simplified version of my original code, so it does not make much sense.

@abraemer
In fact, my original code caused an unintended memory allocation that I thought should be resolved. In the end, by adding `@inline`, I was able to properly optimize the code and eliminate the memory allocations.
Looking at the Julia base functions, `@inline` is rarely used, so I didnâ€™t think it was necessary.

@stevengj
I knew about that article, but did not know that the rule applied to Tuple.

Itâ€™s true that tuples of heterogeneous types are totally fine, as long as the tuple type is known to the compiler; itâ€™s no different than a `struct` with heterogeneous members. But you have to be very careful about iteration over a heterogeneous tuple.

`for` loops over heterogeneous tuples, in particular, are not unrolled (even though the compiler knows the length) and can lead to type-instability issues for heterogeneous tuples. Various threads have complained about this, e.g. Unrolling loops over tuples - why so hard? - #15 by matthias314 and Manually unroll operations with objects of tuple - #2 by mauro3, and there is an old package Unrolled.jl that tries to do this for you.

However, whatâ€™s less known is that certain functions like the `map`, `mapreduce`, and (as of Julia 1.8 thanks to julia#31901) `foreach` functions can be completely unrolled for tuple arguments, so e.g.

``````foreach((One(), Two(),3,"4")) do number

end
``````

is completely unrolled IIRC, and it can potentially be type stable depending on what you do inside the loop. (One difficulty with `foreach` is that, because the â€śloop bodyâ€ť is a closure, you can easily get bitten by julia#15276 if you want to assign to a local variable inside the loop.)

4 Likes

Thank you for precious information!!

This is intriguing, but is it even better to be type-stable here? Yes, it seems faster (and I saw the other thread, and opened PR on it for printing), but if I understand this then the loop will be unrolled, and does that mean many compiled versions? I.e. a lot more code? For maybe little gain, and could it be worse in the end?

It is type stable to unroll a loop over a heterogeneous tuple. The compiler knows the types of the tuple entries.

Iâ€™m not saying itâ€™s not type-stable, or that itâ€™s worse per se, just if unrolled possibly (and assumed the alternative when not type-stable wouldnâ€™t be unrolled).

The question was about if the unrolling might be bad; i.e. using `foreach`. With `for` is unstable right, and not unrolled?

1 Like