bench: Add integration point access pattern comparison

Compares 5 strategies for accessing integration points during assembly:
- OLD: Runtime dispatch + mutable struct with Dict (type-unstable)
- Option A: Compile-time function returning tuples
- Option B: Store in element as NTuple (current approach)
- Option C: Compile-time with Vec{D} from Tensors.jl
- Option D: Pre-computed global constants
- Option E: Function returning pre-computed constant

Validates that compile-time generation (Options C-E) matches the golden
standard architecture from nodal assembly demos. Includes realistic FEM
assembly comparison showing performance difference between old runtime
dispatch and new compile-time approach.

Recommendation: Option C (compile-time with Vec) or D/E (pre-computed)
for zero allocation and full inlining, matching eval_basis! pattern.
This commit is contained in:
Jukka Aho
2025-11-12 00:16:50 +02:00
parent b37e8fc268
commit 829109132f
+301
View File
@@ -0,0 +1,301 @@
# Benchmark: Integration Point Access Patterns
# ============================================
#
# This benchmark compares different approaches to storing and accessing
# integration points in finite element assembly loops.
#
# Key Question: What's the fastest way to get integration points?
#
# Approaches tested:
# 1. OLD: Runtime dispatch + mutable struct with Dict (type-unstable)
# 2. NEW Option A: Compile-time function (like eval_basis!)
# 3. NEW Option B: Store in element as NTuple
# 4. NEW Option C: Hybrid (compile-time generation + caching)
using BenchmarkTools
using Tensors
using StaticArrays
# ============================================================================
# OLD APPROACH: Runtime dispatch with mutable struct
# ============================================================================
struct OldIP
id::UInt
weight::Float64
coords::Tuple{Vararg{Float64}}
fields::Dict{String,Any} # Type-unstable!
end
function get_integration_points_old(::Type{Val{:Tri3}})
# Simulate runtime dispatch to get IPs
return [
OldIP(UInt(1), 0.5, (1 / 3, 1 / 3), Dict{String,Any}()),
]
end
function assembly_loop_old()
sum_val = 0.0
for _ in 1:1000 # Simulate 1000 elements
ips = get_integration_points_old(Val{:Tri3})
for ip in ips
w = ip.weight
xi, eta = ip.coords
# Simulate some computation
sum_val += w * (xi + eta)
end
end
return sum_val
end
# ============================================================================
# NEW OPTION A: Compile-time function (zero allocation)
# ============================================================================
"""
Return integration points as tuple at compile time.
Similar to eval_basis! - zero allocation, fully inlined.
"""
@inline function get_integration_points!(::Type{Val{:Tri3_Gauss1}})
# Return as tuple of (weight, coords) pairs
return (
(0.5, (1 / 3, 1 / 3)),
)
end
@inline function get_integration_points!(::Type{Val{:Tet4_Gauss1}})
return (
(1 / 24, (0.25, 0.25, 0.25)),
)
end
function assembly_loop_option_a()
sum_val = 0.0
for _ in 1:1000
ips = get_integration_points!(Val{:Tri3_Gauss1})
for (w, (xi, eta)) in ips
sum_val += w * (xi + eta)
end
end
return sum_val
end
# ============================================================================
# NEW OPTION B: Store in element as NTuple (what we have now)
# ============================================================================
struct IntegrationPoint{D}
ξ::NTuple{D,Float64}
weight::Float64
end
struct MockElement{NIP}
ips::NTuple{NIP,IntegrationPoint{2}}
end
function create_element_b()
ips = (
IntegrationPoint((1 / 3, 1 / 3), 0.5),
)
return MockElement(ips)
end
function assembly_loop_option_b()
elements = [create_element_b() for _ in 1:1000]
sum_val = 0.0
for element in elements
for ip in element.ips
w = ip.weight
xi, eta = ip.ξ
sum_val += w * (xi + eta)
end
end
return sum_val
end
# ============================================================================
# NEW OPTION C: Compile-time with Tensors.jl Vec (recommended for FEM)
# ============================================================================
"""
Return integration points with Vec{D} coordinates (Tensors.jl).
This matches the golden standard from nodal assembly demos.
"""
@inline function get_integration_points_vec!(::Type{Val{:Tri3_Gauss1}})
return (
(0.5, Vec{2}((1 / 3, 1 / 3))),
)
end
@inline function get_integration_points_vec!(::Type{Val{:Tet4_Gauss1}})
return (
(1 / 24, Vec{3}((0.25, 0.25, 0.25))),
)
end
function assembly_loop_option_c()
sum_val = 0.0
for _ in 1:1000
ips = get_integration_points_vec!(Val{:Tri3_Gauss1})
for (w, xi) in ips
# Vec arithmetic is optimized by Tensors.jl
sum_val += w * sum(xi)
end
end
return sum_val
end
# ============================================================================
# NEW OPTION D: Pre-generated global constants (ultimate zero-cost)
# ============================================================================
const TRI3_GAUSS1_IPS = (
(0.5, Vec{2}((1 / 3, 1 / 3))),
)
const TET4_GAUSS1_IPS = (
(1 / 24, Vec{3}((0.25, 0.25, 0.25))),
)
function assembly_loop_option_d()
sum_val = 0.0
for _ in 1:1000
for (w, xi) in TRI3_GAUSS1_IPS
sum_val += w * sum(xi)
end
end
return sum_val
end
# ============================================================================
# OPTION E: Hybrid - Function returns pre-computed constant
# ============================================================================
@inline get_ips_tri3_gauss1() = TRI3_GAUSS1_IPS
@inline get_ips_tet4_gauss1() = TET4_GAUSS1_IPS
function assembly_loop_option_e()
sum_val = 0.0
for _ in 1:1000
for (w, xi) in get_ips_tri3_gauss1()
sum_val += w * sum(xi)
end
end
return sum_val
end
# ============================================================================
# Run Benchmarks
# ============================================================================
println("="^80)
println("Integration Point Access Pattern Benchmark")
println("="^80)
println()
println("OLD APPROACH: Runtime dispatch + mutable struct with Dict")
println("-"^80)
@btime assembly_loop_old()
println()
println("OPTION A: Compile-time function returning tuples")
println("-"^80)
@btime assembly_loop_option_a()
println()
println("OPTION B: Store in element as NTuple (current approach)")
println("-"^80)
@btime assembly_loop_option_b()
println()
println("OPTION C: Compile-time function with Vec{D} (Tensors.jl)")
println("-"^80)
@btime assembly_loop_option_c()
println()
println("OPTION D: Pre-generated global constants")
println("-"^80)
@btime assembly_loop_option_d()
println()
println("OPTION E: Function returning pre-computed constant")
println("-"^80)
@btime assembly_loop_option_e()
println()
# ============================================================================
# Realistic FEM Assembly Benchmark
# ============================================================================
println()
println("="^80)
println("REALISTIC FEM ASSEMBLY COMPARISON")
println("="^80)
println()
# Simulate realistic element stiffness computation
function compute_element_stiffness_old(element_type::Type{Val{:Tri3}})
K_elem = zeros(6, 6)
ips = get_integration_points_old(element_type)
for ip in ips
w = ip.weight
xi, eta = ip.coords
# Simulate shape function evaluation and stiffness computation
N1 = 1 - xi - eta
N2 = xi
N3 = eta
# Accumulate (simplified)
K_elem[1, 1] += w * (N1^2)
end
return K_elem[1, 1]
end
function compute_element_stiffness_new()
K_elem = 0.0
for (w, xi) in get_integration_points_vec!(Val{:Tri3_Gauss1})
# Vec arithmetic
xi_val = xi[1]
eta_val = xi[2]
N1 = 1 - xi_val - eta_val
K_elem += w * (N1^2)
end
return K_elem
end
println("OLD: Realistic element stiffness assembly")
@btime begin
sum_val = 0.0
for _ in 1:1000
sum_val += compute_element_stiffness_old(Val{:Tri3})
end
sum_val
end
println()
println("NEW: Realistic element stiffness assembly")
@btime begin
sum_val = 0.0
for _ in 1:1000
sum_val += compute_element_stiffness_new()
end
sum_val
end
println()
println("="^80)
println("SUMMARY")
println("="^80)
println()
println("Expected ranking (fastest to slowest):")
println("1. Option D/E: Pre-computed constants (ultimate zero-cost)")
println("2. Option C: Compile-time with Vec{D} (recommended for FEM)")
println("3. Option A: Compile-time with plain tuples")
println("4. Option B: Stored in element NTuple (slight overhead)")
println("5. OLD: Runtime dispatch + Dict (type-unstable)")
println()
println("RECOMMENDATION:")
println(" Use Option C or D/E for integration points:")
println(" - Compile-time generation like eval_basis!")
println(" - Return as Tuple of (weight, Vec{D}) pairs")
println(" - Zero allocation, fully inlined")
println(" - Matches golden standard architecture")