As I’ve worked through a couple projects now, I realized I don’t fully understand the nuances of parametric types, and specifically when used with StaticArrays. My goal of course is to get all the performance I can, but with as little parametric definition as possible. I set up some tests to better understand the type instabilities I consistently see in my models. I basically tested every combination I could think of from no parametric types at all, to sort of partially parameterized types i.e.
struct Type3{T<:AbstractFloat} <: AbstractType1
value::T
array::SVector{3,T}
matrix::SMatrix{3,3,T}
end
to fully parameterized type i.e.
struct Type4{T1<:AbstractFloat,T2<:SVector{3,T1},T3<:SMatrix{3,3,T1}} <: AbstractType1
value::T1
array::T2
matrix::T3
end
and then parameterized vs not parameterized functions and abstract types.
A couple questions:
The following produces 2 allocations per call
abstract type AbstractType1 end
struct Type3{T<:AbstractFloat} <: AbstractType1
value::T
array::SVector{3,T}
matrix::SMatrix{3,3,T}
end
t3 = Type3(1.0, a, m)
f1(in::AbstractType1) = in.matrix * in.array
julia> @btime f1($t3)
113.508 ns (2 allocations: 64 bytes)
I noticed during @code_warntype in later tests that when I had no allocations, SMatrix was defined as SMatrix{3,3,T,9}. For the above case, @code_warntype had SMatrix{3,3,T} in red text. If I change Type3.matrix to be SMatrix{3,3,T,9}, I get:
struct Type8{T<:AbstractFloat} <: AbstractType1 #similar to Type3 but with a 9 in SMatrix
value::T
array::SVector{3,T}
matrix::SMatrix{3,3,T,9}
end
julia> @btime f1($t8)
2.500 ns (0 allocations: 0 bytes)
Question 1: Why is that? I’ve pretty much never defined that 4th type argument in my SMatrices, but I suppose now I will.
Going through my test cases, the minimum parameter definition required to achieve 0 allocations was :
abstract type AbstractType1 end
struct Type9 <: AbstractType1
value::Float64
array::SVector{3,Float64}
matrix::SMatrix{3,3,Float64,9}
end
f1(in::AbstractType1) = in.matrix * in.array
I didn’t have to parameterize the type, abstract type or the function i.e.
abstract type AbstractType2{T} end
f2(in::T) where {T<:AbstractType1} = in.matrix * in.array
This is probably too generic to ask but
Question 2: Are there additional advantages above allocations for parameterizing the type, abstract type or the function? Should it be considered a best practice to always parameterize if it can be?
Perhaps my examples are too simple to really see the nuance, but maybe there’s an obvious answer there. Again, the goal is to not parameterize those if I don’t need to (or to really understand why I need to).
Question 3: Is there an advantage for writing what I called fully parameterized types vs partially parameterized types? i.e.
struct Type3{T<:AbstractFloat} <: AbstractType1
value::T
array::SVector{3,T}
matrix::SMatrix{3,3,T}
end
struct Type4{T1<:AbstractFloat,T2<:SVector{3,T1},T3<:SMatrix{3,3,T1}} <: AbstractType1
value::T1
array::T2
matrix::T3
end
Obviously for really big structs, Type3 would be preferred vs defining a T for every field, but I have seen code that does that. I’m wondering if maybe it’s safer and makes more sense to the compiler to parameterize every field?
I feel like I always go over board with parameterization, yet still run in to type instabilities regularly. I may have asked the questions in a weird way since I clearly don’t fully understand these concepts yet, but I’m hoping to gain some more intuition on these topics.