From 3a1c931021ba5de5949bf880f058ab04cd4a46d4 Mon Sep 17 00:00:00 2001 From: Jukka Aho Date: Sat, 9 May 2026 16:34:36 +0300 Subject: [PATCH] docs(dofs): remove stale performance_analysis markdown Drop superseded DOF extraction benchmark narrative from src/dofs/docs. --- src/dofs/docs/performance_analysis.md | 206 -------------------------- 1 file changed, 206 deletions(-) delete mode 100644 src/dofs/docs/performance_analysis.md diff --git a/src/dofs/docs/performance_analysis.md b/src/dofs/docs/performance_analysis.md deleted file mode 100644 index 9f124c8..0000000 --- a/src/dofs/docs/performance_analysis.md +++ /dev/null @@ -1,206 +0,0 @@ -# DOF Extraction Performance Analysis - -**Date:** November 22, 2025 -**Investigation:** Zero-allocation DOF extraction with @generated functions - -## Executive Summary - -This document summarizes the investigation into DOF extraction performance, comparing @generated functions against manual implementations. - -### Key Findings - -1. **Assembly Code Quality**: Both @generated and manual produce identical assembly (12 loads + 12 stores) -2. **Allocation Claims False**: "112 bytes allocations" reported by @allocated are measurement artifacts -3. **Real GC Pressure**: 0.8 bytes per extraction over 1M operations (negligible) -4. **Performance Gap**: @generated is 50-70x slower than manual in benchmarks -5. **Root Cause**: Dispatch overhead from `Type{VectorDOF{D}}` parameter, NOT extraction logic - -### Performance Numbers - -| Implementation | Min Time | Allocations | Hot Loop Throughput | -|----------------|----------|-------------|---------------------| -| Manual | ~5-10 ns | 0 | 18M calls/sec | -| @generated | ~300-400 ns | 3 (artifact) | 3-4M calls/sec | -| Speedup | **60-70x** | — | **5x** | - -### Verdict - -**The @generated version is production-ready** despite being slower because: - -1. **Context matters**: DOF extraction is 0.6% of assembly time - - 10K elements × 300ns extraction = 3ms - - 10K elements × 50μs assembly = 500ms - - Extraction overhead: negligible - -2. **Clean API wins**: Generic interface is worth 0.6% cost - - Type-safe: `extract_element_dofs(VectorDOF{3}, ...)` - - Self-documenting - - Extensible to arbitrary element types - -3. **Zero allocation**: Real GC pressure is 0.8 bytes/call (negligible) - -## Investigation Timeline - -### Phase 1: Initial Concern -- User noticed "112 bytes allocations" in benchmarks -- Suspected Tensors.jl or StaticArrays causing heap allocations - -### Phase 2: Allocation Analysis -- Tested Vec{3} construction: **0 allocations** ✅ -- Tested SVector construction: **0 allocations** ✅ -- Both types are `isbits`: true ✅ -- Conclusion: Tensors.jl is NOT the problem - -### Phase 3: GC Pressure Test (BREAKTHROUGH) -```julia -# Run 1M extractions, measure actual GC impact -Total allocated: 815,216 bytes -Per extraction: 0.8 bytes -GC runs: 5 - -✅ NEGLIGIBLE GC PRESSURE! -``` - -The "112 bytes" is the **return value size**, not heap allocation. - -### Phase 4: Assembly Analysis -```asm -# The hot path (after bounds checks): -vmovsd xmm0, qword ptr [rsi + 8*rcx - 8] # Load DOF 1 -vmovsd xmm1, qword ptr [rsi + 8*r9 - 8] # Load DOF 2 -... (12 loads total) -vmovsd qword ptr [rdi], xmm0 # Store to result -vmovsd qword ptr [rdi + 8], xmm1 # Store to result -... (12 stores total) -ret -``` - -**No malloc, no function calls, pure load-store operations.** - -### Phase 5: Performance Gap Investigation -Discovered @generated version is 60x slower than manual. Tested: - -1. **AbstractVector → Vector**: No improvement -2. **@inbounds in generated code**: No improvement -3. **Val{D} instead of Type{}**: 2x better, still 30x slower -4. **Manual dispatch to specialized functions**: Still slow -5. **Direct manual inline**: 5-10ns (baseline) - -**Root cause**: Any dispatch adds 100-250ns overhead, even with compile-time types. - -## Technical Details - -### Why @generated Is Slow - -The @generated function compiles to perfect assembly (pure loads), but calling it involves: - -1. **Type parameter dispatch**: `Type{VectorDOF{3}}` → 100-150ns overhead -2. **Function call frame**: Even with `@inline`, not always eliminated -3. **Generic interface cost**: Flexibility has runtime price - -### Why Manual Is Fast - -```julia -@inline function extract_manual(u::Vector{Float64}, indices::NTuple{12, Int}) - @inbounds SVector( - Vec{3}((u[indices[1]], u[indices[2]], u[indices[3]])), - Vec{3}((u[indices[4]], u[indices[5]], u[indices[6]])), - Vec{3}((u[indices[7]], u[indices[8]], u[indices[9]])), - Vec{3}((u[indices[10]], u[indices[11]], u[indices[12]])) - ) -end -``` - -No dispatch, no type parameters, direct call → inlines to pure loads. - -### Assembly Code Comparison - -Both produce **identical assembly** for the extraction logic: -- 12 `vmovsd` loads from memory -- 12 `vmovsd` stores to result buffer -- No heap allocation -- No function calls - -The difference is in the **call site**, not the extraction. - -## Recommendations - -### For Most Users: Use @generated - -```julia -u_elem = extract_element_dofs(VectorDOF{3}, u_global, elem.dof_indices) -``` - -**Pros:** -- Clean, self-documenting API -- Type-safe (compiler enforces correctness) -- Works for any D, any element type -- 0.6% performance cost is acceptable - -**Cons:** -- 60x slower than manual (but still fast enough) - -### For Performance-Critical Paths: Manual - -If DOF extraction shows up in profiling (unlikely), write manual versions: - -```julia -# For Tet4 displacement: -@inline function extract_tet4_displacement(u::Vector{Float64}, inds::NTuple{12,Int}) - @inbounds SVector( - Vec{3}((u[inds[1]], u[inds[2]], u[inds[3]])), - Vec{3}((u[inds[4]], u[inds[5]], u[inds[6]])), - Vec{3}((u[inds[7]], u[inds[8]], u[inds[9]])), - Vec{3}((u[inds[10]], u[inds[11]], u[inds[12]])) - ) -end -``` - -This gives 5-10ns performance at the cost of code duplication. - -## When Manual Might Matter - -Scenarios where extraction overhead matters: - -1. **Pure nodal assembly**: No element matrices, just matvecs -2. **Matrix-free GPU kernels**: Different story (investigate separately) -3. **Millions of small elements**: If extraction > 1% of runtime - -For typical FEM (element assembly dominates), @generated is fine. - -## Files Generated This Session - -### Core Implementation -- `src/elements/ciarlet_extract_dofs.jl` - Original @generated implementation - -### Benchmarks & Analysis -- `examples/dof_extraction_analysis.jl` - Initial LLVM/assembly analysis -- `examples/zero_overhead_proof.jl` - Complete proof of zero-overhead -- `examples/machine_code_proof.jl` - Assembly annotation -- `examples/bounds_check_elimination.jl` - Bounds check investigation -- `examples/real_overhead_analysis.jl` - GC pressure test -- `examples/debug_generated_overhead.jl` - Type inference comparison -- `examples/hot_loop_test.jl` - Real-world performance test -- `examples/test_dispatch_strategies.jl` - Val{} vs Type{} comparison -- `examples/test_optimized_extract.jl` - Specialized implementation test - -### Optimized Versions (Experimental) -- `src/elements/ciarlet_extract_dofs_optimized.jl` - Specialized D=1,2,3 versions - -### Documentation -- `examples/PERFORMANCE_CONCLUSION.md` - Final analysis summary -- `src/dofs/examples/generated_vs_manual_comparison.jl` - Comprehensive benchmark -- `src/dofs/docs/performance_analysis.md` - This document - -## Conclusion - -The @generated function provides a **zero-cost abstraction** in the sense that: -- Assembly code is optimal (pure load-store) -- No heap allocations (0.8 bytes GC pressure over 1M ops) -- Type-stable and compiler-optimized - -The 60x slowdown vs manual is **dispatch overhead**, not extraction overhead. - -For FEM assembly where extraction is <1% of runtime, the clean generic API is worth the cost. - -**Verdict: Production-ready. Ship it.** ✅