perf(scope): memoize _next_deeper — ~40% faster default child-build#348
Merged
Conversation
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>
There was a problem hiding this comment.
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.
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.
Memoize
_next_deeper, which re-sorted the whole enum on every child build — ~44% of the defaultbuild_child_container()(auto-increment) path. Result:_next_deeper1319 → ~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:IntEnumcompares/hashes by integer value, so two custom scopes reusing a value would otherwise collide. A collision-safety test and a newG6bguard bench (the auto-increment pathG6never 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-cigreen at 100% coverage;just lint-ciclean.🤖 Generated with Claude Code