Help to avoid a type-instability

Hi!

I need to construct a struct that has a vector with elements of another structure as in this code:

mutable struct A{T}
    value::T
end

mutable struct B
    v::Vector{A}
end

get_data(a::A{T}) where T = a.value
get_data(b::B) = get_data.(b.v)

a1 = A(1)
a2 = A(true)
a3 = A(1.0)

b = B([a1,a2,a3])

Of course, get_data in elements from A are type stable:

julia> @code_warntype get_data(a1)
Variables
  #self#::Core.Compiler.Const(get_data, false)
  a::A{Int64}

Body::Int64
1 ─ %1 = ^[[A^[[DBase.getproperty(a, :value)::Int64
└──      return %1

julia> @code_warntype get_data(a2)
Variables
  #self#::Core.Compiler.Const(get_data, false)
  a::A{Bool}

Body::Bool
1 ─ %1 = Base.getproperty(a, :value)::Bool
└──      return %1

julia> @code_warntype get_data(a3)
Variables
  #self#::Core.Compiler.Const(get_data, false)
  a::A{Float64}

Body::Float64
1 ─ %1 = Base.getproperty(a, :value)::Float64
└──      return %1

However, get_data in elements from B is not:

julia> @code_warntype get_data(b)
Variables
  #self#::Core.Compiler.Const(get_data, false)
  b::B

Body::Union{BitArray{1}, Array}
1 ─ %1 = Base.getproperty(b, :v)::Array{A,1}
β”‚   %2 = Base.broadcasted(Main.get_data, %1)::Base.Broadcast.Broadcasted{Base.Broadcast.DefaultArrayStyle{1},Nothing,typeof(get_data),Tuple{Array{A,1}}}
β”‚   %3 = Base.materialize(%2)::Union{BitArray{1}, Array}
└──      return %3

This is leading to a huge performance hit in my original code. Can anyone help me to avoid this type-instability?

Use A as parameter of B?

But the vector v in B can have an arbitrary number of As with different types. Like v = [A{Int}, A{Bool}, A{Float64}]. Thus, I have no idea how to do this.

How many different types can hold A?

Well, infinite. Because it can also hold user-defined structures.

1 Like

What about using map(get_data,b.v)? as far as I know, map creates an array and widens the array type to hold the elements on demand, resorting to Any as a fallback

I still get the type instability, even if all the elements in v are of the same type:

julia> @code_warntype get_data(b)
Variables
  #self#::Core.Compiler.Const(get_data, false)
  b::B

Body::Array{_A,1} where _A
1 ─ %1 = Base.getproperty(b, :v)::Array{A,1}
β”‚   %2 = Main.map(Main.get_data, %1)::Array{_A,1} where _A
└──      return %2

Not if B has A as parameter:

julia> b
B{A{Int64}}(A{Int64}[A{Int64}(1), A{Int64}(2), A{Int64}(3)])

julia> @code_warntype get_data(b)
Variables
  #self#::Core.Compiler.Const(get_data, false)
  b::B{A{Int64}}

Body::Array{Int64,1}
1 ─ %1 = Base.getproperty(b, :v)::Array{A{Int64},1}
β”‚   %2 = Base.broadcasted(Main.get_data, %1)::Base.Broadcast.Broadcasted{Base.Broadcast.DefaultArrayStyle{1},Nothing,typeof(get_data),Tuple{Array{A{Int64},1}}}
β”‚   %3 = Base.materialize(%2)::Array{Int64,1}
└──      return %3

Also, it probably doesn’t affect type-stability, but does B need to be mutable?

But how can I do that when v can have for example [A{Int64}, A{Bool}, A{Float64}]?

In this example no, but in my original code yes.

1 Like

If you want the freedom of having inhomogeneous vectors with any types, then type-instabilities are probably unavoidable

3 Likes

OK thanks! I though if I knew the type of all the elements in the vector at compile type, then I could make some tuple or something to avoid the type instability.

After this discussion, it seems that it can be improved. We have a way to make this type stable by changing the v to a Tuple is two conditions:

  • The number of elements in v is fixed; or
  • The types of elements in v are equal.

Thus, I think we can improve by having something like Vararg that can define a number of different types :slight_smile:

I am not expert and perhaps this is impossible. However, I opened a issue in Github to discuss this further:

https://github.com/JuliaLang/julia/issues/34640

Using @kristoffer.carlsson with @giordano I could solve this problem:

mutable struct A{T}
    value::T
end

mutable struct B{V <: Tuple}
    v::V
end

get_data(a::A{T}) where T = a.value
get_data(b::B) = map(get_data,b.v)

a1 = A(1)
a2 = A(true)
a3 = A(1.0)

b = B((a1,a2,a3))
julia> @code_warntype get_data(b)
Variables
  #self#::Core.Compiler.Const(get_data, false)
  b::B{Tuple{A{Int64},A{Bool},A{Float64}}}

Body::Tuple{Int64,Bool,Float64}
1 ─ %1 = Base.getproperty(b, :v)::Tuple{A{Int64},A{Bool},A{Float64}}
β”‚   %2 = Main.map(Main.get_data, %1)::Tuple{Int64,Bool,Float64}
└──      return %2

Thanks for the help!

1 Like