Repository navigation
(perf): selective fallback checkpoint/rewind via touched-others stack - #50
Merged
Merged
Conversation
…tracked acquire handling, and vector resize memory behavior - Deleted `selective_memory_reclamation.md` as the design has been implemented and is no longer needed. - Removed `untracked_acquire_design.md` which outlined a previous approach to handling untracked acquisitions in AdaptiveArrayPools. - Eliminated `vector_resize_memory_behavior.md` that discussed the memory behavior of vector resizing, as the design decisions have been finalized.
…d rewind variants
@with_pool scopes now checkpoint/rewind only the fallback typed pools they actually touch (first-touch lazy checkpoint, per-depth stack segment) instead of eagerly checkpointing and rewinding every registered fallback pool. Fixes order-dependent latency where unrelated fallback registrations slowed hot call sites (active=0 pollution).
…l state The task-local pool is a process-wide singleton shared by every test file. @with_pool's fast path does not guarantee cleanup for exceptions that escape its own scope (only nested leaks within one invocation are caught by the entry-depth guard), so earlier files that deliberately throw across a @with_pool boundary (e.g. test_macro_expansion.jl's stack-trace tests) can leave _current_depth elevated before this testset runs. Reset the task-local pool at the start of the testset so its absolute-depth assertions hold regardless of test file execution order. Root-caused by direct repro (with and without the touched-others changes: identical leak in both), confirming it is pre-existing and unrelated to the semantic switch.
Codecov Report❌ Patch coverage is
Additional details and impacted files@@ Coverage Diff @@
## master #50 +/- ##
==========================================
+ Coverage 95.82% 95.88% +0.05%
==========================================
Files 16 16
Lines 3619 3645 +26
==========================================
+ Hits 3468 3495 +27
+ Misses 151 150 -1
🚀 New features to boost your workflow:
|
reference/api.md renders at 200.05 KiB in CI — just over Documenter's 200 KiB hard limit — because GitHub source links (remotes, CI-only) add a few KiB that local builds don't have. Exempt only this page; all others keep the guardrail.
This was referenced Jul 10, 2026
Merged
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Problem
Reported via FastInterpolations CI benchmarks: a
@with_poolcall site that acquires one fallback (non-fixed-slot) type slows down when unrelated code registers other fallback types in the same task-local pool — even withactive=0,depth=1, and afterreset!. Pool history, not the scope's own work, determined hot-path latency (order-dependent timing in benchmark suites; phase-dependent latency in applications).Root cause
Fallback types had no per-type touch tracking — a single per-depth
_touched_has_others::Boolcovered all of them:_lazy_checkpoint!eagerly checkpointed every registered fallback pool at every lazy scope entry (_typed_lazy_checkpoint!likewise)._others_values::Vector{Any}), paying a dynamic dispatch per pool.Scope cost was
O(all fallback types ever registered)— ~160 ns per registered type per scope on an M-series machine.Fix
Give fallback pools the same first-touch lazy checkpoint discipline fixed slots already have, using an identity list instead of a bitmask (the fallback type set is open):
_touched_others::Vector{Any}+_touched_others_checkpoints::Vector{Int}— a flat stack of fallback pools first-touched at each open depth, with per-depth segment bases (same pattern as_others_ptr_bounds)._touch_fallback_pool!, shared first-touch guard): typedcheckpoint!(pool, T),@generated checkpoint!(pool, types...),_record_type_touch!, andget_typed_pool!new-type registration.d⟺ it is in the depth-dstack segment — except under fullcheckpoint!(pool), whose eager sweep pairs with fullrewind!(pool)'s sweep (segment stays empty, truncate-only)._touched_has_othersis kept (still consumed by_can_use_typed_pathand the S=1 debug validation).reset!docs now note that registered-but-inactive fallbacks no longer add per-scope cost.CPU only; CUDA/Metal extensions keep their current (correct, non-selective) behavior — GPU parity is the next PR.
Benchmarks
Master measured in a clean worktree with identical scripts and preferences (
runtime_check=false), runs serialized, replicated with side order swapped. Julia 1.12, Apple Silicon.Pollution — lazy
@with_poolscope acquiring one fallback type; 1 vs 8 registered (inactive,reset!-ed) fallback types:Scaling on master (same scope, others = 1..8; zero allocations throughout — this PR stays flat at ~55 ns across the whole range):
The marginal cost is nonlinear: master's eager checkpoint sweep and full-registry rewind are two dynamic-dispatch sites cycling through every registered
TypedPool{T}. Up to 4 distinct types Julia's per-call-site dispatch cache stays warm (~45 ns/type); from the 5th type on, every dispatch falls back to the global method-table lookup (~180–360 ns/type). This matches the original report (others=3, warm regime, ~35 ns/type) — and means real applications degrade sharply once a 5th fallback type registers.Fixed-slot-only scopes (no fallback anywhere; measures the new bookkeeping itself):
Float64)The ~1 ns/scope cost (per-scope segment-base push/pop + empty-segment drain; ≈ +0.3% on the originally reported workload) is accepted for this PR. A depth-tagged-stack variant that eliminates it is planned alongside the follow-up bookkeeping rework.
Testing
test/test_touched_others.jl(285 lines): pollution regression (unrelated registered fallbacks are never visited), nested scopes (same type at multiple depths), helper-function untracked touches in typed scopes, statically-tracked fallback types, mid-scope type registration, fullcheckpoint!/rewind!pairing,similar!touch recording,@with_poolintegration, exception-leak recovery, and@allocated == 0regression for lazy and macro roundtrips.src/state.jlcoverage 98%), including the S=1 runtime-check paths.checkpoint!(pool, T)+ helper fallbackacquire!+rewind!(pool, T)previously never restored the fallback'sn_active.Follow-ups (tracked separately)
PoolCheckpointStateto remove the drain loop's dynamic dispatch, folding in the depth-tagged stack to erase the fixed-only ~1 ns cost.Foo{Float64}) as static so those scopes take the typed path.