Commit 6952664
perf: cache window bounds and replace deepcopy on window lists (-21% planning) (#4536)
* perf(plan): replace copy.deepcopy on window lists with a shallow clone
Window dicts only ever hold primitives (start/end/average/target/id/key), so
copy.deepcopy's generic recursive walk is doing no work that a per-dict .copy()
does not - measured 30.0us vs 1.6us per call on a 48-window list, 18.5x.
Adds utils.clone_windows() and uses it for the 13 planning-path deepcopy call
sites, plus a plain .copy() for the flat {minute: float} load forecast in
calculate_marginal_costs.
The copies exist for isolation, not just duplication: thread_run_prediction_export
writes export_window[window_n]["start"] in place, and plan_window_snapshot /
preclip_new are restored later. clone_windows keeps that contract - each dict is
copied, so in-place writes cannot leak either way - and the new tests pin it.
This is performance-neutral on the benchmark: 20 scenarios A/B, 3 reps each, came
out within noise (-0.4% median against ~6% run-to-run spread), because these call
sites were only ~1.5% of plan time. Kept for the explicit isolation contract and
the reduced allocation churn (deepcopy invocations -30%, total calls -3.4%), not
for a speed claim.
Verified byte-identical: all 20 random scenarios unchanged on metric, cost, and
all three PV futures (+0.0000 across the board).
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
* perf(kernel): cache window bounds instead of re-deriving them per simulation
A search runs thousands of simulations over the same charge/export windows,
varying only the limits, but every call re-derived the window start/end bounds
from the window dicts - four ctypes arrays for the kernel scenario and two tuples
for the prediction cache key. That was the largest single block of Python time in
a plan.
Caches them in prediction_kernel keyed on the identity of the window list, with
the derived fields built lazily so a caller that only wants the hash tuple never
pays to build the ctypes arrays. Hit rate on the benchmark is ~94%.
Correctness rests on every mutation of a window's start/end invalidating the
cache, so the 16 in-place assignments on the planning path now go through
set_window_start()/set_window_end(). The guard is run_window_cache_tests, which
replays a full calculate_plan with VALIDATE_WINDOW_CACHE on - that re-derives the
bounds on every cache hit and raises on any stale entry, so a future bare
window["start"] = ... on this path fails the suite rather than silently
simulating the wrong window geometry. The test also asserts the validator itself
catches a planted stale entry, so it cannot pass vacuously.
Pool workers unpickle fresh window lists every call and can never hit the cache,
where leaving it on cost ~3% of a pooled plan, so Pool() now runs
disable_window_cache as its worker initialiser. The cache is bounded and pins the
lists it keys on, so a caller that never repeats a list cannot grow it without
limit or alias a freed list's id() onto the wrong entry.
Measured on random scenario 0 (median of 3):
threads=0 2366.3ms -> 1839.0ms -22.3%
threads=auto 2134.0ms -> 2146.8ms +0.6% (noise)
C++ share of plan time rises from 43.7% to 56.2%; pk_run itself is unchanged at
19,209 calls of 55.0us, which is what confirms the simulation work is identical.
The 20 scenario benchmark drops 42.6s -> 33.4s.
Verified byte-identical: all 20 random scenarios unchanged on metric, cost and
all three PV futures, and kernel_parity passes.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
* fix(kernel): key the window bounds cache on the window count as well as identity
Addresses two of the three review comments on this PR.
A window list that grows or shrinks in place keeps its id(), so identity alone was
not enough to decide a cache hit. run_prediction_kernel passes n_charge/n_export
from len(window_list) alongside the cached arrays, so a stale shorter array would
be read past its end by the C kernel - a memory-safety failure rather than merely
a wrong plan. The count is now part of the hit condition.
Nothing on the planning path resizes a window list today, so this guards the class
rather than fixing a live defect. The test proves it would have bitten: before the
change, appending to a cached list left a one-entry bounds array against two
windows, and popping left window_bound_tuple returning the longer tuple.
Also corrects the correctness note, which pointed at Plan.set_window_start() and
Prediction.set_window_start(). Neither exists - they are module functions here -
and after the rebase the prediction path does not use them at all, since
_prepare_export applies a trial start copy-on-write to a window dict and list of
its own.
The third comment, to build the bound tuple from a generator rather than a list
comprehension, is not taken: measured over 50k calls at 200 windows, the generator
is 19.3% slower (307ms against 366ms), because it pays per-item interpreter
overhead the specialised list comprehension avoids.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>1 parent ea44f61 commit 6952664
7 files changed
Lines changed: 404 additions & 54 deletions
File tree
- .cspell
- apps/predbat
- tests
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
528 | 528 | | |
529 | 529 | | |
530 | 530 | | |
| 531 | + | |
531 | 532 | | |
532 | 533 | | |
533 | 534 | | |
| |||
Large diffs are not rendered by default.
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
22 | 22 | | |
23 | 23 | | |
24 | 24 | | |
25 | | - | |
26 | | - | |
27 | 25 | | |
28 | | - | |
29 | | - | |
30 | | - | |
31 | | - | |
32 | | - | |
| 26 | + | |
33 | 27 | | |
34 | 28 | | |
35 | 29 | | |
| |||
39 | 33 | | |
40 | 34 | | |
41 | 35 | | |
| 36 | + | |
| 37 | + | |
| 38 | + | |
| 39 | + | |
| 40 | + | |
42 | 41 | | |
43 | 42 | | |
44 | 43 | | |
45 | 44 | | |
46 | | - | |
| 45 | + | |
47 | 46 | | |
48 | | - | |
| 47 | + | |
49 | 48 | | |
50 | 49 | | |
51 | 50 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
389 | 389 | | |
390 | 390 | | |
391 | 391 | | |
| 392 | + | |
| 393 | + | |
| 394 | + | |
| 395 | + | |
| 396 | + | |
| 397 | + | |
| 398 | + | |
| 399 | + | |
| 400 | + | |
| 401 | + | |
| 402 | + | |
| 403 | + | |
| 404 | + | |
| 405 | + | |
| 406 | + | |
| 407 | + | |
| 408 | + | |
| 409 | + | |
| 410 | + | |
| 411 | + | |
| 412 | + | |
| 413 | + | |
| 414 | + | |
| 415 | + | |
| 416 | + | |
| 417 | + | |
| 418 | + | |
| 419 | + | |
| 420 | + | |
| 421 | + | |
| 422 | + | |
| 423 | + | |
| 424 | + | |
| 425 | + | |
| 426 | + | |
| 427 | + | |
| 428 | + | |
| 429 | + | |
| 430 | + | |
| 431 | + | |
| 432 | + | |
| 433 | + | |
| 434 | + | |
| 435 | + | |
| 436 | + | |
| 437 | + | |
| 438 | + | |
| 439 | + | |
| 440 | + | |
| 441 | + | |
| 442 | + | |
| 443 | + | |
| 444 | + | |
| 445 | + | |
| 446 | + | |
| 447 | + | |
| 448 | + | |
| 449 | + | |
| 450 | + | |
| 451 | + | |
| 452 | + | |
| 453 | + | |
| 454 | + | |
| 455 | + | |
| 456 | + | |
| 457 | + | |
| 458 | + | |
| 459 | + | |
| 460 | + | |
| 461 | + | |
| 462 | + | |
| 463 | + | |
| 464 | + | |
| 465 | + | |
| 466 | + | |
| 467 | + | |
| 468 | + | |
| 469 | + | |
| 470 | + | |
| 471 | + | |
| 472 | + | |
| 473 | + | |
| 474 | + | |
| 475 | + | |
| 476 | + | |
| 477 | + | |
| 478 | + | |
| 479 | + | |
| 480 | + | |
| 481 | + | |
| 482 | + | |
| 483 | + | |
| 484 | + | |
| 485 | + | |
| 486 | + | |
| 487 | + | |
| 488 | + | |
| 489 | + | |
| 490 | + | |
| 491 | + | |
| 492 | + | |
| 493 | + | |
| 494 | + | |
| 495 | + | |
| 496 | + | |
| 497 | + | |
| 498 | + | |
| 499 | + | |
| 500 | + | |
| 501 | + | |
| 502 | + | |
| 503 | + | |
| 504 | + | |
| 505 | + | |
| 506 | + | |
| 507 | + | |
| 508 | + | |
| 509 | + | |
| 510 | + | |
| 511 | + | |
392 | 512 | | |
393 | 513 | | |
394 | 514 | | |
| |||
774 | 894 | | |
775 | 895 | | |
776 | 896 | | |
777 | | - | |
778 | | - | |
| 897 | + | |
779 | 898 | | |
780 | | - | |
781 | | - | |
| 899 | + | |
782 | 900 | | |
783 | 901 | | |
784 | 902 | | |
| |||
0 commit comments