Skip to content

Performance improvements (allocations) - #397

Merged
prehner merged 9 commits into
mainfrom
vle-perf
Oct 26, 2026
Merged

prehner merged 9 commits into
mainfrom
vle-perf

Conversation

@g-bauer

@g-bauer g-bauer commented Oct 16, 2026 •

Copy link
Copy Markdown
Contributor

These two commits remove most heap allocations from the evaluation of the residual PC-SAFT Helmholtz energy:

  • f9cd39e:
    • packing fractions directly lifted from the MonomerShape instead of materializing the geometry coefficients and the component index
    • computes zeta and d once and shares them between hard sphere, hard chain and dispersion contributions
    • no StateHD clone
    • sums the contributions without collecting them into a Vec
    • removes allocations in other places
  • b29b5dd:
    • the polar contributions use precomputed mean segment numbers and prefactors, and take zeta as input (as above mentioned)
    • cross-association reuses buffers for the Newton step and solves in place (we might ship such a buffered version to num-dual?)
    • iterator folds replace nalgebra operations that allocated in the hot loops

Timings & Allocations

For the 16 mixtures of Table 4 in the feos paper the first derivative of the residual Helmholtz energy is 2.5x to 7.1x faster now. The number of heap allocations per evaluation drops from 32–123 to 3–14.

Mixture Interactions before + f9cd39e + b29b5dd speedup allocations per evaluation
hexane / heptane non-polar 0.682 µs 0.157 µs 0.159 µs 4.3× 32 → 3 → 3
hexane / dimethyl ether dipole 1.011 µs 0.384 µs 0.184 µs 5.5× 48 → 13 → 3
hexane / acetylene quadrupole 1.013 µs 0.385 µs 0.185 µs 5.5× 48 → 13 → 3
hexane / 1-propanol association 0.916 µs 0.274 µs 0.276 µs 3.3× 40 → 5 → 5
acetone / heptane dipole 1.027 µs 0.381 µs 0.184 µs 5.6× 48 → 13 → 3
acetone / dimethyl ether dipole 1.146 µs 0.502 µs 0.219 µs 5.2× 50 → 15 → 3
acetone / acetylene dipole + quadrupole 1.682 µs 0.839 µs 0.238 µs 7.1× 80 → 33 → 3
acetone / 1-propanol dipole + association 1.252 µs 0.501 µs 0.303 µs 4.1× 56 → 15 → 5
carbon dioxide / heptane quadrupole 1.018 µs 0.385 µs 0.184 µs 5.5× 48 → 13 → 3
carbon dioxide / dimethyl ether dipole + quadrupole 1.682 µs 0.835 µs 0.238 µs 7.1× 80 → 33 → 3
carbon dioxide / acetylene quadrupole 1.141 µs 0.496 µs 0.220 µs 5.2× 50 → 15 → 3
carbon dioxide / 1-propanol quadrupole + association 1.244 µs 0.506 µs 0.302 µs 4.1× 56 → 15 → 5
ethanol / heptane association 0.908 µs 0.273 µs 0.276 µs 3.3× 40 → 5 → 5
ethanol / dimethyl ether association + dipole 1.262 µs 0.506 µs 0.303 µs 4.2× 56 → 15 → 5
ethanol / acetylene association + quadrupole 1.252 µs 0.503 µs 0.302 µs 4.1× 56 → 15 → 5
ethanol / 1-propanol cross-association 3.212 µs 2.551 µs 1.271 µs 2.5× 123 → 88 → 14

Time per evaluation of the first temperature derivative (Dual64) of the residual PC-SAFT Helmholtz energy, via cargo bench --profile=release-lto --bench=contributions. Apple M1 Pro, Rust 1.96. Allocations are heap blocks per evaluation, counted with dhat.

Interface changes

  • Breaking: MonomerShape::NonSpherical now holds &DVector<f64> instead of an owned DVector<D>.
  • Additive: new provided method ResidualDyn::reduced_residual_helmholtz_energy_density, which mirrors the method that already exists on Residual. EquationOfState and the ResidualDyn derive macro forward it, so models wrapped for Python take the allocation-free path as well.
  • Crate-internal: the signatures of HardChain, Dispersion, Dipole, Quadrupole and DipoleQuadrupole in pcsaft::eos.

computation out of individual contributions. Additional refactoring to
remove allocations in other places.
from num-dual (for now). Remove some nalgebra methods that allocate in
the hot loops in favor of iterator methods that do not allocate. Lift
zeta out of polar contributions. Refactor polar contributions to
eliminate repeated computations that can be done once.
@g-bauer g-bauer added enhancement New feature or request breaking Contains breaking changes labels Oct 16, 2026
@g-bauer

g-bauer commented Oct 21, 2026

Copy link
Copy Markdown
Contributor Author

Update

New commits since the description above:

  • f9cd345: for_each_contribution is now the single entry point for Helmholtz energies.
    • A model evaluates each contribution and passes it, together with its name, to a closure.
    • The sum (hot path) and the Vec of contributions (output) are provided methods of Residual built on top of it, so every model gets the allocation-free sum without overriding anything.
    • Wrappers (EquationOfState, the ResidualDyn derive macro, the user-defined Python model) forward one method instead of two, so they can no longer silently fall back to the allocating path.
    • HelmholtzEnergyFunctional::evaluate_bulk takes the same kind of closure.
  • a82c070, 75b03c3: benchmark fixes (see below).

Interface changes

This replaces the Additive item in the description: ResidualDyn::reduced_residual_helmholtz_energy_density no longer exists.

  • Breaking: ResidualDyn and Residual have a new required method for_each_contribution(&self, state, f: impl FnMut(&'static str, D)), which replaces reduced_helmholtz_energy_density_contributions as the method models implement.
    • Residual::reduced_residual_helmholtz_energy_density and Residual::reduced_helmholtz_energy_density_contributions are now provided methods built on for_each_contribution.
    • ResidualDyn no longer has reduced_helmholtz_energy_density_contributions or reduced_residual_helmholtz_energy_density. Callers go through Residual, which the blanket impl provides for pointers to ResidualDyn models (&T, Arc<T>, ...).
    • All models in feos, the ResidualDyn derive macro and the user-defined Python model in py-feos are migrated.
  • Breaking: HelmholtzEnergyFunctional::evaluate_bulk passes each contribution to a closure instead of returning a Vec<(&'static str, D)>.

Benchmark fixes

  • contributions (a82c070): since Parallel evaluation of properties for fits/training #289 the benchmark passed V = 1/ρ (per particle) together with the total number of moles, so all 16 states were NaN, and for cross-association every evaluation ran 50 non-converging Newton iterations. It now uses the mole fractions. The table in the description was measured with this fix.
  • dual_numbers_saftvrmie (75b03c3): the state was built from the critical point's volume together with 10 mol, which State::new_nvt rejects (IntensiveState). It now uses 10 mol / ρ_c.
  • state_creation (75b03c3): removed critical_point_binary_p. State::critical_point_binary at a given pressure currently does not converge (NotConverged), independently of this PR. It will be fixed separately, and the benchmark restored.

@g-bauer
g-bauer requested a review from prehner October 21, 2026 12:06
@prehner
prehner merged commit c152cb8 into main Oct 26, 2026
18 checks passed
@prehner
prehner deleted the vle-perf branch October 26, 2026 08:21
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

breaking Contains breaking changes enhancement New feature or request

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants