The C2 warm-singleton gap against the slot-memoized rivals stays open — the memo-swap technique that targeted it was measured and dropped, and closing it now requires removing the resolve_provider dispatch floor itself.
modern-di's warm singleton hit is the one scenario where it sits clearly behind
the field. Two levers were identified; both have since been ruled on, and what
remains open is narrower than the original item.
Why it is open
The single-path compiled resolver (#334) replaced the interpreted recursion with
one memoized per-provider closure — a clean win everywhere (transient −37%, warm
singleton −31%, deep chain −61%, wide −62%). It left the warm hit still paying
resolver_for dispatch, the override front-guard, and fetch_cache_item on
every call.
3.1.2 then inlined the two dict-lookup hit paths (resolver_for and
fetch_cache_item), calling the method only on a miss. That cut the warm hit
~25% — 170 → 128 ns by reference — and every top-level resolve benefits, since
one of the two sits in resolve_provider.
What stays open: the gap to the slot-memoized rivals. dependency-injector
reads a C-level slot on a Cython core, that-depends a lock-free slot,
wireup a self-modifying closure. Pure Python does not reach a C-level slot
read, so part of this gap is a floor, not a defect. The live comparative
numbers are in performance.md,
regenerated by just bench-report — read that, not a snapshot here.
Two levers, both now settled:
- The memo-swap (swap the cached provider's resolver for a bare
return value closure) was built, fully tested, measured at ~1.6x, and
dropped — see
0015-warm-singleton-memo-swap-dropped.
The decisive finding is that resolve_provider's dispatch floor is upstream of
the swap and unremovable by it, so the technique provably cannot reach
"near-free".
- The codegen ceiling on transient and deep-chain is the cost of staying
exec-free: dishka and wireup inline dependency calls into generated source,
removing the per-node closure-call frame modern-di keeps. Re-declined — see
0017-exec-hot-path-declined.
The supporting measurement: closures already capture ~80-90% of the available
ceiling, and exec buys a further 0-4% at fixed arity. So being modestly
behind the codegen leaders on construction-heavy graphs is an accepted floor,
not a bug.
A third step was considered during the 3.1.2 work and deliberately not taken: an
APP-scoped resolver could close over its CacheItem and reach ~16 ns, but the
target is only invariant because one registry belongs to one root — so the
registry would have to reference its root container, reintroducing the reference
cycle removed in 3.1.1. That needs a weakref and a proof, for a margin that
performance.md is the only place to read.
Revisit trigger
A user-reported warm-singleton bottleneck, plus a design that removes the
dispatch floor itself. Re-proposing the memo-swap unchanged is settled; so is
exec codegen. A that-depends-style per-APP-container slot array is the one
untried direction and would need its own measurement against the same bar.
The C2 warm-singleton gap against the slot-memoized rivals stays open — the memo-swap technique that targeted it was measured and dropped, and closing it now requires removing the
resolve_providerdispatch floor itself.modern-di's warm singleton hit is the one scenario where it sits clearly behind
the field. Two levers were identified; both have since been ruled on, and what
remains open is narrower than the original item.
Why it is open
The single-path compiled resolver (#334) replaced the interpreted recursion with
one memoized per-provider closure — a clean win everywhere (transient −37%, warm
singleton −31%, deep chain −61%, wide −62%). It left the warm hit still paying
resolver_fordispatch, the override front-guard, andfetch_cache_itemonevery call.
3.1.2 then inlined the two dict-lookup hit paths (
resolver_forandfetch_cache_item), calling the method only on a miss. That cut the warm hit~25% — 170 → 128 ns by reference — and every top-level resolve benefits, since
one of the two sits in
resolve_provider.What stays open: the gap to the slot-memoized rivals.
dependency-injectorreads a C-level slot on a Cython core,
that-dependsa lock-free slot,wireupa self-modifying closure. Pure Python does not reach a C-level slotread, so part of this gap is a floor, not a defect. The live comparative
numbers are in
performance.md,regenerated by
just bench-report— read that, not a snapshot here.Two levers, both now settled:
return valueclosure) was built, fully tested, measured at ~1.6x, anddropped — see
0015-warm-singleton-memo-swap-dropped.The decisive finding is that
resolve_provider's dispatch floor is upstream ofthe swap and unremovable by it, so the technique provably cannot reach
"near-free".
exec-free: dishka and wireup inline dependency calls into generated source,removing the per-node closure-call frame modern-di keeps. Re-declined — see
0017-exec-hot-path-declined.The supporting measurement: closures already capture ~80-90% of the available
ceiling, and
execbuys a further 0-4% at fixed arity. So being modestlybehind the codegen leaders on construction-heavy graphs is an accepted floor,
not a bug.
A third step was considered during the 3.1.2 work and deliberately not taken: an
APP-scoped resolver could close over its
CacheItemand reach ~16 ns, but thetarget is only invariant because one registry belongs to one root — so the
registry would have to reference its root container, reintroducing the reference
cycle removed in 3.1.1. That needs a weakref and a proof, for a margin that
performance.mdis the only place to read.Revisit trigger
A user-reported warm-singleton bottleneck, plus a design that removes the
dispatch floor itself. Re-proposing the memo-swap unchanged is settled; so is
execcodegen. Athat-depends-style per-APP-container slot array is the oneuntried direction and would need its own measurement against the same bar.