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.):
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:
773.333 ns (34 allocations: 1.61 KiB)
77.403 ns (7 allocations: 832 bytes)
25.251 ns (1 allocation: 368 bytes)
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.
"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.