Warntype when accessing Vector{AbstractType}

hello, I could not resolve the problem like the following: where the field in a struct is a Vector of abstract type, warntype would be triggered when accessing the content of the vector. Any help would be appreciated. thanks.

struct Test
    v::Vector{Real}

    function Test(a::Real, b::Real)
        ab = Vector{Real}(undef, 2)
        ab[1] = a
        ab[2] = b
        new(v)
    end
end

function fun(obj::Test)
    println(obj.v[1])
    println(obj.v[2])
end

obj = Test(0, 1.1)
fun(obj)
@code_warntype fun(obj)

julia> @code_warntype fun(obj)
Body::Nothing
1 ─ %1 = (Base.getfield)(obj, :v)::Array{Real,1}
│   %2 = (Base.arrayref)(true, %1, 1)::Real
│        (Main.println)(%2)
│   %4 = (Base.getfield)(obj, :v)::Array{Real,1}
│   %5 = (Base.arrayref)(true, %4, 2)::Real
│   %6 = (Main.println)(%5)::Core.Compiler.Const(nothing, false)
└──      return %6

in fact, I tried to define a “barrier” for it, but at the end the barrier itself has warntype also:

function barrier_v1(v::Vector{Real})::Int64
    v[1]
end

julia> barrier_v1(obj.v)
0

julia> @code_warntype barrier_v1(obj.v)
Body::Int64
1 ─ %1  = Main.Int64::Core.Compiler.Const(Int64, false)
│   %2  = (Base.arrayref)(true, v, 1)::Real
│   %3  = (isa)(%2, Int64)::Bool
└──       goto #3 if not %3
2 ─ %5  = π (%2, Int64)
└──       goto #4
3 ─ %7  = (Base.convert)(%1, %2)::Any
└──       goto #4
4 ┄ %9  = φ (#2 => %5, #3 => %7)::Any
│         (Core.typeassert)(%9, %1)
│   %11 = π (%9, Int64)
└──       return %11

That’s how it is, see Performance Tips of the manual.

1 Like

so, the only workaround is to define parametric type?

Well, you could do:

struct Test
  v::Vector{<:Real}
...
end

then you can put, say, a Vector{Int} into Test.v and then a function barrier will work. But if you have a Vector{Real} then a function barrier cannot work because after the barrier it is still an abstract eltype.

hm… Vector{<:Real} not work because the each eltype could be different…

parametric type is fine, except when the number of (dynamically determined) types are unknown.

what I mean is, if the number is known, we can define like: (in this example, the number is 2)

struct Test2{S<:Real, T<:Real}
    v1::S
    v2::T

    function Test2(a::SS, b::TT) where {SS<:Real, TT<:Real}
        new{SS, TT}(a, b)
    end
end

however, things get complicated when the “number” is not known, in such case rather than passing a and b, we would like to pass a Vector{Real} into the constructor, and the number of “parametric type” would equal the length of the vector… how to do that???

You can use a tuple instead of a vector. But in this case it sounds like it’d be okay to have a type instability here. If you want to dynamically collect a group of things together — and you don’t know ahead of time what types they’ll be — then trying to communicate those types to Julia ahead of time will be challenging.

Type instabilities are just fine in situations like this. You don’t need absolutely every function you write to have a “clean” code_warntype.

2 Likes

could you show an example that uses a tuple would give better type stability than vector? thanks.

why? isn’t each warntype means slow execution?

Is this in a performance critical section of your code? Did you profile it? If not, just leave it. If it is, it will be reasonably tricky to get fast because operating on a bunch of different things is slow. Except if it is just a small union of different types, then you should encode that. Reference in How to tell if a type is an efficient small type union? might help.

1 Like

Not necessarily. There is certainly a cost to type-unstable code, but it may not have a significant effect on the overall speed of your code. The key thing to remember is that once you pass a value into a function, no matter whether Julia inferred the type of that value or not, the function will be equally fast.

For example, let’s say your code looks like

for i in x
  f(i)
end

if your x is a Vector{Real}, then there will be some time spent at each iteration of the loop looking up the actual type of each element i. But the speed of executing f(i) will be exactly the same. So you can approximate the time spent in that loop as:

time_abstract_loop = (time_type_lookup + time_f_i) * length(x)

If your x is a container of concrete elements, like Vector{Float64}, then that type lookup time goes away, so the time in the loop can be approximated as:

time_concrete_loop = (time_f_i) * length(x)

The slowdown from type instability is the ratio of those two times:

slowdown = time_abstract_loop / time_concrete_loop 
         = (time_type_lookup + time_f_i) / time_f_i

I usually guess about 100ns for time_type_lookup, so if your time_f_i is just a few nanoseconds, then that slowdown can be huge. But if your time_f_i is large, then you should expect to see slowdown very close to 1, meaning the overall speed of your code won’t be affected noticeably.

8 Likes

I just also want to emphasize that there’s a big advantage to type-unstable code — it can perform many dynamic behaviors that aren’t possible (or easy) otherwise. Don’t be scared of type instabilities unless they’re actually appearing within your hot loop as @rdeits nicely describes.

3 Likes

<post deleted: it was answered above, sorry…>