Hello all. I encountered the same problem as in the title. Sorry if this is a known problem.
[Added later] @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
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:
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
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)
Thank you for answers.
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.
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.)
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?
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?