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

**URL:** https://discourse.julialang.org/t/how-come-a-struct-is-type-unstable-if-too-many-different-field-types/110447
**Category:** Performance
**Created:** [February 20, 2024, 9:38am UTC](https://discourse.julialang.org/t/how-come-a-struct-is-type-unstable-if-too-many-different-field-types/110447 "2024-02-20T09:38:45Z")
**Posts on this page:** 3
**Page:** 1

<div class="post-metadata">

### Author: ![Ahmed\_Salih](https://sea2.discourse-cdn.com/julialang/user_avatar/discourse.julialang.org/ahmed_salih/32/206579_2.png) [@Ahmed\_Salih](https://discourse.julialang.org/u/Ahmed_Salih)
#### Post date: [February 20, 2024, 9:38am UTC](https://discourse.julialang.org/t/how-come-a-struct-is-type-unstable-if-too-many-different-field-types/110447/1 "2024-02-20T09:38:45Z")

</div>

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:

```julia
@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:

```julia
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

---

<div class="post-metadata">

### Author: ![abraemer](https://sea2.discourse-cdn.com/julialang/user_avatar/discourse.julialang.org/abraemer/32/51403_2.png) [@abraemer](https://discourse.julialang.org/u/abraemer)
#### Post date: [February 20, 2024, 10:44am UTC](https://discourse.julialang.org/t/how-come-a-struct-is-type-unstable-if-too-many-different-field-types/110447/2 "2024-02-20T10:44:10Z")

</div>

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:

```julia
Body::CLL{Int64, Float64, 2}

```

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

---

<div class="post-metadata">

### Author: ![Benny](https://avatars.discourse-cdn.com/v4/letter/b/49beb7/32.png) [@Benny](https://discourse.julialang.org/u/Benny)
#### Post date: [February 21, 2024, 12:38am UTC](https://discourse.julialang.org/t/how-come-a-struct-is-type-unstable-if-too-many-different-field-types/110447/3 "2024-02-21T00:38:27Z")

</div>

> [@Ahmed\_Salih](#):
>
> 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?

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.

> [@Ahmed\_Salih](#):
>
> 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.

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.
