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>
177 lines
7.8 KiB
Plaintext
177 lines
7.8 KiB
Plaintext
# hsmc dispatch baseline — instruction counts from iai-callgrind.
|
|
#
|
|
# Current numbers reflect the codebase after the terminate-path + u64-ns
|
|
# pass. `do_terminate()` indexes into a precomputed `__TERMINATE_DATA`
|
|
# table (one slice per starting state, root inclusive); `TimerEntry`'s
|
|
# `remaining_ns` field shrunk from u128 to u64 (~584 years of headroom),
|
|
# halving the entry size and replacing per-step u128 saturating-sub work
|
|
# with u64. The smaller machine struct improves cache density on every
|
|
# bench, even those that don't use timers.
|
|
#
|
|
# Re-run with:
|
|
#
|
|
# cargo bench --bench dispatch
|
|
#
|
|
# Named baselines under target/iai/ for diffing past changes:
|
|
# pre_lca_fix — original O(H²) LCA contains() scan
|
|
# before_path_precompute — post-LCA-fix, pre-precompute
|
|
# before_timer_skip — post-precompute, pre-timer-skip (runtime is_empty)
|
|
# before_dispatch_precompute — post-timer-skip, pre-dispatch-table
|
|
# before_timer_elide — post-dispatch-table, pre-codegen-elide
|
|
# before_terminate_and_timer_opts — post-codegen-elide, pre-terminate-path / u64
|
|
#
|
|
# To diff a future change against the current numbers:
|
|
#
|
|
# cargo bench --bench dispatch -- --save-baseline=before_<your_change>
|
|
# <change>
|
|
# cargo bench --bench dispatch -- --baseline=before_<your_change>
|
|
#
|
|
# Numbers below are from a 6.19.14-zen kernel x86_64 box, valgrind 3.25.1,
|
|
# iai-callgrind 0.16.1, --release.
|
|
|
|
dispatch::dispatch::h2_cold_start cold:setup_h2_cold()
|
|
Instructions: 38
|
|
Estimated Cycles: 201
|
|
|
|
dispatch::dispatch::h2_warm_lateral warm:setup_h2_warm()
|
|
Instructions: 84
|
|
Estimated Cycles: 397
|
|
|
|
dispatch::dispatch::h4_cross_tree cross:setup_h4_warm()
|
|
Instructions: 101
|
|
Estimated Cycles: 456
|
|
|
|
dispatch::dispatch::h4_up up:setup_h4_warm()
|
|
Instructions: 98
|
|
Estimated Cycles: 384
|
|
|
|
dispatch::dispatch::h4_self self_:setup_h4_warm()
|
|
Instructions: 101
|
|
Estimated Cycles: 456
|
|
|
|
dispatch::dispatch::h8_cross_tree cross:setup_h8_warm()
|
|
Instructions: 93
|
|
Estimated Cycles: 443
|
|
|
|
dispatch::dispatch::bubble_leaf leaf:setup_bubble_warm()
|
|
Instructions: 99
|
|
Estimated Cycles: 453
|
|
|
|
dispatch::dispatch::bubble_mid mid:setup_bubble_warm()
|
|
Instructions: 99
|
|
Estimated Cycles: 453
|
|
|
|
dispatch::dispatch::bubble_top top:setup_bubble_warm()
|
|
Instructions: 99
|
|
Estimated Cycles: 453
|
|
|
|
dispatch::dispatch::timer_fire_dispatch fire:setup_timer_fire_armed()
|
|
Instructions: 191
|
|
Estimated Cycles: 706
|
|
|
|
dispatch::dispatch::terminate_event quit:setup_term_warm()
|
|
Instructions: 69
|
|
Estimated Cycles: 311
|
|
|
|
dispatch::dispatch::bubble_cross_tree switch:setup_bubble_cross_warm()
|
|
Instructions: 94
|
|
Estimated Cycles: 444
|
|
|
|
dispatch::dispatch::chain_self_emit chain:setup_chain_warm()
|
|
Instructions: 419
|
|
Estimated Cycles: 1301
|
|
|
|
# ─── LCA O(H) fix delta (pre_lca_fix → before_path_precompute) ──────
|
|
#
|
|
# Bench Instr Δ Cycle Δ
|
|
# h2_warm_lateral -22.8% -22.1%
|
|
# h4_cross_tree -25.9% -10.6%
|
|
# h4_self -21.5% -11.7%
|
|
# h8_cross_tree -46.2% -41.0% ← deepest LCA stress
|
|
# chain_self_emit -15.4% -10.4%
|
|
#
|
|
# ─── Precompute-paths delta (before_path_precompute → before_timer_skip) ──
|
|
#
|
|
# Bench Instr Δ Cycle Δ
|
|
# h2_warm_lateral -19.5% -4.8%
|
|
# h4_cross_tree -45.0% -28.6%
|
|
# h8_cross_tree -48.6% -34.8%
|
|
# chain_self_emit -28.1% -14.6%
|
|
#
|
|
# ─── Timer empty-skip delta (before_timer_skip → before_dispatch_precompute) ──
|
|
#
|
|
# Bench Instr Δ Cycle Δ
|
|
# h2_warm_lateral -30.8% -35.1%
|
|
# h4_cross_tree -38.4% -35.4%
|
|
# h8_cross_tree -54.4% -52.9%
|
|
# chain_self_emit -27.9% -29.2%
|
|
#
|
|
# ─── Dispatch-table delta (before_dispatch_precompute → before_timer_elide) ──
|
|
#
|
|
# Bench Instr Δ Cycle Δ
|
|
# bubble_mid -19.4% -4.7% 3-hop __PARENT walk → table lookup
|
|
# bubble_top -22.3% -5.8% 4-hop walk → table lookup
|
|
# h8_cross_tree +4.2% +15.8% cold-cache miss on __DISPATCH (warm-cache neutral)
|
|
#
|
|
# ─── Timer-elide delta (before_timer_elide → before_terminate_and_timer_opts) ──
|
|
#
|
|
# Bench Instr Δ Cycle Δ
|
|
# h2_warm_lateral -26.9% -9.7%
|
|
# h4_cross_tree -22.2% -20.1%
|
|
# h8_cross_tree -24.2% -20.8%
|
|
# bubble_top -22.1% -23.2%
|
|
# chain_self_emit -18.0% -8.4%
|
|
#
|
|
# ─── Terminate-path + u64-ns delta (before_terminate_and_timer_opts → current) ──
|
|
#
|
|
# Bench Instr Δ Cycle Δ Notes
|
|
# h2_cold_start -2.6% -14.1% smaller machine struct, tighter cache
|
|
# h2_warm_lateral -3.4% -21.2% (RAM hits 11→8)
|
|
# h4_cross_tree -3.8% -1.7%
|
|
# h4_up -3.9% -22.3% (RAM hits 10→7)
|
|
# h4_self -3.8% -8.4%
|
|
# h8_cross_tree -1.1% -0.7%
|
|
# bubble_leaf -6.6% -9.6%
|
|
# bubble_mid -6.6% -3.0%
|
|
# bubble_top -6.6% -9.6%
|
|
# timer_fire_dispatch +2.7% -12.6% smaller TimerEntry → -3 RAM hits
|
|
# terminate_event -22.5% -23.4% ← headline: precomputed exit path + queue.clear()
|
|
# bubble_cross_tree -2.1% -0.9%
|
|
# chain_self_emit -0.9% -3.1%
|
|
#
|
|
# Two changes in this pass:
|
|
#
|
|
# (1) `__TERMINATE_DATA` / `__TERMINATE_RANGE` — per-state precomputed
|
|
# exit chain up to and including __Root. `do_terminate()` indexes the
|
|
# table once and iterates the slice, replacing the runtime `__PARENT`
|
|
# walk + per-iteration match. Also swapped the queue-drain loop
|
|
# (`while self.queue.pop_front().is_some() {}`) for `self.queue.clear()`,
|
|
# which heapless 0.8 implements as O(1) (resets indices, no per-element
|
|
# pop). Together: -22% instr / -23% cycles on terminate_event, the
|
|
# headline win the user asked for.
|
|
#
|
|
# (2) `TimerEntry.remaining_ns: u128 → u64`. Internal-only field type
|
|
# change. u64 nanoseconds bounds at ~584 years — past any realistic
|
|
# Duration. Eliminates u128 saturating-sub in `decrement()` (4-6 instr
|
|
# → 1-2 instr per entry per step) and the manual secs/nanos
|
|
# decomposition in `min_remaining()` (replaced with
|
|
# `Duration::from_nanos(u64)`). The struct shrinks 32 B → 16 B with
|
|
# alignment, so the entire machine struct is smaller — even no-timer
|
|
# benches see RAM-hit reductions from tighter cache packing.
|
|
#
|
|
# Net across the six optimization passes (pre_lca_fix → current):
|
|
# h8_cross_tree dispatch is **-82% instructions / -72% cycles** vs the
|
|
# original O(H²) LCA scan. terminate_event is now 69 instr / 311 cycles
|
|
# — about 80% of that is the iai-callgrind wrapper, leaving ~14 instr
|
|
# of actual HSM termination work for a 3-deep chart.
|
|
#
|
|
# ─── Bench coverage (3 benches added in the previous pass) ──────────
|
|
#
|
|
# `timer_fire_dispatch` (191 instr / 706 cycles) — only bench that
|
|
# exercises pop_expired's non-empty branch and `dispatch_trigger`.
|
|
# `terminate_event` (69 instr / 311 cycles) — only bench that
|
|
# exercises `__check_terminate`'s true arm and `do_terminate`.
|
|
# `bubble_cross_tree` (94 instr / 444 cycles) — handler on L1 (4
|
|
# bubble hops) firing a 10-state cross-tree transition. Realistic
|
|
# HSM worst case.
|