refactor: Zero-allocation basis functions and immutable Element

MAJOR PERFORMANCE REFACTORING:

1. Shape functions return tuples instead of allocating vectors:
   - eval_basis!(): Returns NTuple{N,T} directly (zero allocations)
   - eval_dbasis!(): Returns NTuple{N,Vec{D}} directly (zero allocations)
   - API boundary (get_basis/get_dbasis) still returns vectors for compat

2. Element is now immutable with compile-time known structure:
   - connectivity: Vector{UInt} → NTuple{N,UInt}
   - integration_points: Vector{IP} → NTuple{NIP,IP}
   - Element{N,NIP,M,B} parametrized by connectivity/IP count
   - Changed from 'mutable struct' to 'struct'

3. Helper function for immutability:
   - with_integration_points(element, ips) returns new element
   - get_integration_points() returns tuple directly

Benefits:
- Zero allocations in hot paths (basis evaluation)
- Compile-time sizes enable better optimization
- Type stability improvements
- Stack allocation instead of heap

Breaking changes:
- Element.connectivity is now tuple (use collect() for vector)
- Element is immutable (use with_integration_points for updates)

Tests: All 157 tests passing
This commit is contained in:
Jukka Aho
2025-11-09 03:29:36 +02:00
parent 065156b40a
commit 907ec0b183
4 changed files with 61 additions and 46 deletions
+8 -7
View File
@@ -45,27 +45,28 @@ abstract type AbstractBasis{dim} end
# This allows calling methods on both Seg2 and Seg2()
Base.length(B::T) where {T<:AbstractBasis} = length(T)
Base.size(B::T) where {T<:AbstractBasis} = size(T)
eval_basis!(B::T, N, xi) where {T<:AbstractBasis} = eval_basis!(T, N, xi)
eval_dbasis!(B::T, dN, xi) where {T<:AbstractBasis} = eval_dbasis!(T, dN, xi)
# Updated signatures: eval_basis! and eval_dbasis! now return tuples
eval_basis!(B::T, ::Type{U}, xi) where {T<:AbstractBasis,U} = eval_basis!(T, U, xi)
eval_dbasis!(B::T, xi) where {T<:AbstractBasis} = eval_dbasis!(T, xi)
# Allocating versions (convenience wrappers)
# Allocating versions (convenience wrappers) - now they just call and collect
"""
eval_basis(basis::AbstractBasis{dim}, xi) -> Vector{Float64}
Evaluate basis functions at point `xi`, allocating return vector.
See also: [`eval_basis!`](@ref) for non-allocating version.
See also: [`eval_basis!`](@ref) for non-allocating version that returns tuple.
"""
eval_basis(B::AbstractBasis{dim}, xi) where {dim} = eval_basis!(B, zeros(length(B)), xi)
eval_basis(B::AbstractBasis{dim}, ::Type{T}, xi) where {dim,T} = collect(eval_basis!(B, T, xi))
"""
eval_dbasis(basis::AbstractBasis{dim}, xi) -> Vector{Vec{dim, Float64}}
Evaluate basis function derivatives at point `xi`, allocating return vector.
See also: [`eval_dbasis!`](@ref) for non-allocating version.
See also: [`eval_dbasis!`](@ref) for non-allocating version that returns tuple.
"""
eval_dbasis(B::AbstractBasis{dim}, xi) where {dim} = eval_dbasis!(B, zeros(Vec{dim}, length(B)), xi)
eval_dbasis(B::AbstractBasis{dim}, xi) where {dim} = collect(eval_dbasis!(B, xi))
# Declare interface functions (will be implemented by basis generator)
function get_reference_element_coordinates end
+12 -16
View File
@@ -148,15 +148,13 @@ function create_basis(name, description, X::Vector{<:Vecish{D,T}}, basis, dbasis
N = length(X)
@debug "create basis given basis functions and derivatives" name description X basis dbasis
Q = Expr(:block)
for i = 1:N
push!(Q.args, :(N[$i] = $(basis[i])))
end
# Build tuple expression for eval_basis! return: (N1, N2, N3, ...)
basis_tuple_args = [basis[i] for i = 1:N]
basis_tuple = Expr(:tuple, basis_tuple_args...)
V = Expr(:block)
for i = 1:N
push!(V.args, :(dN[$i] = Vec(float.(tuple($(dbasis[:, i]...))))))
end
# Build tuple expression for eval_dbasis! return: (dN1, dN2, dN3, ...)
dbasis_tuple_args = [:(Vec(float.(tuple($(dbasis[:, i]...))))) for i = 1:N]
dbasis_tuple = Expr(:tuple, dbasis_tuple_args...)
if D == 1
unpack = :((u,) = xi)
@@ -187,18 +185,16 @@ function create_basis(name, description, X::Vector{<:Vecish{D,T}}, basis, dbasis
return $X
end
@inline function eval_basis!(::Type{$name}, N::Vector{<:Number}, xi::Vec)
@assert length(N) == $N
# Return tuple directly - zero allocations!
@inline function eval_basis!(::Type{$name}, ::Type{T}, xi::Vec) where T
$unpack
@inbounds $Q
return N
@inbounds return $basis_tuple
end
@inline function eval_dbasis!(::Type{$name}, dN::Vector{<:Vec{$D}}, xi::Vec)
@assert length(dN) == $N
# Return NTuple{N,Vec{D}} directly - zero allocations!
@inline function eval_dbasis!(::Type{$name}, xi::Vec)
$unpack
@inbounds $V
return dN
@inbounds return $dbasis_tuple
end
end
return code