mirror of
https://github.com/JuliaFEM/JuliaFEM.jl.git
synced 2026-08-06 04:21:33 +00:00
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:
@@ -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")
|
||||
Reference in New Issue
Block a user