How come a struct is type unstable, if too many different field types?

Hello!

The real code is a bit of a mess, so just giving an example and looking for a hint. I have defined a struct as:

@with_kw struct CLL{I,T,D}
    Points::Vector{SVector{D,T}}
    MaxValidIndex::Base.RefValue{Int} = Ref(0)
    CutOff::T
    CutOffSquared::T                         = CutOff^2
    Padding::I                               = 2
    HalfPad::I                               = convert(typeof(Padding),Padding//2)
    ZeroOffset::I                            = 1 #Since we start from 0 when generating cells

    ListOfInteractions::Vector{Tuple{I,I,T}} = Vector{Tuple{Int,Int,getsvecT(eltype(Points))}}(undef,length(Points)^2)
    Stencil::Vector{NTuple{D, I}}            = neighbors(Val(getsvecD(eltype(Points))) )
    
    Cells::Vector{NTuple{D, I}}              = ExtractCells(Points,CutOff,Val(getsvecD(eltype(Points))))
    UniqueCells::Vector{NTuple{D, I}}        = unique(Cells)
    Nmax::I                                  = maximum(reinterpret(Int,@view(Cells[:]))) + ZeroOffset
    Layout::Array{Vector{I}, D}              = GenerateM(Nmax,ZeroOffset,HalfPad,Padding,Cells,Val(getsvecD(eltype(Points))))
end

In which I is an Integer type, T is a float type and D is dimensions, 2d or 3d.

I have noticed that when I use @code_warntype on this struct I get:

MethodInstance for Core.kwcall(::@NamedTuple{Points::Vector{SVector{2, Float64}}, CutOff::Float64}, ::Type{CLL})
  from kwcall(::NamedTuple, ::Type{CLL}) @ Main c:\git\SPHExample\example\TempRepoCLL.jl:7
Arguments
  _::Core.Const(Core.kwcall)
  @_2::@NamedTuple{Points::Vector{SVector{2, Float64}}, CutOff::Float64}
  @_3::Type{CLL}
Locals
  @_4::Any
  Points::Vector{SVector{2, Float64}}
  MaxValidIndex::Base.RefValue{Int64}
  CutOff::Float64
  CutOffSquared::Float64
  Padding::Int64
  HalfPad::Int64
  ZeroOffset::Int64
  ListOfInteractions::Vector{Tuple{Int64, Int64, Float64}}
  Stencil::Vector{Tuple{Int64, Int64}}
  Cells::Vector{Tuple{Int64, Int64}}
  UniqueCells::Vector{Tuple{Int64, Int64}}
  Nmax::Int64
  Layout::Matrix{Vector{Int64}}
  @_18::SubArray{Tuple{Int64, Int64}, 1, Vector{Tuple{Int64, Int64}}, Tuple{Base.Slice{Base.OneTo{Int64}}}, true}
Body::CLL{Int64, Float64, 2}
1 ──        Core.NewvarNode(:(@_4))
β”‚    %2   = Core.isdefined(@_2, :Points)::Core.Const(true)
└───        goto #3 if not %2
2 ──        (@_4 = Core.getfield(@_2, :Points))
└───        goto #4
3 ──        Core.Const(:(Core.UndefKeywordError(:Points)))
└───        Core.Const(:(@_4 = Core.throw(%6)))
4 ┄─ %8   = @_4::Vector{SVector{2, Float64}}
β”‚           (Points = %8)
β”‚    %10  = Core.isdefined(@_2, :MaxValidIndex)::Core.Const(false)
└───        goto #6 if not %10
5 ──        Core.Const(:(@_4 = Core.getfield(@_2, :MaxValidIndex)))
└───        Core.Const(:(goto %15))
6 ┄─        (@_4 = Main.Ref(0))
β”‚    %15  = @_4::Base.RefValue{Int64}
β”‚           (MaxValidIndex = %15)
β”‚    %17  = Core.isdefined(@_2, :CutOff)::Core.Const(true)
└───        goto #8 if not %17
7 ──        (@_4 = Core.getfield(@_2, :CutOff))
└───        goto #9
8 ──        Core.Const(:(Core.UndefKeywordError(:CutOff)))
└───        Core.Const(:(@_4 = Core.throw(%21)))
9 ┄─ %23  = @_4::Float64
β”‚           (CutOff = %23)
β”‚    %25  = Core.isdefined(@_2, :CutOffSquared)::Core.Const(false)
└───        goto #11 if not %25
10 ─        Core.Const(:(@_4 = Core.getfield(@_2, :CutOffSquared)))
└───        Core.Const(:(goto %34))
11 β”„ %29  = Main.:^::Core.Const(^)
β”‚    %30  = CutOff::Float64
β”‚    %31  = Core.apply_type(Base.Val, 2)::Core.Const(Val{2})
β”‚    %32  = (%31)()::Core.Const(Val{2}())
β”‚           (@_4 = Base.literal_pow(%29, %30, %32))
β”‚    %34  = @_4::Float64
β”‚           (CutOffSquared = %34)
β”‚    %36  = Core.isdefined(@_2, :Padding)::Core.Const(false)
└───        goto #13 if not %36
12 ─        Core.Const(:(@_4 = Core.getfield(@_2, :Padding)))
└───        Core.Const(:(goto %41))
13 β”„        (@_4 = 2)
β”‚    %41  = @_4::Core.Const(2)
β”‚           (Padding = %41)
β”‚    %43  = Core.isdefined(@_2, :HalfPad)::Core.Const(false)
└───        goto #15 if not %43
14 ─        Core.Const(:(@_4 = Core.getfield(@_2, :HalfPad)))
└───        Core.Const(:(goto %50))
15 β”„ %47  = Main.typeof(Padding::Core.Const(2))::Core.Const(Int64)
β”‚    %48  = (Padding::Core.Const(2) // 2)::Core.Const(1//1)
β”‚           (@_4 = Main.convert(%47, %48))
β”‚    %50  = @_4::Core.Const(1)
β”‚           (HalfPad = %50)
β”‚    %52  = Core.isdefined(@_2, :ZeroOffset)::Core.Const(false)
└───        goto #17 if not %52
16 ─        Core.Const(:(@_4 = Core.getfield(@_2, :ZeroOffset)))
└───        Core.Const(:(goto %57))
17 β”„        (@_4 = 1)
...

It does make sense to me why it is Any, from the code snippet above I see that _@4 is overwritten a lot of times.

I noticed that if I limit my struct to the first four fields, i.e. before hitting I field type, then it does not come up as Any in @code_warntype but up as a yellow Union.. which is great. It seems like the β€œfourth” type breaks type stability?

How would I go about removing this kind of type instability, can anyone point to an example?

Kind regards

I think this has something to do with the @with_kw you use in the definition but I am not sure. However this type instability is well contained so shouldn’t cause any problems. As you can see the return type is well inferred:

Body::CLL{Int64, Float64, 2}

So unless you create a very large amount of these structs you shouldn’t worry :slight_smile:

2 Likes

That’s the Union-splitting cutoff, basically the compiler branches to type-stable copies of the code for small Union (2 or 3) type instabilities. Compiling more copies incurs costs for more hardware-related reasons than just the branching. Bear in mind that larger Unions may be inferred but not do Union-splitting, in which case they are printed red, which unfortunately doesn’t show when pasted to discourse.

The @_4 variable checks if your keyword arguments NamedTuple contains a value for a field, and if not it computes the given default expression or throws an error. This is not something the macro does but how keyword arguments work in general. A method with enough keyword arguments to run into >3 types will also use an ::Any-inferred variable.

The good news is a bunch of local variables sharing names with the struct’s fields take the value from @_4 right after it’s done for each field, and the variables’ types are concretely inferred each time. Bearing in mind the full @code_warntype wasn’t posted and further static analysis (like JET.jl) and runtime profiling (like BenchmarkTools.jl) are absent, I would hazard a guess that the overall @_4::Any has little if any runtime costs.

1 Like