Skip to content

perf(scope): memoize _next_deeper — ~40% faster default child-build#348

Merged
lesnik512 merged 1 commit into
mainfrom
perf/memoize-next-deeper
Jul 19, 2026
Merged

perf(scope): memoize _next_deeper — ~40% faster default child-build#348
lesnik512 merged 1 commit into
mainfrom
perf/memoize-next-deeper

Conversation

@lesnik512

Copy link
Copy Markdown
Member

Memoize _next_deeper, which re-sorted the whole enum on every child build — ~44% of the default build_child_container() (auto-increment) path. Result: _next_deeper 1319 → ~130 ns; default child-build ~2983 → ~1750 ns (about −40%). The explicit-scope path integrations use is unaffected.

The memo keys on (type(scope), scope), not the bare member: IntEnum compares/hashes by integer value, so two custom scopes reusing a value would otherwise collide. A collision-safety test and a new G6b guard bench (the auto-increment path G6 never covered) lock it in. Lazy-allocation of the remaining per-child allocations is measured and deferred.

Design + measurements: planning/changes/2026-07-19.02-memoize-next-deeper.md. Discovery + deferred leads: planning/deferred.md.

just test-ci green at 100% coverage; just lint-ci clean.

🤖 Generated with Claude Code

The default build_child_container() (no explicit scope -> auto-increment) spent
~44% of its time in _next_deeper, which ran `sorted(m for m in type(scope) if
m > scope)` on every child build -- recomputing a constant function of an
immutable enum member. Memoize it: _next_deeper 1319 -> ~130 ns, default
child-build ~2983 -> ~1750 ns (about -40%). The explicit-scope path integrations
use never calls _next_deeper and is unaffected.

Key the memo on (type(scope), scope), NOT the bare member: IntEnum members
compare and hash by integer value, so two custom scopes reusing a value (a
TENANT=6 in two different enums) would collide under a plain member key and one
would serve the other's next-deeper answer. Caught by the existing custom-scope
suite; a dedicated collision-safety test now guards it. A new G6b guard bench
covers the auto-increment path (G6 passes an explicit scope and never exercised
it).

Lazy-allocation of the other per-child allocations (RLock, CacheRegistry,
ContextRegistry) is measured and deferred (planning/deferred.md) -- it touches
the resolve hot path and needs a net measurement.

test-ci green at 100% coverage; lint-ci clean.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

@github-actions github-actions Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Benchmark

Details
Benchmark suite Current: 2eb9fa5 Previous: b807a16 Ratio
benchmarks/test_guard_lifecycle.py::test_g6_build_child_container 416525.47101637523 iter/sec (stddev: 0.000060426852605499975) 430847.6717489951 iter/sec (stddev: 0.000053943054805399885) 1.03
benchmarks/test_guard_lifecycle.py::test_g6b_build_child_container_auto_scope 463538.9646637597 iter/sec (stddev: 0.000009367273774331461)
benchmarks/test_guard_lifecycle.py::test_g7_request_lifecycle 63204.348431683975 iter/sec (stddev: 0.000012849184132377635) 62212.608184089884 iter/sec (stddev: 0.000013873284337533808) 0.98
benchmarks/test_guard_resolve.py::test_g1_transient_resolve 1226508.589982989 iter/sec (stddev: 4.11183749554404e-7) 1237764.3929808394 iter/sec (stddev: 5.27126978125101e-7) 1.01
benchmarks/test_guard_resolve.py::test_g2_cached_resolve 2293303.2098920047 iter/sec (stddev: 6.981759225730512e-8) 2377133.8484962545 iter/sec (stddev: 6.530132937685283e-8) 1.04
benchmarks/test_guard_resolve.py::test_g3_deep_chain 491329.62107306055 iter/sec (stddev: 5.807228885759617e-7) 517427.83633450605 iter/sec (stddev: 5.811991024595401e-7) 1.05
benchmarks/test_guard_resolve.py::test_g4_wide_resolve 310878.35885876394 iter/sec (stddev: 7.612154394528255e-7) 319951.73880093044 iter/sec (stddev: 6.482677923804022e-7) 1.03
benchmarks/test_guard_resolve.py::test_g5_cross_scope 1074151.7694677808 iter/sec (stddev: 5.059455241074563e-7) 1104499.9240585808 iter/sec (stddev: 4.147508606564314e-7) 1.03

This comment was automatically generated by workflow using github-action-benchmark.

@lesnik512
lesnik512 merged commit 3fa3428 into main Jul 19, 2026
9 checks passed
@lesnik512
lesnik512 deleted the perf/memoize-next-deeper branch July 19, 2026 09:37
lesnik512 added a commit that referenced this pull request Jul 19, 2026
Bundles the unreleased performance work since 2.30.0: #349 (2.4x cold
first-resolve), #348 (~40% faster default child-build), #347 (uniform
dispatch-floor win). No API or behavior change.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant