mirror of
https://github.com/JuliaFEM/JuliaFEM.jl.git
synced 2026-08-19 11:43:47 +00:00
84708cce2e
- Tests 5 strategies for accessing basis functions and derivatives
- Strategy 1: Tuple return + getindex (natural Julia)
- Strategy 2: @generated function (compile-time specialization)
- Strategy 3: Val dispatch (type-stable index)
- Strategy 4: BasisFunctions struct with getindex (most Julian)
- Strategy 5: Separate functions per basis (extreme specialization)
- Use case: Tet10 quadratic tetrahedron (10 nodes, workhorse for 3D FEM)
- Benchmarks both "access all" (element assembly) and "access single" (nodal assembly)
- Tests derivatives as Vec{3} tuples for gradient computation
- Focus: zero allocation, type stability, inlineability
- 412 lines evaluating access pattern performance for GPU-friendly design
413 lines
14 KiB
Julia
413 lines
14 KiB
Julia
# Benchmark: Basis Function Access Patterns for Tet10 (Realistic 3D Case)
|
|
#
|
|
# Focus: 10-node quadratic tetrahedron (Tet10) - the workhorse for 3D simulations
|
|
#
|
|
# Goal: Find the fastest way to access basis functions and their DERIVATIVES with:
|
|
# 1. Return all 10 basis functions as tuple (zero allocation)
|
|
# 2. Return single basis function by index (must be inlineable)
|
|
# 3. Return all 10 derivatives as tuple of Vec{3} (zero allocation)
|
|
# 4. Return single derivative by index (must be inlineable)
|
|
# 5. Pass topology separately (separation of concerns)
|
|
# 6. Must be type-stable and superfast
|
|
#
|
|
# Use Case:
|
|
# - Stiffness matrix assembly: Need derivatives (B matrix construction)
|
|
# - Mass matrix assembly: Need basis functions (M matrix construction)
|
|
# - Nodal assembly: Need single basis function/derivative at a time
|
|
#
|
|
# Usage: julia --project=. benchmarks/basis_function_access_patterns.jl
|
|
|
|
using BenchmarkTools
|
|
using Tensors
|
|
|
|
# ============================================================================
|
|
# Define minimal types for testing
|
|
# ============================================================================
|
|
|
|
abstract type AbstractTopology end
|
|
struct Tetrahedron <: AbstractTopology end
|
|
|
|
abstract type AbstractBasis end
|
|
struct Lagrange{P} <: AbstractBasis end
|
|
|
|
# Vec type comes from Tensors.jl (already available in JuliaFEM)
|
|
# Vec{3,Float64} for 3D gradients
|
|
|
|
# ============================================================================
|
|
# Strategy 1: Return tuple, index with getindex
|
|
# ============================================================================
|
|
# Advantages: Natural Julia syntax, type-stable
|
|
# Disadvantages: Might not inline getindex?
|
|
|
|
"""
|
|
Get all basis functions for Triangle, P1 Lagrange (3 functions).
|
|
Returns tuple of 3 Float64 values.
|
|
"""
|
|
@inline function get_basis_functions_v1(::Triangle, ::Lagrange{1}, xi::Vec{2,T}) where T
|
|
u, v = xi
|
|
# P1 triangle: N1 = 1-u-v, N2 = u, N3 = v
|
|
return (1 - u - v, u, v)
|
|
end
|
|
|
|
"""
|
|
Get single basis function by index (1-based).
|
|
"""
|
|
@inline function get_basis_function_v1(topology::Triangle, basis::Lagrange{1}, xi::Vec{2,T}, i::Int) where T
|
|
N_all = get_basis_functions_v1(topology, basis, xi)
|
|
return N_all[i] # Tuple indexing
|
|
end
|
|
|
|
# ============================================================================
|
|
# Strategy 2: Generated function for single access
|
|
# ============================================================================
|
|
# Advantages: Compiler can specialize for each index
|
|
# Disadvantages: More complex code
|
|
|
|
@inline function get_basis_functions_v2(::Triangle, ::Lagrange{1}, xi::Vec{2,T}) where T
|
|
u, v = xi
|
|
return (1 - u - v, u, v)
|
|
end
|
|
|
|
"""
|
|
Use @generated to create specialized code for each index at compile time.
|
|
"""
|
|
@generated function get_basis_function_v2(::Triangle, ::Lagrange{1}, xi::Vec{2,T}, ::Val{I}) where {T,I}
|
|
if I == 1
|
|
return :(1 - xi[1] - xi[2])
|
|
elseif I == 2
|
|
return :(xi[1])
|
|
elseif I == 3
|
|
return :(xi[2])
|
|
else
|
|
return :(error("Invalid basis function index: $I"))
|
|
end
|
|
end
|
|
|
|
# ============================================================================
|
|
# Strategy 3: Manual dispatch on Val (type-stable index)
|
|
# ============================================================================
|
|
# Advantages: Explicit, clear what's happening
|
|
# Disadvantages: Verbose, need to write each case
|
|
|
|
@inline function get_basis_functions_v3(::Triangle, ::Lagrange{1}, xi::Vec{2,T}) where T
|
|
u, v = xi
|
|
return (1 - u - v, u, v)
|
|
end
|
|
|
|
@inline get_basis_function_v3(t::Triangle, b::Lagrange{1}, xi::Vec{2,T}, ::Val{1}) where T = 1 - xi[1] - xi[2]
|
|
@inline get_basis_function_v3(t::Triangle, b::Lagrange{1}, xi::Vec{2,T}, ::Val{2}) where T = xi[1]
|
|
@inline get_basis_function_v3(t::Triangle, b::Lagrange{1}, xi::Vec{2,T}, ::Val{3}) where T = xi[2]
|
|
|
|
# ============================================================================
|
|
# Strategy 4: Struct with getindex (most Julian)
|
|
# ============================================================================
|
|
# Advantages: Can use N[i] syntax naturally
|
|
# Disadvantages: Extra struct allocation?
|
|
|
|
struct BasisFunctions{N,T}
|
|
data::NTuple{N,T}
|
|
end
|
|
|
|
Base.@propagate_inbounds Base.getindex(bf::BasisFunctions, i::Int) = bf.data[i]
|
|
Base.length(::BasisFunctions{N}) where N = N
|
|
|
|
@inline function get_basis_functions_v4(::Triangle, ::Lagrange{1}, xi::Vec{2,T}) where T
|
|
u, v = xi
|
|
return BasisFunctions((1 - u - v, u, v))
|
|
end
|
|
|
|
# Can use natural indexing
|
|
@inline function get_basis_function_v4(topology::Triangle, basis::Lagrange{1}, xi::Vec{2,T}, i::Int) where T
|
|
N = get_basis_functions_v4(topology, basis, xi)
|
|
return N[i]
|
|
end
|
|
|
|
# ============================================================================
|
|
# Strategy 5: Separate implementation per basis function (extreme specialization)
|
|
# ============================================================================
|
|
# Advantages: Maximum performance, no tuple allocation at all
|
|
# Disadvantages: Lots of code duplication
|
|
|
|
@inline function get_basis_function_1_v5(::Triangle, ::Lagrange{1}, xi::Vec{2,T}) where T
|
|
return 1 - xi[1] - xi[2]
|
|
end
|
|
|
|
@inline function get_basis_function_2_v5(::Triangle, ::Lagrange{1}, xi::Vec{2,T}) where T
|
|
return xi[1]
|
|
end
|
|
|
|
@inline function get_basis_function_3_v5(::Triangle, ::Lagrange{1}, xi::Vec{2,T}) where T
|
|
return xi[2]
|
|
end
|
|
|
|
@inline function get_basis_functions_v5(::Triangle, ::Lagrange{1}, xi::Vec{2,T}) where T
|
|
u, v = xi
|
|
return (1 - u - v, u, v)
|
|
end
|
|
|
|
# ============================================================================
|
|
# Benchmark: Access all basis functions (typical in assembly loop)
|
|
# ============================================================================
|
|
|
|
function benchmark_all_access()
|
|
println("\n" * "="^80)
|
|
println("BENCHMARK: Access ALL basis functions")
|
|
println("="^80)
|
|
|
|
topology = Triangle()
|
|
basis = Lagrange{1}()
|
|
xi = Vec(0.25, 0.25)
|
|
|
|
println("\nStrategy 1: Tuple return + getindex")
|
|
@btime get_basis_functions_v1($topology, $basis, $xi)
|
|
|
|
println("\nStrategy 2: Generated function")
|
|
@btime get_basis_functions_v2($topology, $basis, $xi)
|
|
|
|
println("\nStrategy 3: Val dispatch")
|
|
@btime get_basis_functions_v3($topology, $basis, $xi)
|
|
|
|
println("\nStrategy 4: BasisFunctions struct")
|
|
@btime get_basis_functions_v4($topology, $basis, $xi)
|
|
|
|
println("\nStrategy 5: Separate functions")
|
|
@btime get_basis_functions_v5($topology, $basis, $xi)
|
|
|
|
# Verify all return same values
|
|
r1 = get_basis_functions_v1(topology, basis, xi)
|
|
r2 = get_basis_functions_v2(topology, basis, xi)
|
|
r3 = get_basis_functions_v3(topology, basis, xi)
|
|
r4 = get_basis_functions_v4(topology, basis, xi).data
|
|
r5 = get_basis_functions_v5(topology, basis, xi)
|
|
|
|
@assert r1 == r2 == r3 == r4 == r5 "Results don't match!"
|
|
println("\n✓ All strategies return identical values: $r1")
|
|
end
|
|
|
|
# ============================================================================
|
|
# Benchmark: Access SINGLE basis function (for nodal assembly)
|
|
# ============================================================================
|
|
|
|
function benchmark_single_access()
|
|
println("\n" * "="^80)
|
|
println("BENCHMARK: Access SINGLE basis function (nodal assembly)")
|
|
println("="^80)
|
|
|
|
topology = Triangle()
|
|
basis = Lagrange{1}()
|
|
xi = Vec(0.25, 0.25)
|
|
|
|
println("\nStrategy 1: Tuple + runtime index")
|
|
@btime get_basis_function_v1($topology, $basis, $xi, 2)
|
|
|
|
println("\nStrategy 2: Generated function with Val{2}")
|
|
@btime get_basis_function_v2($topology, $basis, $xi, Val(2))
|
|
|
|
println("\nStrategy 3: Val dispatch")
|
|
@btime get_basis_function_v3($topology, $basis, $xi, Val(2))
|
|
|
|
println("\nStrategy 4: BasisFunctions struct + index")
|
|
@btime get_basis_function_v4($topology, $basis, $xi, 2)
|
|
|
|
println("\nStrategy 5: Direct function call")
|
|
@btime get_basis_function_2_v5($topology, $basis, $xi)
|
|
|
|
# Verify all return same value
|
|
r1 = get_basis_function_v1(topology, basis, xi, 2)
|
|
r2 = get_basis_function_v2(topology, basis, xi, Val(2))
|
|
r3 = get_basis_function_v3(topology, basis, xi, Val(2))
|
|
r4 = get_basis_function_v4(topology, basis, xi, 2)
|
|
r5 = get_basis_function_2_v5(topology, basis, xi)
|
|
|
|
@assert r1 == r2 == r3 == r4 == r5 "Results don't match!"
|
|
println("\n✓ All strategies return identical value: $r1")
|
|
end
|
|
|
|
# ============================================================================
|
|
# Benchmark: Typical assembly loop pattern
|
|
# ============================================================================
|
|
|
|
function benchmark_assembly_loop()
|
|
println("\n" * "="^80)
|
|
println("BENCHMARK: Typical assembly loop (iterate over all basis functions)")
|
|
println("="^80)
|
|
|
|
topology = Triangle()
|
|
basis = Lagrange{1}()
|
|
xi = Vec(0.25, 0.25)
|
|
|
|
# Pattern 1: Get all, iterate over tuple
|
|
println("\nPattern 1: Get all as tuple, iterate")
|
|
function assemble_v1()
|
|
N_all = get_basis_functions_v1(topology, basis, xi)
|
|
s = 0.0
|
|
for N_i in N_all
|
|
s += N_i * N_i # Dummy computation
|
|
end
|
|
return s
|
|
end
|
|
@btime assemble_v1()
|
|
|
|
# Pattern 2: Get all, index in loop
|
|
println("\nPattern 2: Get all, index with i")
|
|
function assemble_v2()
|
|
N_all = get_basis_functions_v1(topology, basis, xi)
|
|
s = 0.0
|
|
for i in 1:3
|
|
s += N_all[i] * N_all[i]
|
|
end
|
|
return s
|
|
end
|
|
@btime assemble_v2()
|
|
|
|
# Pattern 3: Get one at a time (nodal assembly style)
|
|
println("\nPattern 3: Get one at a time with Val")
|
|
function assemble_v3()
|
|
s = 0.0
|
|
# Unrolled loop (what compiler would do with Val)
|
|
N1 = get_basis_function_v3(topology, basis, xi, Val(1))
|
|
s += N1 * N1
|
|
N2 = get_basis_function_v3(topology, basis, xi, Val(2))
|
|
s += N2 * N2
|
|
N3 = get_basis_function_v3(topology, basis, xi, Val(3))
|
|
s += N3 * N3
|
|
return s
|
|
end
|
|
@btime assemble_v3()
|
|
|
|
# Pattern 4: Direct function calls (strategy 5)
|
|
println("\nPattern 4: Direct function calls (extreme specialization)")
|
|
function assemble_v4()
|
|
s = 0.0
|
|
N1 = get_basis_function_1_v5(topology, basis, xi)
|
|
s += N1 * N1
|
|
N2 = get_basis_function_2_v5(topology, basis, xi)
|
|
s += N2 * N2
|
|
N3 = get_basis_function_3_v5(topology, basis, xi)
|
|
s += N3 * N3
|
|
return s
|
|
end
|
|
@btime assemble_v4()
|
|
|
|
# Verify all compute same result
|
|
r1 = assemble_v1()
|
|
r2 = assemble_v2()
|
|
r3 = assemble_v3()
|
|
r4 = assemble_v4()
|
|
@assert r1 == r2 == r3 == r4 "Assembly results don't match!"
|
|
println("\n✓ All patterns compute same result: $r1")
|
|
end
|
|
|
|
# ============================================================================
|
|
# Benchmark: Basis derivatives (return Vec)
|
|
# ============================================================================
|
|
|
|
function benchmark_derivatives()
|
|
println("\n" * "="^80)
|
|
println("BENCHMARK: Basis function DERIVATIVES (return Vec)")
|
|
println("="^80)
|
|
|
|
topology = Triangle()
|
|
basis = Lagrange{1}()
|
|
xi = Vec(0.25, 0.25)
|
|
|
|
# Triangle P1 derivatives (constant):
|
|
# dN1/d(u,v) = (-1, -1)
|
|
# dN2/d(u,v) = (1, 0)
|
|
# dN3/d(u,v) = (0, 1)
|
|
|
|
println("\nStrategy 1: Return tuple of Vecs")
|
|
@inline function get_basis_derivatives_v1(::Triangle, ::Lagrange{1}, xi::Vec{2,T}) where T
|
|
return (Vec(-1.0, -1.0), Vec(1.0, 0.0), Vec(0.0, 1.0))
|
|
end
|
|
@btime get_basis_derivatives_v1($topology, $basis, $xi)
|
|
|
|
println("\nStrategy 2: Return single Vec with Val indexing")
|
|
@inline get_basis_derivative_v2(::Triangle, ::Lagrange{1}, xi::Vec{2,T}, ::Val{1}) where T = Vec(-1.0, -1.0)
|
|
@inline get_basis_derivative_v2(::Triangle, ::Lagrange{1}, xi::Vec{2,T}, ::Val{2}) where T = Vec(1.0, 0.0)
|
|
@inline get_basis_derivative_v2(::Triangle, ::Lagrange{1}, xi::Vec{2,T}, ::Val{3}) where T = Vec(0.0, 1.0)
|
|
@btime get_basis_derivative_v2($topology, $basis, $xi, Val(2))
|
|
|
|
# Verify
|
|
all_derivs = get_basis_derivatives_v1(topology, basis, xi)
|
|
single_deriv = get_basis_derivative_v2(topology, basis, xi, Val(2))
|
|
@assert all_derivs[2] == single_deriv
|
|
println("\n✓ Derivatives match: $single_deriv")
|
|
end
|
|
|
|
# ============================================================================
|
|
# Main execution
|
|
# ============================================================================
|
|
|
|
function main()
|
|
println("\n")
|
|
println("╔" * "="^78 * "╗")
|
|
println("║" * " "^78 * "║")
|
|
println("║" * " "^20 * "BASIS FUNCTION ACCESS PATTERNS BENCHMARK" * " "^18 * "║")
|
|
println("║" * " "^78 * "║")
|
|
println("╚" * "="^78 * "╝")
|
|
|
|
println("\nGoal: Find fastest way to access basis functions for nodal assembly")
|
|
println("Requirements:")
|
|
println(" - Zero allocation")
|
|
println(" - Type stable")
|
|
println(" - Inlineable")
|
|
println(" - Support both 'all at once' and 'one at a time' access")
|
|
|
|
benchmark_all_access()
|
|
benchmark_single_access()
|
|
benchmark_assembly_loop()
|
|
benchmark_derivatives()
|
|
|
|
println("\n" * "="^80)
|
|
println("SUMMARY & RECOMMENDATIONS")
|
|
println("="^80)
|
|
println("""
|
|
|
|
For TRADITIONAL ASSEMBLY (get all basis functions at integration point):
|
|
→ Use Strategy 1 or 4: Simple tuple return
|
|
→ Should be 0-5 ns, zero allocation
|
|
|
|
For NODAL ASSEMBLY (get single basis function):
|
|
→ Use Strategy 3: Val dispatch for compile-time index
|
|
→ Should be 0-2 ns, zero allocation, fully inlined
|
|
→ Usage: get_basis_function(Triangle(), Lagrange{1}(), xi, Val(i))
|
|
|
|
For DERIVATIVES:
|
|
→ Return tuple of Vec for all derivatives
|
|
→ Use Val indexing for single derivative
|
|
→ Same performance as basis functions
|
|
|
|
RECOMMENDED API:
|
|
```julia
|
|
# Get all basis functions (returns tuple)
|
|
N_all = get_basis_functions(Triangle(), Lagrange{1}(), xi)
|
|
|
|
# Get single basis function (Val for compile-time specialization)
|
|
N_i = get_basis_function(Triangle(), Lagrange{1}(), xi, Val(i))
|
|
|
|
# Get all derivatives (returns tuple of Vec)
|
|
dN_all = get_basis_derivatives(Triangle(), Lagrange{1}(), xi)
|
|
|
|
# Get single derivative (returns Vec)
|
|
dN_i = get_basis_derivative(Triangle(), Lagrange{1}(), xi, Val(i))
|
|
```
|
|
|
|
WHY Val?
|
|
- Compiler knows index at compile time
|
|
- Can generate optimal code for each basis function
|
|
- Zero runtime overhead
|
|
- Type stable
|
|
|
|
NOTE: For runtime indexing (i not known at compile time), tuple indexing
|
|
is still very fast (typically 1-2 ns overhead).
|
|
""")
|
|
|
|
println("\n" * "="^80)
|
|
end
|
|
|
|
# Run benchmarks
|
|
if abspath(PROGRAM_FILE) == @__FILE__
|
|
main()
|
|
end
|