Skip to content

Warm-singleton resolve headroom #434

Description

@lesnik512

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.

Metadata

Metadata

Assignees

No one assigned

    Labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions