# 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
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
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
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

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