mirror of
https://github.com/JuliaFEM/JuliaFEM.jl.git
synced 2026-09-10 13:17:42 +00:00
829109132f
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.
302 lines
8.2 KiB
Julia
302 lines
8.2 KiB
Julia
# 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")
|