Non critical type-instability?

Suppose the following

struct A{T} v::Vector{T} end

Base.convert(::Type{A{T}}, a::A{S}) where {T,S} = A(convert(Vector{T}, a.v))

Base.promote_rule(::Type{A{T}}, ::Type{A{S}}) where {T,S} = A{promote_type(T,S)}

function *(a::A{T}, b::A{S}) where {T,S}
a,b = promote(a,b)
c = typeof(a)(zero(a.v))
for k in eachindex(a.v)
my_product!(c, a, b, k)
end
return c
end

where my_product! is some customized inlined operation which updates c.v[k].

Let a::A{BigFloat} and b::A{Float64}. Looking at @code_warntype *(a, b) shows that the type of b in red as the union of A{Float64} and A{BigFloat} but the type of a is correctly inferred as A{BigFloat}.

What is the reason for this? I was expecting the compiler to either not know for both or know for both…

Is it critical for performance (assuming my_product!is type stable of course)? I do see a large time differences by using BenchmarkTools, but it is not clear if it only due to the promote step in the definition of *… Is there a macro that can clarify this?

I suspect part of the problem is that you’re re-assigning the output of promote to the same variables a, b. The compiler’s pretty good at handling that sort of thing, but it makes reading @code_warntype unnecessarily confusing since the values pointed to by those variables will have different types before and after that assignment.

What happens if you do a_promoted, b_promoted = promote(a, b) and then do your operations on those?

Interesting, now it is c who is unstable (i.e. Body is in red) despite the fact that a_promoted seems to be well predicted and c = typeof(a_promoted)(zero(a_promoted.v))…

I did a bit more reading about @code_warntype. So, the type of c is in fact properly inferred but the returned type is not properly inferred… This seems very odd since we return c, no?

In fact, does this falls into the comment of @rdeits:

I suspect part of the problem is that you’re re-assigning the output of promote to the same variables a, b . The compiler’s pretty good at handling that sort of thing, but it makes reading @code_warntype unnecessarily confusing since the values pointed to by those variables will have different types before and after that assignment.

Subsequently, can I conclude from this answer that the type instability displayed by @code_warntype is not “really there”?

I am just guessing here, but what happens if you then define the type returned by the function in their signature?

Do you mean Base.:*(a::A{T}, b::A{S})::A{promote_type(T,S)} where {T,S}? If so, I get the error ERROR: TypeError: in Type{...} expression, expected UnionAll, got typeof(promote_type)

Sorry, I did miss this detail, if you depend on a function to know the type of the method I do not think you can do that. I am not expert at that, so I can be wrong, but I think people use @generated for that. Or just typeassert at the call if you know the return type statically in that context.

Oh yes perhaps @generated would solve it… (as I cannot know a priori the return type unless a and b are specified) Thanks for your help!

Nonetheless, I am still curious if that would matter in the end. Indeed, I understood the comment of @rdeits as: " anyways it is a display problem from @code_warntype but the compiler knows what to do".

EDIT:
here is the @generated piece:
@generated Base.:*(a::A, b::B) = :(*(promote(a, b)...)
and the regular multiplication is

function *(a::T, b::T) where {T<:A}
c = T(zero(a.v))
for k in eachindex(a.v)
my_product!(c, a, b, k)
end
return c
end

1 Like

Interesting, now it is c who is unstable (i.e. Body is in red) despite the fact that a_promoted seems to be well predicted and c = typeof(a_promoted)(zero(a_promoted.v)) …

I can’t reproduce that–using a_ and b_ for the promoted variables, everything looks fine to me:

julia> function *(a::A{T}, b::A{S}) where {T,S}
         a_, b_ = promote(a,b)
         c = typeof(a_)(zero(a_.v))
         for k in eachindex(a_.v)
           my_product!(c, a_, b_, k)
         end
         return c
       end
* (generic function with 1 method)
julia> @code_warntype a * b
Variables
  #self#::Core.Compiler.Const(*, false)
  a::A{BigFloat}
  b::A{Float64}
  a_::A{BigFloat}
  @_5::Int64
  b_::A{BigFloat}
  c::A{BigFloat}
  @_8::Union{Nothing, Tuple{Int64,Int64}}
  k::Int64

Body::A{BigFloat}

By the way, a typical way to implement this sort of promoted math operation is to separate the promotion step from the multiplication. That is, you write out a single * function given two inputs of the same type:

julia> function *(a::A{T}, b::A{T}) where {T}
         c = typeof(a)(zero(a.v))
         for k in eachindex(a.v)
           my_product!(c, a, b, k)
         end
         return c
       end

and then a more generic * method for arguments of different types, in which you promote the two arguments to a common type and call * on the result:

julia> function *(a::A{T}, b::A{S}) where {T,S}
         *(promote(a, b)...)
       end

This avoids needing to worry about mixing up the promoted and un-promoted input arguments within the body of the function, and it helps keep your core * function simpler.

1 Like

Indeed, I copied and paste your function * and it works… I must have made a typo when I wrote it, sorry about that.

So it seems that your suggestion agrees with the edit on my last post; except that I used the @generated as suggested by @Henrique_Becker. Now, it seems like a superfluous thing to do as the multiple dispatch should work perfectly, right?

Thanks for your help :slight_smile:

2 Likes