Skip to content

StrandExecutor still rebuilds the map node and the Strand on every serial dispatch -- 152 bytes, 2 allocations, and removing them needs a deregistration hook #670

Description

@Yaraslaut

What is left after morph#660

morph#660 listed three directions and this took the second — shrink what the
drain-and-erase rebuilds. Measured on 7a343e6f, x86-64 Linux, GCC 16.2.1 /
libstdc++, -O2, with tests/bench/bench_dispatch_allocations.cpp:

before   4159 allocations / 398072 bytes over 200 round trips   (20.80 / 1990.4 per call)
         #7    32 bytes  strand.hpp:104   _strands map node
         #8   152 bytes  strand.hpp:106   make_shared<Strand>
         #9    64 bytes  <stl_deque>      the Strand's std::queue deque map
         #10  512 bytes  <stl_deque>      the Strand's std::queue first buffer

after    3786 allocations / 279208 bytes over 200 round trips   (18.93 / 1396.0 per call)
         #7    32 bytes   _strands map node
         #8   120 bytes   make_shared<Strand>

152 bytes and 2 allocations per serial local dispatch remain, about 11% of
the 1396 bytes a round trip now costs. They are the unordered_map node and the
Strand control block, and no container change removes them: they exist because
scheduleNext erases the map entry the moment the strand drains, so the next
post() for the same model takes the if (!slot) branch and builds both again.

Why it was not done in morph#660

Keeping the slot alive across the drain is direction 1 of morph#660's three, and
it is not a container swap — it is a change of lifetime policy. StrandExecutor
has no deregistration hook: the erase is the only thing that ever removes a
key, so a retained slot is retained until ~StrandExecutor. A process that
registers and drops many short-lived models would grow _strands without bound.

Making that safe means adding a forget(ModelId)-shaped API and wiring it into
LocalBackend/RemoteServer model deregistration, and doing it without
disturbing the {_mapMtx, strand->mtx} atomicity that
docs/spec/core/executor.md ("The per-key serialisation invariant") and the
comments at strand.hpp:81/:167 record a previous defect forcing into shape.
That is a different, larger and riskier change than the one morph#660 closed
with, so it was deliberately not smuggled in.

Verification status

  • Reproduced: both figures above are runs on this workstation, before and
    after the morph#660 change, same binary, same flags. The four attributions in
    the "before" block come from a backtrace() added to the bench's counting
    hook, resolved through dladdr + addr2line -f -i -C — real output, not a
    reading of the code.
  • Inferred, not measured: that the two remaining allocations are
    unavoidable without a lifetime change. The reasoning is the erase, which is
    visible in the code; no alternative design was prototyped and measured.
  • Not measured: the time cost of either the current state or a fix. Nothing
    here claims a latency change; 152 bytes of short-lived churn per dispatch may
    be entirely acceptable.
  • Not measured: how much of a realistic workload is serial. Under concurrent
    load the strand stays armed and this cost amortises away completely.
  • Not verified: the figures on libc++, MSVC or the WASM toolchain.

What would change the verdict

  • Close as invalid if a measurement shows the remaining 152 bytes are noise
    for every workload that matters — which is the likely outcome, and is why this
    is filed rather than worked. 11% of an allocation census is not by itself a
    reason to change a lifetime rule.
  • Raise in priority if a serial GUI workload is demonstrated to be
    allocation-bound, or if a deregisterModel hook lands on StrandExecutor for
    an unrelated reason, at which point the retained-slot design costs much less.

Filed as the remainder of morph#660 rather than reopening it: morph#660's own
"Directions" section named three, one was taken, and the other two are a
different change.

🤖 Generated with Claude Code

https://claude.ai/code/session_01VptDWG2fKr2vBnLSJcgzgW

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    area: coreSubsystem: coreenhancementNew feature or requesttriage: validWell-framed; implement as written

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions