5.9 KiB
Solver optimization audit
Measured on an Intel Core i5-12600K, Linux, GCC 16.2.1, Release (-O3,
no fast-math). Reference is commit 589d22a. Performance results are specific
to this machine; Windows/MSVC runtime measurements remain pending. The retained
changes use portable C++ and compiler vectorization, with no new CPU requirement.
Retained changes
- Boundary solves start from the previous converged face pressure when it is inside the new physical bracket. Otherwise they start at the bracket midpoint. The safeguarded Newton solver, residual tolerance, iteration cap, choking, reverse flow, donor budgets and inertance calculation remain unchanged.
- Pipe preparation computes primitive states and wave speeds in an independent loop, then validates states and reduces the CFL timestep. GCC confirms 128-bit vectorization of the arithmetic loop. Invalid states still throw before flux updates. No approximate reciprocal/square root or relaxed arithmetic is used.
- Pipe fresh-air tracer fractions smaller than the smallest normal float (approximately 1.175e-38) are rounded to zero. Previously, purged intake/exhaust tracer tails persisted as subnormals, causing severe sustained-load slowdown. This affects species attribution below that precision, not total density, momentum or energy. The regression suite checks bulk conservation and retention of normal tracer concentrations. Global flush-to-zero modes are not enabled.
Starting guesses change numerical trajectories within the existing convergence tolerance; long engine/audio traces need not be bit-identical to the reference.
Profiling and experiments
A -pg build running every scenario for two simulated seconds attributed about
39% of sampled executable time to pipe advancement, 38% to orifice boundary
solves, and 13% to Solver::step (including inlined preparation). Turbo mechanics
were under 1% and outdoor emitter rendering about 1%. These are sampled gprof
results, not precise attribution of time inside dynamically linked math functions.
fluidsim_thread_benchmark now uses each scenario's actual pipe geometry and
compares persistent 1/2/4-worker execution with barriers per substep. This isolates
the parallelizable pipe work; boundary/component work would remain serial. Every
current scenario loses with workers, so production remains serial. The synthetic
64-pipe, 256-cells-per-pipe case benefits from workers, confirming that this
strategy has a useful scale threshold. It does not justify adding its scheduling
and synchronization cost to the current small graphs.
SIMD-disabled and -march=native builds are comparison experiments. Native
compilation is not enabled in the project defaults, since the executable could
then require instructions missing on another machine.
Results
Mean wall seconds per simulated second, six-second sustained-load runs:
| Scenario | Reference | Optimized | Time reduction | SIMD disabled |
|---|---|---|---|---|
| Twin | .528 | .405 | 23% | .423 |
| Turbo twin | 1.368 | .649 | 53% | .684 |
| B205 | .884 | .689 | 22% | .737 |
| Two-stroke racing | .561 | .470 | 16% | .535 |
| Two-stroke straight | .539 | .464 | 14% | .525 |
| Helmholtz | .167 | .141 | 16% | .160 |
| Pulse | .256 | .238 | 7% | .315 |
Idle/transient optimized means (seconds per simulated second): twin .394/.399, turbo .627/.651, B205 .666/.683, racing two-stroke .450/.459, straight two-stroke .452/.453, Helmholtz .141/.140, and pulse .238/.238.
The pulse scenario has no orifice, so its improvement isolates the SIMD-friendly preparation change. Disabling SIMD increases time in every scenario. An optional native-instruction build improves some timings further, but is not shipped as the default: FMA also changes rounding and the binary is CPU-specific.
Initial isolated changes measured approximately .40 seconds for the twin after boundary warm starts alone (previously .53). The sustained turbo still took about 1.2 seconds after that change; eliminating subnormal tracer tails brought it to about .65. The diagnostic found 22 subnormal pipe fields before truncation and zero afterwards. This is distinct from reducing CFL substeps or geometry; neither was changed. Six-second idle and throttle/release runs also complete for every scenario. These are finite test windows, not a guarantee of real-time performance for every user configuration or arbitrarily long simulation.
Reproduction
Final validation: all nine CTest suites passed both with SIMD enabled (28.50 s) and disabled (30.86 s). The fluid validation executable includes 30 checks, including tracer underflow conservation. The Release SFML desktop target also rebuilt successfully. This audit verifies desktop compilation and headless audio rendering; interactive playback underrun testing remains a separate user check.
cmake -S . -B build-opt -DCMAKE_BUILD_TYPE=Release -DFLUIDSIM_BUILD_DESKTOP=OFF
cmake --build build-opt -j
ctest --test-dir build-opt --output-on-failure
./build-opt/fluidsim_scenario_benchmark 6 loaded
./build-opt/fluidsim_scenario_benchmark 6 idle
./build-opt/fluidsim_scenario_benchmark 6 transient
./build-opt/fluidsim_thread_benchmark
The scenario benchmark runs twin, turbo twin, B205, racing and straight-pipe
two-strokes, Helmholtz and pulse scenarios, including their normal audio path.
loaded holds four-strokes at 7,000 RPM and two-strokes at 10,000 RPM using an
ideal speed-holding dyno; engine throttle is 70%. idle releases the pedal;
transient starts released, applies 70%, then releases for the last two seconds.
The optional final argument selects one scenario, e.g. 6 loaded turbo.
CSV timing/checksum goes to stdout; subnormal and iteration diagnostics go to
stderr. Inspection happens outside the timed sample-processing loop.
Use -DFLUIDSIM_ENABLE_SIMD=OFF in a separate build for scalar comparisons.
Run timed experiments sequentially, without concurrent builds or simulations.
The raw final measurements are retained in verification/optimization/.