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.rs — AssignConstCurr, the per-step flows evaluation, run_to, set_value, and the saved-results chunk ring.
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 onTIME, 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 eachrun_toinstead 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_valueoverrides must still take effect: the constant phase must be re-run afterset_valueso an override of a constant (or of something feeding a constant-derived aux) propagates correctly. The hoisting must preserve current override semantics exactly.Complications
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 inlinedeval_bytecodeswamped the structural win. Documented indocs/design/engine-performance.mdas 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.rs—AssignConstCurr, the per-step flows evaluation,run_to,set_value, and the saved-results chunk ring.