4

I have a question about type instability when accessing the fields of an abstract type (in julia v0.6).

Suppose I have a type hierarchy, all of which share the same instance variable. I know that it is both type-unstable and not-guaranteed to be correct to access the field correctly, since someone could always define a new subtype that lacks the expected variable. However, even when wrapping the member access in a function, the access is still type unstable, and I can't figure out why.

Assume we have a simple type hierarchy:

julia> begin
       abstract type AT end
       mutable struct T1 <: AT
         x::Int
       end
       mutable struct T2 <: AT
         x::Int
       end
       end

Instead of accessing a.x directly, we wrap it in a function barrier:

julia> getX(a::AT)::Int = a.x
>> getX (generic function with 1 method)

julia> @code_warntype getX(T1(1))
Variables:
  #self# <optimized out>
  a::T1

Body:
  begin
      return (Core.getfield)(a::T1, :x)::Int64
  end::Int64

Note that the access through this method is type-stable, since it can infer the type of a to be T1.

However, when I use getX in a context where the compiler can't know the type of the variable ahead-of-time, it's still type-unstable:

julia> foo() = getX(rand([T1(1),T2(2)]))
>> foo (generic function with 1 method)

julia> @code_warntype foo()
Variables:
  #self# <optimized out>
  T <optimized out>

Body:
  begin
      SSAValue(0) = (Core.tuple)($(Expr(:new, :(Main.T1), 1)), $(Expr(:new, :(Main.T2), 2)))::Tuple{T1,T2}
      SSAValue(2) = $(Expr(:invoke, MethodInstance for rand(::Array{AT,1}), :(Main.rand), :($(Expr(:invoke, MethodInstance for copy!(::Array{AT,1}, ::Tuple{T1,T2}), :(Base.copy!), :($(Expr(:foreigncall, :(:jl_alloc_array_1d), Array{AT,1}, svec(Any, Int64), Array{AT,1}, 0, 2, 0))), SSAValue(0))))))
      return (Core.typeassert)((Base.convert)(Main.Int, (Core.getfield)(SSAValue(2), :x)::Any)::Any, Main.Int)::Int64
  end::Int64

Note that it inlined the body of getX, and replaced it with essentially tmp.x::Int64. This surprises me, since I was expecting getX to dispatch to one of the two instantiations of the same definition we saw above, where no assert is necessary since the type is known.


I thought that this does make some sense if getX is actually only defined for the abstract base type AT -- there would be no methods to dispatch to in the way I'm imagining. So I tried redefining getX such that it would generate a specific method for each subtype as follows:

julia> getX(a::T where T<:AT)::Int = a.x
>> getX (generic function with 1 method)

But that is actually an identical definition, and nothing changed:

julia> methods(getX)
>> # 1 method for generic function "getX":
getX(a::AT) in Main at none:1

Any idea how I can get this to work?

NHDaly
  • 7,390
  • 4
  • 40
  • 45
  • `getX` is type stable, and you don't need the type assertion. But `rand([T1(1),T2(2)])` is _not_ type stable. – DNF Mar 30 '18 at 10:50

2 Answers2

3

If you define getX to take subtypes of AT then you have type stable code for foo:

julia> function getX(a::T)::Int where T <: AT
           a.x
       end
getX (generic function with 1 method)

julia> foo() = getX(rand([T1(1),T2(2)]))
foo (generic function with 1 method)

julia> @code_warntype foo()
Variables:
  #self# <optimized out>
  T <optimized out>

Body:
  begin
      SSAValue(0) = (Core.tuple)($(Expr(:new, :(Main.T1), 1)), $(Expr(:new, :(Main.T2), 2)))::Tuple{T1,T2}
      return (Main.getX)($(Expr(:invoke, MethodInstance for rand(::Array{AT,1}), :(Main.rand), :($(Expr(:invoke, MethodInstance for copy!(::Array{AT,1}, ::Tuple{T1,T2}), :(Base.copy!), :($(Expr(:foreigncall, :(:jl_alloc_array_1d), Array{AT,1}, svec(Any, Int64), Array{AT,1}, 0, 2, 0))), SSAValue(0)))))))::Int64
  end::Int64

I'm not sure why this, maybe someone with more knowledge on the matter will shed some light on it. Also, this is only type-stable because getX is guaranteed to return an Int. If you didn't force getX to do this, you wouldn't be guaranteed to return an Int as you might have different AT subtypes that hold non-Ints.

niczky12
  • 4,953
  • 1
  • 24
  • 34
  • Yes you are right, this definitely fixes my problem. And it is more reliable even than manually defining each method, which stops working after 5+ types... So basically my problem was just that I wrote `getX(a::T where T<:AT)::Int` instead of `getX(a::T)::Int where T <: AT`... What is the difference between those? And why does the second one only work with `function ... end` instead of the simpler `f(x) = y` syntax? – NHDaly Apr 08 '18 at 21:41
  • 1
    You could do it as a one-liner: `getX(a::T) where {T <: AT} = a.x::Int`. As for why this works and the other not, I'm not 100% sure and I don't want to just guess. It might be worth referencing the question on the Julia slack or Discourse. – niczky12 Apr 09 '18 at 08:02
  • Ah, yes, thanks for that. I forgot about the brackets syntax. :) – NHDaly Apr 09 '18 at 14:33
0

Ah, so I needed to manually define the different versions of getX myself.


I was mixing up julia's type dispatch mechanism with C++'s template instantiation. I think I incorrectly imagined that julia defines a new version of getX for every T it's invoked with, in the same way as the C++ template mechanism.

In this case, I was almost correct when I said

I was expecting getX to dispatch to one of the two instantiations [...] where no assert is necessary since the type is known.

However, in this case, there aren't actually two different methods to dispatch to -- there's only one. If I actually define the two different methods, the dispatch mechanism can satisfy the type stability:

julia> begin
       abstract type AT end
       mutable struct T1 <: AT
         x::Int
       end
       mutable struct T2 <: AT
         x::Int
       end
       end

julia> getX(a::T1) = a.x
>> getX (generic function with 1 method)

julia> getX(a::T2) = a.x
>> getX (generic function with 2 methods)

julia> foo() = getX(rand([T1(1),T2(2)]))
>> foo (generic function with 1 method)

julia> @code_warntype foo()
Variables:
  #self# <optimized out>
  T <optimized out>

Body:
  begin
      SSAValue(0) = (Core.tuple)($(Expr(:new, :(Main.T1), 1)), $(Expr(:new, :(Main.T2), 2)))::Tuple{T1,T2
}
      return (Main.getX)($(Expr(:invoke, MethodInstance for rand(::Array{AT,1}), :(Main.rand), :($(Expr(:invoke, MethodInstance for copy!(::Array{AT,1}, ::Tuple{T1,T2}), :(Base.copy!), :($(Expr(:foreigncall, :(:jl_alloc_array_1d), Array{AT,1}, svec(Any, Int64), Array{AT,1}, 0, 2, 0))), SSAValue(0)))))))::Int64
  end::Int64

I found the inspiration for fixing this here: https://stackoverflow.com/a/40223936/751061

NHDaly
  • 7,390
  • 4
  • 40
  • 45
  • Although, interestingly, it seems that if the number of types+methods for `getX` hits 5 or more, it goes back to type unstable again. :( Perhaps there's some hard-coded value in there that it can do the calculation for 4 or fewer types but not for more than that? – NHDaly Mar 29 '18 at 20:12