perf: precompute transition/dispatch/terminate paths; shrink TimerEntry to u64
Six codegen-side optimization passes against step() and friends. No user-facing API changes; all 296 tests pass under no-default, tokio, embassy, and tokio+journal. Cumulative on h8_cross_tree (deepest dispatch in the suite): -82% instructions / -72% cycles vs the pre_lca_fix baseline. * __PATH_DATA + __PATH_RANGE: per-(from, to) precomputed exit/enter sequences. Up/self/lateral semantics fold into one indexed lookup; removes lca() and is_up_transition() runtime helpers and the per-call heapless::Vec<u16, 16> ancestry build. Hierarchy depth no longer bounded by Vec capacity. * __DISPATCH[(state, event)]: precomputed handler resolution. The runtime __PARENT bubble walk + per-hop __handler_lookup call in step() collapses to one indexed lookup. Bubble resolution is now O(1) regardless of depth -- bubble_top (4-hop) is within 3 instructions of bubble_leaf (handler on current). __handler_lookup retained for timer dispatch. * __TERMINATE_DATA: per-state precomputed exit chain (root inclusive -- chart-root entry/exit actions are valid). do_terminate() indexes the slice instead of walking __PARENT, and uses Deque::clear() (O(1) in heapless 0.8) instead of pop_front-loop. terminate_event bench: -22.5% instr / -23.4% cycles. * TimerTable empty-skip: pop_expired / min_remaining / decrement / cancel_state each get inlined is_empty() early-out with the non-empty body marked #[cold]. * Codegen-static timer-machinery elide: when chart has no Duration triggers, every TimerTable call is omitted from step() and exit_state(). step() returns None directly. dispatch_trigger and the timer-fired arms remain but are unreachable; LLVM DCEs them. * TimerEntry.remaining_ns: u128 -> u64 (~584-year ceiling). Halves entry size; replaces u128 saturating-sub with u64. The smaller machine struct improves cache density on every bench, even timer-free ones. Bench coverage: four new benches added to cover paths the original suite missed -- bubble_leaf/mid/top (handler resolution at varying ancestor depths), timer_fire_dispatch (only bench exercising pop_expired's non-empty branch and dispatch_trigger), terminate_event (only bench exercising do_terminate), bubble_cross_tree (deep bubble + 10-state cross-tree, the realistic HSM worst case). See hsmc/benches/baseline.txt for the per-pass delta blocks and docs/003. latency-analysis.md sections 1, 9, and 12 for the cost-surface analysis. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
6 files changed
+1249
-338
No files matched your search
+110
-73
@@ -26,23 +26,30 @@ can pick from it; nothing here is a commitment.
|
||||
|
||||
| # | Source | Class | Per-event cost (estimate) | Location |
|
||||
|----|-----------------------------------------------|-----------------------------|------------------------------------------------------------------------------------------------------------|---------------------------------------|
|
||||
| 1 | `dispatch().await` loop | Discretionary, async-only | 2 awaits cold-start / 1 await steady-state per dispatched event. Each is ~free if action body never `Pending`-yields. | `codegen.rs:2330-2345` |
|
||||
| 2 | LCA computation for lateral transitions | Discretionary | ~~O(H²) `heapless::Vec<u16,32>::contains` linear scan, H ≤ 32 hard limit~~. **Now O(H) via depth-walk over the `__DEPTH` table. Measured impact: -46% instructions on h8_cross_tree, -23% on h2_warm_lateral, +1% noise on h4_up (doesn't go through `lca()`). See `hsmc/benches/baseline.txt` and §6 in mitigation table.** | `codegen.rs:2601-2628` |
|
||||
| 3 | Entry-path `heapless::Vec` build | Discretionary | One stack `heapless::Vec<u16,16>` filled per transition; H pushes + H reads. | `codegen.rs:2586-2598` |
|
||||
| 4 | Parent walk for handler search | Intrinsic | O(H) `__PARENT[s]` indexing until first matching state. | `codegen.rs:2283-2297` |
|
||||
| 5 | `__handler_lookup` nested match | Intrinsic | LLVM lowers to jump tables (confirmed via `LJTI*` labels in spot-check). ~1 indirect jump per call. | `codegen.rs` (search "__handler_lookup") |
|
||||
| 6 | `run_action` dispatch | Intrinsic | One match arm + monomorphized trait call + 1 vtable hop on `&mut dyn QueuePush` (the `EmitProxy`). | `codegen.rs:2425-2489` |
|
||||
| 7 | Event queue push/pop | Intrinsic | `heapless::Deque::push_back` / `pop_front`, both O(1). | `codegen.rs:2316-2319` |
|
||||
| 8 | `__chart_observe!` macro arms | Compile-eliminable when off | Verified: zero residue when all sink features are off. Each call site collapses to `{};`. | `lib.rs:158-712` |
|
||||
| 9 | Journal `Vec::push` per atom | Conditional on `journal` | Amortized O(1); ~16 B per `TraceEvent`. Expanded chart shows 104 push call sites; ~11 fire per transition. | `journal.rs:26-32`, `lib.rs:158-712` |
|
||||
| 10 | Async-fn future size (binary cost) | Discretionary, async-only | `step` closure ~970 lines of asm; `run_handlers` ~830; total Lat-chart machinery ≈ 3000 asm lines. | spot-check measurement |
|
||||
| 11 | `tokio::sync::mpsc::unbounded_channel` | Tokio-only, std-bound | Heap-backed channel for cross-task event injection. Embassy uses bounded `embassy_sync::channel` instead. | runtime adapters |
|
||||
| # | Source | Class | Per-event cost (estimate) | Status |
|
||||
|----|-----------------------------------------------|-----------------------------|------------------------------------------------------------------------------------------------------------|---------------------------------------|
|
||||
| 1 | `dispatch().await` loop | Discretionary, async-only | 2 awaits cold-start / 1 await steady-state per dispatched event. ~free if action body never `Pending`-yields. | open |
|
||||
| 2 | LCA + transition path build | Discretionary | ~~O(H²) `heapless::Vec<u16,32>::contains` scan~~ → ~~O(H) depth-walk~~ → **O(1) `__PATH_RANGE` lookup**. Up/self/lateral folded into the precomputed table; `lca()`, `is_up_transition()`, and the per-call `heapless::Vec<u16,16>` ancestry build are all gone. | resolved |
|
||||
| 3 | Parent walk for handler search | Discretionary | ~~O(H) `__PARENT` walk + per-hop `__handler_lookup` call~~ → **O(1) `__DISPATCH[(state, event)]` lookup**. Every (state, event) pair statically resolved at codegen. `__handler_lookup` retained for timer dispatch only. | resolved |
|
||||
| 4 | `__handler_lookup` nested match (timer path) | Intrinsic | Still used for timer dispatch. LLVM lowers to jump tables (verified `LJTI*`). ~1 indirect jump per call. | open (already at floor) |
|
||||
| 5 | `run_action` dispatch | Intrinsic | One match arm + monomorphized trait call + 1 vtable hop on `&mut dyn QueuePush` (verified devirtualized when callsite type is known). | open (already at floor) |
|
||||
| 6 | Event queue push/pop | Intrinsic | `heapless::Deque::push_back` / `pop_front`, both O(1). | open (already at floor) |
|
||||
| 7 | `__chart_observe!` macro arms | Compile-eliminable when off | Zero residue when all sink features off. Each call site collapses to `{};`. | verified zero-cost |
|
||||
| 8 | Journal `Vec::push` per atom | Conditional on `journal` | Amortized O(1); ~16 B per `TraceEvent`. ~11 push sites fire per transition. | opt-in |
|
||||
| 9 | Async-fn future size (binary cost) | Discretionary, async-only | ~3000 asm lines of chart machinery for a 5-state chart; dominated by async-fn state machines. | open (mitigation H3) |
|
||||
| 10 | `tokio::sync::mpsc::unbounded_channel` | Tokio-only, std-bound | Heap-backed channel for cross-task event injection. Embassy uses bounded `embassy_sync::channel`. | feature-specific |
|
||||
| 11 | TimerTable per-step / per-exit fixed cost | Discretionary | ~~Unconditional `pop_expired` + `cancel_state` calls every step / exit~~ → **inlined `is_empty()` + `#[cold]` non-empty body** (runtime branch on charts that use timers; compile-time elided on timer-free charts). | resolved |
|
||||
| 12 | TimerEntry size + per-decrement work | Discretionary | ~~`remaining_ns: u128`, 32-byte entry, u128 saturating-sub per decrement~~ → **`u64`, 16-byte entry, u64 saturating-sub** (~584-year ceiling; Duration::from_nanos round-trips exactly). | resolved |
|
||||
| 13 | `do_terminate()` exit walk | Discretionary | ~~Runtime `__PARENT` walk + `pop_front`-loop queue drain~~ → **precomputed `__TERMINATE_DATA` slice + `Deque::clear()`** (O(1)). | resolved |
|
||||
|
||||
The top three discretionary items (1, 2, 3) account for almost all the
|
||||
optimization headroom. Items 4–7 are intrinsic to the chosen semantics —
|
||||
they're already at or near the algorithmic floor for hierarchical state
|
||||
machines. Item 8 is verified zero-cost. Item 9 is opt-in. Items 10 and 11
|
||||
matter for code size and feature ergonomics but not per-event µs.
|
||||
Items 2, 3, 11, 12, 13 are fully resolved by the precompute / inline-
|
||||
empty-check passes in this round of work — see `hsmc/benches/baseline.txt`
|
||||
for the per-pass delta blocks. Items 4–6 are at or near the algorithmic
|
||||
floor for hierarchical state machines. Item 7 is verified zero-cost.
|
||||
Items 1 (async overhead) and 9 (async machinery binary size) are the
|
||||
only meaningful open items; both are addressable via a `dispatch_sync()`
|
||||
fast path (mitigation H3 in §9), which would be a user-facing API
|
||||
change and is therefore out-of-scope for this round.
|
||||
|
||||
---
|
||||
|
||||
@@ -54,28 +61,32 @@ a simple lateral transition, no observation features, `cargo build
|
||||
|
||||
| Stage | Cycles (rough) | Notes |
|
||||
|----------------------------------------|--------------------|------------------------------------------------|
|
||||
| `queue.pop_front` | ~10 | `heapless::Deque` index op |
|
||||
| `__event_to_id` | ~5 | match arm |
|
||||
| Parent walk for handler | ~30 × H | ≤ H array indexes |
|
||||
| `__handler_lookup` | ~10 | jump table indirect |
|
||||
| `run_action` (1 action) | ~20-30 | match + monomorphized call + 1 vtable hop |
|
||||
| `is_up_transition` | ~30 | parent walk |
|
||||
| `lca` (lateral) | ~10 × H | depth-walk via `__DEPTH` (was ~50 × H² pre-fix)|
|
||||
| Exit chain (H states × A actions) | ~60 × H × A | |
|
||||
| Entry path build | ~30 × H | heapless::Vec push |
|
||||
| Enter chain (H states × A actions) | ~60 × H × A | |
|
||||
| **Total `.step()` steady-state** | ~1.0 – 2.0 µs | for H=3, A=1; user action body excluded |
|
||||
| `queue.pop_front` | ~5–10 | `heapless::Deque` index op |
|
||||
| `__event_to_id` | ~3–5 | match arm (jump table) |
|
||||
| `__DISPATCH[(state, event)]` lookup | ~5 | one indexed load + sentinel branch |
|
||||
| `run_action` (1 action) | ~10–20 | match + monomorphized call + 1 vtable hop |
|
||||
| Path lookup (`__PATH_RANGE`) | ~5 | one indexed load + slice arithmetic |
|
||||
| Exit chain (H states × A actions) | ~3 × H + 30 × A | per-state work is now ~3 instr each |
|
||||
| Enter chain (H states × A actions) | ~3 × H + 30 × A | per-state work is now ~3 instr each |
|
||||
| Timer machinery (timer-free chart) | 0 | codegen-elided when `ir.duration_triggers.is_empty()` |
|
||||
| Timer machinery (timer chart, no fire) | ~3 | inlined `is_empty()` branches × 3 |
|
||||
| Timer machinery (timer firing) | ~30–40 | decrement + pop_expired + dispatch_trigger |
|
||||
| **Total `.step()` steady-state** | ~0.4 – 0.8 µs | for H=3, A=1; user action body excluded |
|
||||
| Async overhead (`.dispatch()` loop) | per-await checkpoint | ~free if no `Pending`-yield |
|
||||
|
||||
These are back-of-envelope numbers; actual measurements would tighten
|
||||
them by ~2× in either direction, depending on inlining decisions.
|
||||
|
||||
For empirical numbers (deterministic, instruction-counted), see
|
||||
`hsmc/benches/baseline.txt`. Headline numbers post-LCA-fix:
|
||||
h2_warm_lateral 210 instr / ~1k cycles; h4_cross_tree 394 instr / ~1.4k
|
||||
cycles; h8_cross_tree 508 instr / ~1.6k cycles; chain_self_emit 1036 instr
|
||||
across 4 events (≈260 instr/event). Mitigations H1/H2 in §9 target the
|
||||
remaining LCA + entry-path stack-vec overhead.
|
||||
`hsmc/benches/baseline.txt`. Headline numbers (current):
|
||||
h2_warm_lateral 84 instr / 397 cycles; h4_cross_tree 101 instr / 456
|
||||
cycles; h8_cross_tree 93 instr / 443 cycles; bubble_top 99 instr /
|
||||
453 cycles (4-hop handler resolution); bubble_cross_tree 94 instr /
|
||||
444 cycles (4-hop bubble + 10-state cross-tree); terminate_event 69
|
||||
instr / 311 cycles; timer_fire_dispatch 191 instr / 706 cycles;
|
||||
chain_self_emit 419 instr across 4 events (≈105 instr/event).
|
||||
Cumulative on h8_cross_tree: **-82% instructions / -72% cycles** vs
|
||||
the original O(H²) LCA scan.
|
||||
|
||||
---
|
||||
|
||||
@@ -295,9 +306,8 @@ audit of every loop and await pattern in the runtime and codegen:
|
||||
|---------------------------------------|---------------------------|-------------------------|--------------------------|---------------------|
|
||||
| Main `run()` outer | codegen.rs:1290+, 1379+ | by `terminated` flag | yes — always | cooperative |
|
||||
| `dispatch()` drain loop | codegen.rs:2339 | by queue size | only via `step().await` | suspect — fairness |
|
||||
| Transition exit chain | codegen.rs:2532, 2577 | by hierarchy depth | yes (async variant) | OK |
|
||||
| Transition exit/enter slice loops | codegen.rs (transition) | by precomputed slice len| yes (async variant) | OK |
|
||||
| Termination drain | codegen.rs:2750 | by hierarchy + queue | yes (async variant) | OK |
|
||||
| `lca()` ancestry walk | codegen.rs:2605 | by hierarchy depth | no (sync helper) | OK |
|
||||
| Timer decrement / pop | runtime / lib.rs | by timer-table size | sync, off the event-loop | OK |
|
||||
| Send/recv backends | codegen.rs:1308, 1405 | n/a | always `.await` | cooperative |
|
||||
|
||||
@@ -363,21 +373,25 @@ feel: L = afternoon, M = couple of days, H = a week or more.
|
||||
|
||||
| ID | Mitigation | Targets | Impact | Effort | Cost |
|
||||
|-----|-----------------------------------------------------------------------------------------------------------------------------------|-----------|-----------------------------------------|--------|---------------------------------------|
|
||||
| H0 | ~~Replace `lca()` with an O(H) depth-walk using the existing `__DEPTH` table; remove the `heapless::Vec<u16,32>` set entirely.~~ **Landed.** Measured impact: h8_cross_tree -46% / -41% cycles; h4_cross_tree -26% / -11%; h2_warm_lateral -23% / -22%; h4_self -22% / -12%; chain_self_emit -15% / -10%. See `hsmc/benches/baseline.txt`. | #2 | (delivered) | - | - |
|
||||
| H1 | Codegen per-transition exit/entry/descend chains as `&'static [u16]` arrays; eliminate runtime `heapless::Vec` build entirely. | #2, #3 | High — kills LCA *and* heapless allocs | M | ~10–30 B per transition; bounded |
|
||||
| H2 | Same idea but precompute LCA only (small static table or per-transition LCA u16 field). | #2 | Med-High | L | ~2 B per transition |
|
||||
| H3 | Provide a `dispatch_sync()` API that bypasses `.await` when the chart's actions are fully synchronous. | #1, #10 | High for sync-action charts | L-M | API surface growth |
|
||||
| H4 | Codegen a per-state inlined event match (skip the parent walk by emitting `(state, event) → (handler_state, actions, target)` directly per state). | #4 | Low-Med | M | binary grows ~`states × events` |
|
||||
| M1 | Eliminate the trailing `step().await` in `dispatch()` when queue empties mid-step (let `step` signal "done" so the loop exits without re-entering). | #1 | Low | L | minor refactor |
|
||||
| M2 | `#[inline]` on `__handler_lookup`, `__state_name`, etc. for tighter cross-call inlining. | #5, #6 | Low (LLVM probably already does this) | L | none |
|
||||
| M3 | Lazy field eval in `trace-tracing` arm using `tracing`'s field-closure form to skip evaluation when filtered. | feature on| Low | L | none |
|
||||
| L1 | Audit residual `_t_state`, `_t_trigger`, `__handler_state` local bindings in observation-off builds — verify LLVM elides them. | feature off| Negligible | L | none |
|
||||
| H0 | Replace `lca()` with an O(H) depth-walk via the `__DEPTH` table. | #2 | **Landed.** | - | - |
|
||||
| H1 | Codegen per-(from, to) precomputed exit/enter chains as `__PATH_DATA` + `__PATH_RANGE`. | #2 | **Landed.** Up/self/lateral folded into one indexed lookup; `lca()` and `is_up_transition()` removed. | - | ~`6·N²` B index + ~`N²·H` B path data |
|
||||
| T1 | Inlined `is_empty()` early-out on `pop_expired` / `min_remaining` / `decrement` / `cancel_state` with `#[cold]` non-empty bodies. | #11 | **Landed.** | - | - |
|
||||
| T2 | Codegen-static elide of all `TimerTable` calls when `ir.duration_triggers.is_empty()`. | #11 | **Landed.** Most charts are timer-free; `step()` returns `None` directly. | - | - |
|
||||
| H4 | `__DISPATCH[(state, event)]` const array; one indexed lookup replaces the runtime __PARENT bubble walk + per-hop `__handler_lookup`. | #3 | **Landed.** Each (state, event) statically resolved at codegen. | - | ~24 B per (state, event) pair |
|
||||
| U1 | Shrink `TimerEntry.remaining_ns: u128 → u64` (~584-year ceiling). Halves entry size; replaces u128 saturating-sub with u64. | #12 | **Landed.** | - | - |
|
||||
| K1 | `__TERMINATE_DATA` per-state precomputed exit chain (root inclusive); `do_terminate()` indexes a slice instead of walking __PARENT, and uses `Deque::clear()` (O(1)) for the queue drain. | #13 | **Landed.** terminate_event -22.5% instr / -23.4% cycles. | - | ~`(H+1)·N` B path data |
|
||||
| H3 | Provide a `dispatch_sync()` API that bypasses `.await` when the chart's actions are fully synchronous. | #1, #9 | High for sync-action charts | L-M | API surface growth (user-facing) |
|
||||
| H5 | Sparse `__PATH_RANGE` / `__DISPATCH` for very large charts (N > 64): emit only reachable pairs with a small perfect-hash. Latency-neutral; cuts O(N²) static memory growth. | cost mitigation | Low at typical N; significant at N > 100 | M | codegen complexity |
|
||||
| M1 | Eliminate the trailing `step().await` in `dispatch()` when queue empties mid-step. | #1 | Low | L | minor refactor |
|
||||
| M2 | Lazy field eval in `trace-tracing` arm using `tracing`'s field-closure form to skip evaluation when filtered. | #8 (on) | Low | L | none |
|
||||
|
||||
H1 is the clear top candidate: it collapses the two largest discretionary
|
||||
costs (#2, #3) into one static-table lookup, removes one runtime stack
|
||||
allocation per transition, and is implementable purely as a codegen
|
||||
change in `hsmc-macros/src/codegen.rs` without touching the runtime
|
||||
contract.
|
||||
H0/H1/T1/T2/H4/U1/K1 all landed in the most recent round of work.
|
||||
Callgrind now reports the entire HSM machinery as a single `step`
|
||||
symbol — every helper inlined. The only remaining optimization avenue
|
||||
that meaningfully affects per-event latency is H3 (`dispatch_sync()`),
|
||||
which is excluded as user-facing. Memory-side mitigations (H5) only
|
||||
matter for N > 64-state charts; embedded users with large charts are
|
||||
the audience.
|
||||
|
||||
---
|
||||
|
||||
@@ -393,13 +407,13 @@ For anyone reading this audit and wanting to verify or extend it:
|
||||
- `:26-32` — `extern crate alloc` and `Vec` import (only here)
|
||||
- `:65-150+` — `TraceEvent` enum (the journal vocabulary)
|
||||
- **`hsmc-macros/src/codegen.rs`**
|
||||
- `:1997-1999` — `__PARENT`, `__DEPTH`, `__DEFAULT_CHILD` static-table generation
|
||||
- `:2247-2309` — `step()` (sync vs. async branches)
|
||||
- `:2330-2345` — `dispatch()` await loop
|
||||
- `:2425-2489` — `run_action` (sync vs. async)
|
||||
- `:2500-2598` — `transition`, `is_up_transition`, lateral entry-path build
|
||||
- `:2601-2615` — `lca()` runtime O(H²) computation
|
||||
- `:631-667` — name-lookup helpers (`__state_name`, `__action_name`, etc.)
|
||||
- `compute_transition_paths()` (top of file, after `compute_chart_hash`) — builds `__PATH_DATA` + `__PATH_RANGE` at codegen time
|
||||
- `__PARENT`, `__DEPTH`, `__DEFAULT_CHILD`, `__PATH_DATA`, `__PATH_RANGE` static-table emission (search "const __PARENT")
|
||||
- `step()` (search "fn step" / "async fn step") — sync vs. async branches
|
||||
- `dispatch()` await loop (search "fn dispatch")
|
||||
- `run_action` (search "fn run_action") — sync vs. async
|
||||
- `transition` (search "fn transition") — now a thin slice-iter wrapper
|
||||
- name-lookup helpers (`__state_name`, `__action_name`, etc.) at `:631-667`
|
||||
- **Test reference shape**: `hsmc/tests/state_chart_const.rs` is the
|
||||
smallest sync-only chart in the tree; `hsmc/tests/det_lateral.rs` is
|
||||
the reference async + journal shape used for the asm spot-check.
|
||||
@@ -439,25 +453,48 @@ handler lookup, action dispatch, queue ops) is small and well-monomorphized.
|
||||
Observation cost is verified zero when features are off, and stays
|
||||
proportional to the user's chosen sink mix when on.
|
||||
|
||||
**The LCA cost (item #2 in §1) has been addressed** with the depth-walk
|
||||
fix landed alongside this analysis. Measured impact peaks at
|
||||
**-46% instructions / -41% cycles** on the deepest cross-tree
|
||||
transition (`h8_cross_tree`); see `hsmc/benches/baseline.txt`. The
|
||||
former O(H²) scan and the `heapless::Vec<u16,32>` ancestry-set
|
||||
allocation are gone; the 32-state hierarchy ceiling is also gone.
|
||||
**Items #2, #3, #11, #12, and #13 in §1 have all landed** across six
|
||||
optimization passes (H0 → H1 → T1 → H4 → T2 → K1+U1). Cumulative impact
|
||||
on the deepest dispatch (h8_cross_tree) is **-82% instructions / -72%
|
||||
cycles** vs the original O(H²) LCA scan. The headline wins by bench:
|
||||
|
||||
The two remaining optimization targets are:
|
||||
- `terminate_event` 69 instr / 311 cycles (~14 instr of HSM work after
|
||||
subtracting bench wrapper) — `do_terminate()` is one indexed slice
|
||||
+ `Deque::clear()`.
|
||||
- `bubble_top` 99 instr / 453 cycles — 4-hop handler resolution
|
||||
collapses to one indexed `__DISPATCH` lookup; per-dispatch cost is
|
||||
bench-shape-independent.
|
||||
- `bubble_cross_tree` 94 instr / 444 cycles — realistic HSM worst
|
||||
case (deep bubble + 10-state cross-tree) costs the same as a plain
|
||||
cross-tree because bubble lookup is O(1) and transition is
|
||||
O(path-length).
|
||||
- `h8_cross_tree` 93 instr / 443 cycles — 7-deep cross-tree dispatch.
|
||||
|
||||
1. **Entry-path `heapless::Vec` build** (item #3 in §1) — still allocates
|
||||
one stack `heapless::Vec<u16,16>` per lateral transition. Could be
|
||||
replaced with codegen-time precomputed static `&'static [u16]`
|
||||
chains, eliminating the entry-path build entirely. (See mitigation H1.)
|
||||
2. **Async coloring code-size cost** (item #10) — chart machinery is
|
||||
Callgrind reports the entire HSM machinery as a single `step` symbol —
|
||||
`run_handlers`, `run_action`, `transition`, `do_terminate`,
|
||||
`exit_state`, `enter_state_no_descent`, `EmitProxy::push`, all four
|
||||
`TimerTable` methods, the dispatch-table lookup, and the
|
||||
terminate-path lookup are all inlined.
|
||||
|
||||
Cost remaining, in priority order:
|
||||
|
||||
1. **Async coloring code-size cost** (item #9) — chart machinery is
|
||||
~3,000 asm lines for a 5-state chart, dominated by async-fn state
|
||||
machines. A `dispatch_sync()` fast path (mitigation H3) would let
|
||||
users with all-sync actions skip this entirely.
|
||||
machines. Mitigation H3 (a `dispatch_sync()` fast path) would let
|
||||
users with all-sync actions skip this entirely. **User-facing API
|
||||
change** — out of scope for the current optimization pass.
|
||||
2. **Bench-wrapper noise floor** — at the smallest dispatch's ~38–80
|
||||
instructions, the iai-callgrind wrapper (`black_box`, `send`,
|
||||
instrumented call-edge crossing) accounts for ~45 instructions.
|
||||
Sub-bench-wrapper improvements are below the measurement floor.
|
||||
3. **Memory cost of `__PATH_DATA` / `__PATH_RANGE` / `__DISPATCH` /
|
||||
`__TERMINATE_DATA` at large N** — `__PATH_RANGE` is O(N²);
|
||||
`__DISPATCH` is O(N×E); `__TERMINATE_DATA` is O(N×H). For N ≤ 32 /
|
||||
E ≤ 16 / H ≤ 8 the total static footprint is a few KiB. A
|
||||
64-state × 32-event chart would push past ~80 KiB. Mitigation H5
|
||||
sketches a sparse encoding.
|
||||
|
||||
Both are addressable purely in `hsmc-macros/src/codegen.rs` without
|
||||
breaking runtime semantics or feature gates. The bench harness in
|
||||
`hsmc/benches/dispatch.rs` is in place to measure each mitigation as it
|
||||
lands.
|
||||
All addressable purely in `hsmc-macros/src/codegen.rs` and `hsmc/src/lib.rs`
|
||||
without breaking runtime semantics or feature gates. The bench harness
|
||||
in `hsmc/benches/dispatch.rs` (13 benches, every dispatch shape
|
||||
covered) is in place to measure each mitigation as it lands.
|
||||
Reference in new issue
Block a user