Files

14 KiB
Raw Permalink Blame History

FluidSim engine and solver performance profile

Run date: 2026-10-24
Raw measurements: profile-runs.csv
Harness: tools/scenario_profile.cpp
Analyzer: tools/analyze_scenario_profile.py

These runs predate the geometry-defined header layouts. Current scenarios have revised header lengths; rerun the harness for current performance figures.

Summary

The solver is the main cost in every tested configuration. At 20 cells/m it takes about 76–92% of measured process() time; controls take 3–9%, audio rendering 3–12%, and component updates 2–3% of solver time. Across the run matrix, the median boundary share of solver time falls from 56.5% at 20 cells/m to 30.5% at 80 cells/m; pipe updates rise from 32.8% to 55.0%. The bottleneck shifts from boundary resolution toward pipe-cell advancement as density grows.

Mesh density has a nonlinear runtime effect because smaller cells also require more CFL substeps. Raising density from 20 to 80 cells/m increased median frame cost by 3.0×–6.4× across these engines. The V8 was most expensive: at 80 cells/m it used about 1,054 cells and took about 134 µs per frame in the profiled run.

At a fixed density of 20 cells/m, Rusanov was the fastest solver. Relative to Rusanov at that same density, MUSCL cost 16–50% more depending on engine, while HLLE and HLLC were generally 3–25% more. At your solver-specific listening targets, MUSCL at 25 cells/m is the fastest overall because it reaches your preferred sound at a coarser mesh.

Across all tested scenarios and densities, the median sampled frame cost was 11.6 µs for Rusanov, 13.0 µs for HLLE, 13.5 µs for HLLC, and 15.9 µs for MUSCL. Those pooled values summarize this exact sweep and are affected by its chosen scenario and density mix.

No pipe positivity repairs or HLLC fallbacks occurred in the measured runs.

Method

  • 6 TOML scenarios, four solvers, 4 mesh densities, and two repetitions: 192 measured runs.
  • Scenarios: five, saab-chambered, single-450, twin, v8-h, yz-fat. yz-fat is the two-stroke case; the others are four-stroke.
  • Densities: 10, 20, 40, 80 cells/m, applied to the scenario's configured geometry.
  • Solvers: Rusanov, HLLE, HLLC, and MUSCL. Solver choice was applied to every pipe in each scenario.
  • Each run warmed up for 5,000 frames, then measured 22,050 frames (0.5 simulated seconds) at 44.1 kHz. Engine speed was reset to the scenario's configured initial RPM (or 4,500 RPM when unset) before every frame; the two-stroke was held at 7,000 RPM. Throttle was 55%, load 10%, clutch 0%.
  • Internal profiling sampled every other measured process() call. Each run therefore contains 11,025 profiled frames. End-to-end wall time includes all measured frames; stage timings come from sampled frames.
  • Build: Release, MinGW GCC 16.2.0, Windows, Intel Core i5-12600K. The machine was otherwise used interactively, so small differences should not be over-interpreted.

Cost at 20 cells/m

Each cell shows median end-to-end wall microseconds per frame and wall/simulated-time ratio across the two repetitions. A ratio below 1.0 means this isolated scenario ran faster than real time. Boundary, pipe, and component columns are their share of inclusive solver time; controls and audio are their share of total sampled process time.

Scenario Solver Cells µs/frame Wall / sim Controls Audio Boundary / solver Pipe / solver Components / solver
five rusanov 138 9.5 0.42 8% 5% 67% 22% 2%
five hlle 138 10.5 0.46 7% 5% 59% 30% 2%
five hllc 138 11.0 0.49 7% 5% 56% 33% 2%
five muscl 138 13.6 0.60 6% 4% 60% 32% 2%
saab-chambered rusanov 123 12.7 0.56 5% 4% 59% 29% 2%
saab-chambered hlle 123 14.9 0.66 4% 3% 50% 37% 2%
saab-chambered hllc 123 15.9 0.70 4% 3% 47% 40% 2%
saab-chambered muscl 123 19.2 0.84 3% 3% 52% 38% 2%
single-450 rusanov 52 3.6 0.16 8% 13% 54% 33% 3%
single-450 hlle 52 4.1 0.18 7% 12% 46% 39% 2%
single-450 hllc 52 4.1 0.18 7% 12% 44% 42% 2%
single-450 muscl 52 5.3 0.23 7% 10% 50% 39% 2%
twin rusanov 24 3.6 0.16 9% 13% 75% 15% 4%
twin hlle 24 3.7 0.16 9% 12% 71% 18% 3%
twin hllc 24 3.9 0.17 9% 12% 69% 21% 3%
twin muscl 24 4.2 0.19 8% 12% 69% 21% 3%
v8-h rusanov 268 17.7 0.78 6% 4% 63% 28% 2%
v8-h hlle 268 20.1 0.88 6% 3% 56% 34% 2%
v8-h hllc 268 21.4 0.94 6% 3% 53% 37% 2%
v8-h muscl 268 26.2 1.15 5% 2% 58% 34% 2%
yz-fat rusanov 30 9.0 0.40 3% 4% 63% 24% 3%
yz-fat hlle 30 10.2 0.45 3% 4% 57% 30% 3%
yz-fat hllc 30 10.4 0.46 3% 4% 54% 33% 3%
yz-fat muscl 30 11.4 0.50 3% 3% 54% 34% 2%

The three solver sub-stages are nested inside the solver measurement, so their percentages should not be added to the solver percentage. Their shares also do not sum to 100%: solver setup, CFL preparation, and loop overhead make up the remainder.

Mesh-density scaling

Median profiled total cost per frame, aggregated over all four solvers and both repetitions. Cell counts are the actual total across all pipes in the scenario.

Scenario Density: cells / µs 80 vs 20 cost
five 10: 74 / 9.2; 20: 138 / 10.9; 40: 273 / 22.1; 80: 544 / 53.7 4.92×
saab-chambered 10: 68 / 12.5; 20: 123 / 15.6; 40: 243 / 22.8; 80: 476 / 51.2 3.29×
single-450 10: 27 / 3.3; 20: 52 / 4.3; 40: 104 / 6.4; 80: 208 / 17.7 4.14×
twin 10: 13 / 3.8; 20: 24 / 4.0; 40: 45 / 5.3; 80: 89 / 12.0 3.01×
v8-h 10: 144 / 16.7; 20: 268 / 20.9; 40: 528 / 54.0; 80: 1054 / 133.5 6.37×
yz-fat 10: 17 / 8.9; 20: 30 / 10.8; 40: 57 / 14.0; 80: 108 / 20.1 1.87×

Solver cost at 20 cells/m

Change in median total_ms_per_frame versus Rusanov for the same scenario and mesh density.

Scenario Rusanov HLLE HLLC MUSCL
five +0% +11% +16% +44%
saab-chambered +0% +17% +24% +50%
single-450 +0% +14% +15% +47%
twin +0% +3% +8% +16%
v8-h +0% +13% +21% +45%
yz-fat +0% +14% +15% +26%

All solvers at 50 cells/m

This fixed-density sweep isolates solver cost at your preferred mesh scale. Values are medians across six scenarios and two repetitions; boundary and pipe shares are fractions of inclusive solver time.

50 cells/m raw runs

Solver Median µs/frame Boundary / solver Pipe / solver
rusanov 14.8 47.2% 38.2%
hlle 20.6 36.2% 50.1%
hllc 22.6 29.5% 56.8%
muscl 29.1 42.3% 48.0%

User-rated quality targets

You reported that these densities sound great: Rusanov 70 cells/m, HLLE 60, HLLC 45, and MUSCL 25. The following timings compare those listening targets; the benchmark measures compute and output statistics, not subjective sound quality.

Quality-target raw runs

Solver Cells/m Median µs/frame (six scenarios) Median wall/sim V8 cells V8 µs/frame V8 wall/sim
rusanov 70 17.4 0.77 930 68.0 3.00
hlle 60 22.2 0.98 794 76.3 3.36
hllc 45 19.6 0.87 601 63.3 2.79
muscl 25 14.0 0.62 333 30.1 1.33

MUSCL at its 25 cells/m target had the lowest median compute cost across these cases. On the V8, it was about 30 µs/frame versus about 63 µs for HLLC at 45 cells/m, 68 µs for Rusanov at 70, and 76 µs for HLLE at 60. A wall/sim ratio above 1 means the isolated scenario takes longer than real time on this CPU.

Compiler SIMD comparison

Compared the Release build (GCC auto-vectorization enabled) with FLUIDSIM_ENABLE_SIMD=OFF, using the same quality-target matrix and run lengths.

Solver Median SIMD speedup Matched audio output
rusanov -1.1% audio checksums identical (max delta 0)
hlle +1.4% audio checksums identical (max delta 0)
hllc +3.2% audio checksums identical (max delta 0)
muscl +3.1% audio checksums identical (max delta 0)

Positive speedup means vectorization was faster. The changes are small and mixed (roughly 0–3% median, within the noise of these short runs), while measured audio RMS, peaks, and checksums matched exactly. The current AoS cell/flux layout is not yielding a large automatic-SIMD gain; an explicit SoA or batched kernel would need a focused prototype and a broader numerical comparison.

Paired raw runs by solver: rusanov SIMD / scalar, hlle SIMD / scalar, hllc SIMD / scalar, muscl SIMD / scalar.

Lazy solver scratch buffers

The pipe now allocates MUSCL reconstruction buffers only for MUSCL, and Roe caches only for HLLE/HLLC. Rusanov no longer allocates either unused cache. On the V8 at each solver's quality target:

Solver Cells Scratch before Scratch after Saved
rusanov 930 112,120 B 60,040 B 52,080 B (46%)
hlle 794 95,800 B 64,040 B 31,760 B (33%)
hllc 601 72,640 B 48,600 B 24,040 B (33%)
muscl 333 40,480 B 35,152 B 5,328 B (13%)

The buffer change reduced solver scratch allocation without changing audio checksums (maximum delta 0). It did not produce a throughput gain in this short paired comparison (median frame time changed +0.5% after the change), so keep it as a memory-footprint reduction rather than counting it as a CPU optimization. Scratch stays allocated after a solver is first selected, allowing fast mode switches.

Paired raw runs: rusanov before / after, hlle before / after, hllc before / after, muscl before / after.

A separate experiment fused primitive conversion, state validation, and minimum-timestep reduction into one pipe pass. Paired speed changes were rusanov +0.1%, hlle -0.1%, hllc -1.1%, muscl +0.7%; audio checksums stayed identical (maximum delta 0). There was no consistent gain, so that loop change was reverted.

Fused-pass raw runs: rusanov, hlle, hllc, muscl.

CPU versus memory and threading

A Windows process sample during the 50 cells/m sweep showed about 103% process CPU (approximately one full logical CPU), 7.1 MiB working set, and 3.8 MiB private bytes. The i5-12600K has 10 cores / 16 logical processors. At 50 cells/m, the V8 has 662 pipe cells; its pipe data plus solver scratch is about 96 KiB before the scratch-buffer reduction and about 60 KiB for Rusanov afterward. This is an inference of CPU-bound execution rather than RAM-capacity pressure: the working data is small and fits in the CPU's private L2 cache, but hardware cache-miss/bandwidth counters were not collected, so L1/L2 traffic is not ruled out.

The existing per-step thread benchmark measured the V8's 26 pipes at 50 cells/m: 1 worker took 0.049 s, 2 took 0.188 s, and 4 took 0.267 s for 4,000 steps. Only its artificial 64-pipe × 256-cell case benefited (0.908 s with 1 worker, 0.546 s with 4). Real engine pipes are too small to amortize per-step synchronization; don’t parallelize each pipe step without a persistent task system and a much larger workload.

An experimental GCC -march=native build crashed in the five-cylinder 50 cells/m profile with Windows access-violation exit 0xC0000005; it was rejected. The default portable Release build remains the tested build.

The focused post-change test run passed 8/8: mesh resolution, junctions, scenario catalog, Rusanov/HLLE/HLLC/MUSCL fluid validation, and outdoor audio.

Interpretation and limits

Optimization priorities

  1. For the 50 cells/m listening range, start with pipe advancement for HLLE/HLLC: pipe updates use about 51–57% of solver time. At your target densities, they remain the largest stage for the V8 with Rusanov 70, HLLE 60, and HLLC 45. Reducing unnecessary per-cell passes or improving the cell/flux layout is the most promising CPU work.
  2. Boundary resolution is still a large second target (about 30–47% of solver time at 50 cells/m), and is the largest stage for the twin and for MUSCL in several cases. Optimize the characteristic-junction root solve only with the acoustic junction benchmarks guarding its behavior.
  3. Leave component updates and audio rendering for later; they are smaller absolute costs in the larger engines. Keep optimizing the pipe and boundary stages first.
  • Boundary resolution and pipe advancement trade places as density and solver change. At 50 cells/m, boundaries are largest for Rusanov, while pipe advancement leads for HLLE and HLLC; the V8's junction network still makes boundaries substantial.
  • Increasing density raises both the number of cells and the number of solver substeps needed to meet the CFL limit. This explains why runtime growth can exceed the cell-count growth alone.
  • At low densities, controls and audio are a larger fraction of total work because there are few cells to update. Their absolute time stays comparatively small as the mesh grows.
  • Results are for this Release build and machine, with profiling enabled on alternating frames. Instrumentation adds some timing overhead; the tables are best used for relative comparisons within this run. Very short runs, other compilers, build flags, CPUs, and background load can change absolute timings.
  • This benchmark isolates one scenario at a time. It excludes desktop rendering, UI work, output-device scheduling, and audio callback buffering. It does not compare sound quality or engine-state accuracy across mesh densities or solver methods.
  • Two repetitions provide a directional profile, not a high-confidence microbenchmark. Repeat on a quiet machine before using small differences to drive optimization decisions.

Reproduce

cmake --build out/build/windows-gcc --target fluidsim_scenario_profile -j 8
out/build/windows-gcc/fluidsim_scenario_profile verification/performance/2026-10-24/profile-runs.csv 5000 22050 2
py -3 tools/analyze_scenario_profile.py verification/performance/2026-10-24/profile-runs.csv verification/performance/2026-10-24/profile-report.md