# How to efficiently create a struct to store parametric structs in tuples

**URL:** https://discourse.julialang.org/t/how-to-efficiently-create-a-struct-to-store-parametric-structs-in-tuples/112931
**Category:** New to Julia
**Tags:** question
**Created:** [April 14, 2024, 5:48pm UTC](https://discourse.julialang.org/t/how-to-efficiently-create-a-struct-to-store-parametric-structs-in-tuples/112931 "2024-04-14T17:48:05Z")
**Posts on this page:** 3
**Page:** 1

<div class="post-metadata">

### Author: ![walexaindre](https://sea2.discourse-cdn.com/julialang/user_avatar/discourse.julialang.org/walexaindre/32/208375_2.png) [@walexaindre](https://discourse.julialang.org/u/walexaindre)
#### Post date: [April 14, 2024, 5:48pm UTC](https://discourse.julialang.org/t/how-to-efficiently-create-a-struct-to-store-parametric-structs-in-tuples/112931/1 "2024-04-14T17:48:05Z")

</div>

Hi!, I’m working with a PDE and I want suggestions on how properly store components inside a struct with no type instabilities considering that components are parametric structs too.

For a MWE imagine something minimal like this (This behaves like what I need / want but don’t make any sense nor is correct.):

```julia
using BenchmarkTools

struct Offset{M}
    offsets::NTuple{M, Int}
end

Base.length(o::Offset{M}) where{M} = M

struct SymmetricOffsets{N}
    offsets::NTuple{N,Offset}
end

Base.length(o::SymmetricOffsets) = sum(length,o.offsets)

function apply_offsets(val::NTuple{N,Int},offsets::SymmetricOffsets{N}) where N

    res = fill(val,length(offsets))
    
    idx = 1
    dim = 1
    for offset in offsets.offsets
        for oidx in offset.offsets
            tmp = val[dim] + oidx
            res[idx] = Base.setindex(res[idx],tmp,dim)
            idx += 1
        end
        dim += 1
    end
    res
end

struct SymmetricOffsets2{M1,M2,M3}
    offsets::Tuple{Offset{M1},Offset{M2},Offset{M3}}
end

Base.length(o::SymmetricOffsets2{M1,M2,M3}) where {M1,M2,M3} = M1+M2+M3

function apply_offsets(val::NTuple{N,Int},offsets::SymmetricOffsets2{M1,M2,M3}) where {N,M1,M2,M3}

    res = fill(val,length(offsets))
    
    idx = 1
    for dim in 1:3
        for oidx in offsets.offsets[dim].offsets
            tmp = val[dim] + oidx
            res[idx] = Base.setindex(res[idx],tmp,dim)
            idx += 1
        end
    end

    res
end

struct SymmetricOffsets3{M1,M2,M3}
    o1::Offset{M1}
    o2::Offset{M2}
    o3::Offset{M3}
end

Base.length(o::SymmetricOffsets3{M1,M2,M3}) where {M1,M2,M3} = M1+M2+M3

function apply_offsets(val::NTuple{N,Int},offsets::SymmetricOffsets3{M1,M2,M3}) where {N,M1,M2,M3}

    res = fill(val,length(offsets))
    
    idx = 1

    for oidx in offsets.o1.offsets
        tmp = val[1] + oidx
        res[idx] = Base.setindex(res[idx],tmp,1)
        idx += 1
    end

    for oidx in offsets.o2.offsets
        tmp = val[2] + oidx
        res[idx] = Base.setindex(res[idx],tmp,2)
        idx += 1
    end

    for oidx in offsets.o3.offsets
        tmp = val[3] + oidx
        res[idx] = Base.setindex(res[idx],tmp,3)
        idx += 1
    end

    res
end

offset1 = Offset((1,2,3))
offset2 = Offset((4,5,6,7))
offset3 = Offset((8,9,10,11,12))

symmetric_offsets = SymmetricOffsets((offset1,offset2,offset3))
symmetric_offsets2 = SymmetricOffsets2((offset1,offset2,offset3))
symmetric_offsets3 = SymmetricOffsets3(offset1,offset2,offset3)

@btime apply_offsets($(1,2,3),$symmetric_offsets)
@btime apply_offsets($(1,2,3),$symmetric_offsets2)
@btime apply_offsets($(1,2,3),$symmetric_offsets3)

@code_warntype apply_offsets((1,2,3),symmetric_offsets)
@code_warntype apply_offsets((1,2,3),symmetric_offsets2)
@code_warntype apply_offsets((1,2,3),symmetric_offsets3)

```

Note that if I unroll the for loop in SymmetricOffsets2 then I get similar results to SymmetricOffsets3.

Results:

```julia
  773.333 ns (34 allocations: 1.61 KiB)
  77.403 ns (7 allocations: 832 bytes)
  25.251 ns (1 allocation: 368 bytes)

```

```julia
MethodInstance for apply_offsets(::Tuple{Int64, Int64, Int64}, ::SymmetricOffsets{3})
  from apply_offsets(val::Tuple{Vararg{Int64, N}}, offsets::SymmetricOffsets{N}) where N @ Main 
Static Parameters
  N = 3
Arguments
  #self#::Core.Const(apply_offsets)
  val::Tuple{Int64, Int64, Int64}
  offsets::SymmetricOffsets{3}
Locals
  @_4::Union{Nothing, Tuple{Offset, Int64}}
  dim::Int64
  idx::Int64
  res::Any
  @_8::Union{Nothing, Tuple{Int64, Int64}}
  offset::Offset
  oidx::Int64
  tmp::Int64
Body::Any
1 ─ %1 = Main.length(offsets)::Any
│ (res = Main.fill(val, %1))
│ (idx = 1)
│ (dim = 1)
│ %5 = Base.getproperty(offsets, :offsets)::Tuple{Offset, Offset, Offset}
│ (@_4 = Base.iterate(%5))
│ %7 = (@_4::Core.PartialStruct(Tuple{Offset, Int64}, Any[Offset, Core.Const(2)]) === nothing)::Core.Const(false)
│ %8 = Base.not_int(%7)::Core.Const(true)
└── goto #7 if not %8
2 ┄ %10 = @_4::Tuple{Offset, Int64}
│ (offset = Core.getfield(%10, 1))
│ %12 = Core.getfield(%10, 2)::Int64
│ %13 = Base.getproperty(offset, :offsets)::Tuple{Vararg{Int64, M}} where M
│ (@_8 = Base.iterate(%13))
│ %15 = (@_8 === nothing)::Bool
│ %16 = Base.not_int(%15)::Bool
└── goto #5 if not %16
3 ┄ %18 = @_8::Tuple{Int64, Int64}
│ (oidx = Core.getfield(%18, 1))
│ %20 = Core.getfield(%18, 2)::Int64
│ %21 = Base.getindex(val, dim)::Int64
│ (tmp = %21 + oidx)
│ %23 = Base.setindex::Core.Const(Base.setindex)
│ %24 = Base.getindex(res, idx)::Any
│ %25 = tmp::Int64
│ %26 = (%23)(%24, %25, dim)::Any
│ Base.setindex!(res, %26, idx)
│ (idx = idx + 1)
│ (@_8 = Base.iterate(%13, %20))
│ %30 = (@_8 === nothing)::Bool
│ %31 = Base.not_int(%30)::Bool
└── goto #5 if not %31
4 ─ goto #3
5 ┄ (dim = dim + 1)
│ (@_4 = Base.iterate(%5, %12))
│ %36 = (@_4 === nothing)::Bool
│ %37 = Base.not_int(%36)::Bool
└── goto #7 if not %37
6 ─ goto #2
7 ┄ return res

MethodInstance for apply_offsets(::Tuple{Int64, Int64, Int64}, ::SymmetricOffsets2{3, 4, 5})
  from apply_offsets(val::Tuple{Vararg{Int64, N}}, offsets::SymmetricOffsets2{M1, M2, M3}) where {N, M1, M2, M3} @ Main 
Static Parameters
  N = 3
  M1 = 3
  M2 = 4
  M3 = 5
Arguments
  #self#::Core.Const(apply_offsets)
  val::Tuple{Int64, Int64, Int64}
  offsets::SymmetricOffsets2{3, 4, 5}
Locals
  @_4::Union{Nothing, Tuple{Int64, Int64}}
  idx::Int64
  res::Vector{Tuple{Int64, Int64, Int64}}
  @_7::Union{Nothing, Tuple{Int64, Int64}}
  dim::Int64
  oidx::Int64
  tmp::Int64
Body::Vector{Tuple{Int64, Int64, Int64}}
1 ─ %1 = Main.length(offsets)::Core.Const(12)
│ (res = Main.fill(val, %1))
│ (idx = 1)
│ %4 = (1:3)::Core.Const(1:3)
│ (@_4 = Base.iterate(%4))
│ %6 = (@_4::Core.Const((1, 1)) === nothing)::Core.Const(false)
│ %7 = Base.not_int(%6)::Core.Const(true)
└── goto #7 if not %7
2 ┄ %9 = @_4::Tuple{Int64, Int64}
│ (dim = Core.getfield(%9, 1))
│ %11 = Core.getfield(%9, 2)::Int64
│ %12 = Base.getproperty(offsets, :offsets)::Tuple{Offset{3}, Offset{4}, Offset{5}}
│ %13 = Base.getindex(%12, dim)::Union{Offset{3}, Offset{4}, Offset{5}}
│ %14 = Base.getproperty(%13, :offsets)::Union{Tuple{Int64, Int64, Int64}, NTuple{4, Int64}, NTuple{5, Int64}}
│ (@_7 = Base.iterate(%14))
│ %16 = (@_7::Core.PartialStruct(Tuple{Int64, Int64}, Any[Int64, Core.Const(2)]) === nothing)::Core.Const(false)
│ %17 = Base.not_int(%16)::Core.Const(true)
└── goto #5 if not %17
3 ┄ %19 = @_7::Tuple{Int64, Int64}
│ (oidx = Core.getfield(%19, 1))
│ %21 = Core.getfield(%19, 2)::Int64
│ %22 = Base.getindex(val, dim)::Int64
│ (tmp = %22 + oidx)
│ %24 = Base.setindex::Core.Const(Base.setindex)
│ %25 = Base.getindex(res, idx)::Tuple{Int64, Int64, Int64}
│ %26 = tmp::Int64
│ %27 = (%24)(%25, %26, dim)::Tuple{Int64, Int64, Int64}
│ Base.setindex!(res, %27, idx)
│ (idx = idx + 1)
│ (@_7 = Base.iterate(%14, %21))
│ %31 = (@_7 === nothing)::Bool
│ %32 = Base.not_int(%31)::Bool
└── goto #5 if not %32
4 ─ goto #3
5 ┄ (@_4 = Base.iterate(%4, %11))
│ %36 = (@_4 === nothing)::Bool
│ %37 = Base.not_int(%36)::Bool
└── goto #7 if not %37
6 ─ goto #2
7 ┄ return res

MethodInstance for apply_offsets(::Tuple{Int64, Int64, Int64}, ::SymmetricOffsets3{3, 4, 5})
  from apply_offsets(val::Tuple{Vararg{Int64, N}}, offsets::SymmetricOffsets3{M1, M2, M3}) where {N, M1, M2, M3} @ Main 
Static Parameters
  N = 3
  M1 = 3
  M2 = 4
  M3 = 5
Arguments
  #self#::Core.Const(apply_offsets)
  val::Tuple{Int64, Int64, Int64}
  offsets::SymmetricOffsets3{3, 4, 5}
Locals
  @_4::Union{Nothing, Tuple{Int64, Int64}}
  @_5::Union{Nothing, Tuple{Int64, Int64}}
  @_6::Union{Nothing, Tuple{Int64, Int64}}
  idx::Int64
  res::Vector{Tuple{Int64, Int64, Int64}}
  oidx@_9::Int64
  tmp@_10::Int64
  oidx@_11::Int64
  tmp@_12::Int64
  oidx@_13::Int64
  tmp@_14::Int64
Body::Vector{Tuple{Int64, Int64, Int64}}
1 ── Core.NewvarNode(:(@_4))
│ Core.NewvarNode(:(@_5))
│ %3 = Main.length(offsets)::Core.Const(12)
│ (res = Main.fill(val, %3))
│ (idx = 1)
│ %6 = Base.getproperty(offsets, :o1)::Offset{3}
│ %7 = Base.getproperty(%6, :offsets)::Tuple{Int64, Int64, Int64}
│ (@_6 = Base.iterate(%7))
│ %9 = (@_6::Core.PartialStruct(Tuple{Int64, Int64}, Any[Int64, Core.Const(2)]) === nothing)::Core.Const(false)
│ %10 = Base.not_int(%9)::Core.Const(true)
└─── goto #4 if not %10
2 ┄─ %12 = @_6::Tuple{Int64, Int64}
│ (oidx@_9 = Core.getfield(%12, 1))
│ %14 = Core.getfield(%12, 2)::Int64
│ %15 = Base.getindex(val, 1)::Int64
│ (tmp@_10 = %15 + oidx@_9)
│ %17 = Base.setindex::Core.Const(Base.setindex)
│ %18 = Base.getindex(res, idx)::Tuple{Int64, Int64, Int64}
│ %19 = tmp@_10::Int64
│ %20 = (%17)(%18, %19, 1)::Tuple{Int64, Int64, Int64}
│ Base.setindex!(res, %20, idx)
│ (idx = idx + 1)
│ (@_6 = Base.iterate(%7, %14))
│ %24 = (@_6 === nothing)::Bool
│ %25 = Base.not_int(%24)::Bool
└─── goto #4 if not %25
3 ── goto #2
4 ┄─ %28 = Base.getproperty(offsets, :o2)::Offset{4}
│ %29 = Base.getproperty(%28, :offsets)::NTuple{4, Int64}
│ (@_5 = Base.iterate(%29))
│ %31 = (@_5::Core.PartialStruct(Tuple{Int64, Int64}, Any[Int64, Core.Const(2)]) === nothing)::Core.Const(false)
│ %32 = Base.not_int(%31)::Core.Const(true)
└─── goto #7 if not %32
5 ┄─ %34 = @_5::Tuple{Int64, Int64}
│ (oidx@_11 = Core.getfield(%34, 1))
│ %36 = Core.getfield(%34, 2)::Int64
│ %37 = Base.getindex(val, 2)::Int64
│ (tmp@_12 = %37 + oidx@_11)
│ %39 = Base.setindex::Core.Const(Base.setindex)
│ %40 = Base.getindex(res, idx)::Tuple{Int64, Int64, Int64}
│ %41 = tmp@_12::Int64
│ %42 = (%39)(%40, %41, 2)::Tuple{Int64, Int64, Int64}
│ Base.setindex!(res, %42, idx)
│ (idx = idx + 1)
│ (@_5 = Base.iterate(%29, %36))
│ %46 = (@_5 === nothing)::Bool
│ %47 = Base.not_int(%46)::Bool
└─── goto #7 if not %47
6 ── goto #5
7 ┄─ %50 = Base.getproperty(offsets, :o3)::Offset{5}
│ %51 = Base.getproperty(%50, :offsets)::NTuple{5, Int64}
│ (@_4 = Base.iterate(%51))
│ %53 = (@_4::Core.PartialStruct(Tuple{Int64, Int64}, Any[Int64, Core.Const(2)]) === nothing)::Core.Const(false)
│ %54 = Base.not_int(%53)::Core.Const(true)
└─── goto #10 if not %54
8 ┄─ %56 = @_4::Tuple{Int64, Int64}
│ (oidx@_13 = Core.getfield(%56, 1))
│ %58 = Core.getfield(%56, 2)::Int64
│ %59 = Base.getindex(val, 3)::Int64
│ (tmp@_14 = %59 + oidx@_13)
│ %61 = Base.setindex::Core.Const(Base.setindex)
│ %62 = Base.getindex(res, idx)::Tuple{Int64, Int64, Int64}
│ %63 = tmp@_14::Int64
│ %64 = (%61)(%62, %63, 3)::Tuple{Int64, Int64, Int64}
│ Base.setindex!(res, %64, idx)
│ (idx = idx + 1)
│ (@_4 = Base.iterate(%51, %58))
│ %68 = (@_4 === nothing)::Bool
│ %69 = Base.not_int(%68)::Bool
└─── goto #10 if not %69
9 ── goto #8
10 ┄ return res

```

My questions are:

What is the most performant and easy way to have different parametric components stored inside a struct ?

Now, I’m thinking that this is only achievable with metaprogramming and generating combinations of parameters for different cases (N). Is that right?

For more context, I’m solving a PDE with any amount of components (Usually between 2 and 8) and for every component I need to store a pair of functions and some default values.

```julia
"PDE component"
struct SchrodingerPDEComponent{Tv,Fn,Hn}
    σ::Tv #Dispersion coefficient
    f::Fn 
    ψ::Hn #Initial condition
end

"Generic handler for non polynomic potentials"
struct SchrodingerPDENonPolynomic{N,M,Tv,Comp<:SchrodingerPDEComponent{Tv},Potential} <: SchrodingerPDE
    boundaries::NTuple{N,NTuple{2,Tv}}
    components::NTuple{M,Comp}
    F::Potential
    T::Tv
end

```

However, this kind of problem of store parametric structs inside other structs happens to me in a very wide varierty of parts of my solver and I want some insights on how to do that in a proper and Julian way. I don’t want to generate my structs and functions with macros but I’m open to it if that is the most efficient way to do that.

---

<div class="post-metadata">

### Author: ![Tarny\_GG\_Channie](https://avatars.discourse-cdn.com/v4/letter/t/3bc359/32.png) [@Tarny\_GG\_Channie](https://discourse.julialang.org/u/Tarny_GG_Channie)
#### Post date: [April 14, 2024, 9:31pm UTC](https://discourse.julialang.org/t/how-to-efficiently-create-a-struct-to-store-parametric-structs-in-tuples/112931/2 "2024-04-14T21:31:37Z")

</div>

> [@walexaindre](#):
>
> ```julia
> struct SymmetricOffsets{N}
> offsets::NTuple{N,Offset}
> end
> 
> ```

Here’s an issue. Offset is not fully parameterized here. This means ANY Offset can be in here.

Instead, you could parameterize the entire tuple.

```julia
struct SymmetricOffsets{T<:Tuple}
    offsets::T
end

```

---

<div class="post-metadata">

### Author: ![aryavorskiy](https://sea2.discourse-cdn.com/julialang/user_avatar/discourse.julialang.org/aryavorskiy/32/43466_2.png) [@aryavorskiy](https://discourse.julialang.org/u/aryavorskiy)
#### Post date: [April 14, 2024, 10:18pm UTC](https://discourse.julialang.org/t/how-to-efficiently-create-a-struct-to-store-parametric-structs-in-tuples/112931/3 "2024-04-14T22:18:28Z")

</div>

Hi!

The general rule of thumb when defining parametric structs is that every field must have strictly defined type when all parameters are set. Let’s take a look at your example:

```julia
struct Offset{M}
    offsets::NTuple{M, Int}
end

struct SymmetricOffsets{N}
    offsets::NTuple{N,Offset}
end

```

The `Offset` types follows this rule - the `offsets` field of `Offset{2}` will be always `NTuple{2,Int}`, or, in other words, `Tuple{Int,Int}`. However, with `SymmetricOffset{2}` the type of `offsets` is not fully defined: it can be `Tuple{Offset{1}, Offset{2}}`, or `Tuple{Offset{3}, Offset{4}}` - you get the point.

To avoid this, you can make the type of `offsets` a parameter - like this:

```julia
struct SymmetricOffsets{TupleT}
    offsets::TupleT
end

```

This may look a bit redundant, but it makes the compiler’s work a lot easier. To enforce some type-checks for convenience, define an [inner constructor](https://docs.julialang.org/en/v1/manual/constructors/#man-inner-constructor-methods):

```julia
struct SymmetricOffsets{TupleT}
    offsets::TupleT
    offsets(offsets::T) where {T<:Tuple{Vararg{Offset}}} = new{T}(offsets)
end

```

This will make the performance of `SymmetricOffsets` and `SymmetricOffsets2` identical.

To make it as fast as `SymmetricOffsets3`, you need to avoid iterating over a tuple with undefined eltype (e. g. in your outer loop). This can be done by replacing it with recursion. Since we never iterate over the tuple explicitly, there will be no type instability.

```julia
const SymmetricOffsetsN{N} = SymmetricOffsets{<:Tuple{Vararg{Any,N}}}
@inline function apply_offsets!(res, val, idx, dim, offsets)
    isempty(offsets) && return
    offset = first(offsets)
    for oidx in offset.offsets
        tmp = val[dim] + oidx
        res[idx] = Base.setindex(res[idx],tmp,dim)
        idx += 1
    end
    apply_offsets!(res, val, idx, dim+1, Base.tail(offsets))
end
function apply_offsets(val::NTuple{N,Int},offsets::SymmetricOffsetsN{N}) where N
    res = fill(val,length(offsets))
    apply_offsets!(res, val, 1, 1, offsets.offsets)
    res
end

```

These are the benchmarks I got:

```julia
  37.462 ns (1 allocation: 368 bytes)
  132.453 ns (7 allocations: 832 bytes)
  38.066 ns (1 allocation: 368 bytes)

```

**Also note that if all `Offset`s were of equal lengths, we would not have needed all this trickery**
