Skip to content

engine: time-invariant variable hoisting — a constant phase evaluated once per run_to #712

@bpowers

Description

@bpowers

Context

Identified during the simlin-engine VM performance round 2 (see docs/design/engine-performance.md "Round 2 wins (2026-06-03)" and the unprioritized run-side swings at the bottom of that file). Sibling perf issues from the same campaign: #601 (threaded dispatch), #602 (lookup hot path), #603 (stride-step flat_offset), #604 (bundle marginal branch/dispatch reductions), #711 (lazy IF).

Problem

Simple constants are re-assigned (AssignConstCurr) and constant-derived auxiliaries are fully re-computed on every timestep, even though their values are identical at every step. A variable whose flow equation transitively depends only on constants — with no dependency on TIME, stocks, PREVIOUS, time-dependent builtins (PULSE/RAMP/STEP/etc.), and no module evaluations — produces the same value every step. These could be evaluated once at the top of each run_to instead of per-step.

Why it matters

Performance: on constant-heavy models a meaningful fraction of the per-step program is recomputing values that never change. The run is throughput-bound on the round-2 machine, so removing time-invariant work from the per-step loop is a direct win.

Semantics to preserve

set_value overrides must still take effect: the constant phase must be re-run after set_value so an override of a constant (or of something feeding a constant-derived aux) propagates correctly. The hoisting must preserve current override semantics exactly.

Complications

  • Results presentation: each saved results chunk must still carry these variables' values. The chunk ring currently gets them from the per-step flows evaluation; if those slots are no longer written per step, hoisting needs either a copy-forward of the invariant slots into each saved chunk or a layout change so saved chunks reference the once-computed values.
  • Classification correctness: the transitive "depends only on constants" analysis must conservatively exclude TIME/stocks/PREVIOUS/time-dependent builtins/module evals. A false positive would silently freeze a variable that should vary.

First step

A measurement, before any implementation: determine what fraction of the C-LEARN / WORLD3 per-step program is actually time-invariant. If the fraction is small, the layout/copy-forward complexity may not pay for itself.

Related context / cautionary note

A provably-equivalent rewrite of vector_elm_map's strict-slice base as a precomputed affine dot product measured a consistent ~5 ms REGRESSION on the 137 ms C-LEARN run — codegen perturbation of the giant inlined eval_bytecode swamped the structural win. Documented in docs/design/engine-performance.md as a negative result reinforcing #604's "measure everything near the eval loop" rule. Measure this hoisting empirically rather than assuming the opcode-count reduction translates to wall-clock.

Refs

  • docs/design/engine-performance.md — "Round 2 wins (2026-06-03)" and the "Time-invariant hoisting" bullet under the unprioritized run-side swings.
  • src/simlin-engine/src/vm.rsAssignConstCurr, the per-step flows evaluation, run_to, set_value, and the saved-results chunk ring.

Metadata

Metadata

Assignees

No one assigned

    Labels

    engineIssues with the rust-based simulation engineenhancementNew feature or request

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions