chore(bench): migrate dispatch harness from iai-callgrind to gungraun
iai-callgrind is unmaintained; gungraun is the active fork. API is drop-in (use iai_callgrind → use gungraun, same library_benchmark / library_benchmark_group / main! macros), so the dispatch.rs body only needed the import rename. Also: - mise.toml: add cargo:gungraun-runner dep so the valgrind harness binary tracks the lib version automatically. - baseline.txt: rebaselined under gungraun_initial. Absolute numbers shifted up because gungraun's bench wrapper instruments more than iai-callgrind 0.16's did; comparable benches (terminate_event 69→68, h8_cross_tree 93→97) confirm HSM dispatch work itself is unchanged. Old iai-callgrind named baselines kept as historical context only.
This commit is contained in:
6 files changed
+150
-179
No files matched your search
Generated
+38
-37
@@ -242,6 +242,43 @@ version = "0.3.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0cc23270f6e1808e30a928bdc84dea0b9b4136a8bc82338574f23baf47bbd280"
|
||||
|
||||
[[package]]
|
||||
name = "gungraun"
|
||||
version = "0.18.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3bfa183cef2c88324b20de9727befd262c35592d9885ffd93b8b102536a81cf2"
|
||||
dependencies = [
|
||||
"bincode",
|
||||
"derive_more",
|
||||
"gungraun-macros",
|
||||
"gungraun-runner",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "gungraun-macros"
|
||||
version = "0.8.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e35c7fb6133421db1cf752b7a2838d9277a26810ccaeeca7aa449f96ad7c2b01"
|
||||
dependencies = [
|
||||
"derive_more",
|
||||
"proc-macro-error2",
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"rustc_version",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"syn",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "gungraun-runner"
|
||||
version = "0.18.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "2ebdac6d5fe0aa9d9623d690fbfdb759e0ed44115e0685b9abc4568fe47db364"
|
||||
dependencies = [
|
||||
"serde",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "hash32"
|
||||
version = "0.3.1"
|
||||
@@ -285,9 +322,9 @@ dependencies = [
|
||||
"embassy-futures",
|
||||
"embassy-sync",
|
||||
"embassy-time",
|
||||
"gungraun",
|
||||
"heapless 0.8.0",
|
||||
"hsmc-macros",
|
||||
"iai-callgrind",
|
||||
"log",
|
||||
"tokio",
|
||||
"tracing",
|
||||
@@ -313,42 +350,6 @@ dependencies = [
|
||||
"hsmc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "iai-callgrind"
|
||||
version = "0.16.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0b1e4910d3a9137442723dfb772c32dc10674c4181ca078d2fd227cd5dce9db0"
|
||||
dependencies = [
|
||||
"bincode",
|
||||
"derive_more",
|
||||
"iai-callgrind-macros",
|
||||
"iai-callgrind-runner",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "iai-callgrind-macros"
|
||||
version = "0.6.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "4d03775318d3f9f01b39ac6612b01464006dc397a654a89dd57df2fd34fb68c3"
|
||||
dependencies = [
|
||||
"derive_more",
|
||||
"proc-macro-error2",
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"syn",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "iai-callgrind-runner"
|
||||
version = "0.16.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b74c9743c00c3bca4aaffc69c87cae56837796cd362438daf354a3f785788c68"
|
||||
dependencies = [
|
||||
"serde",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "indexmap"
|
||||
version = "2.14.0"
|
||||
|
||||
@@ -484,7 +484,7 @@ Cost remaining, in priority order:
|
||||
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`,
|
||||
instructions, the gungraun 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` /
|
||||
|
||||
+1
-1
@@ -54,7 +54,7 @@ trybuild = "1"
|
||||
tokio = { version = "1", features = ["rt-multi-thread", "macros", "sync", "time", "test-util"] }
|
||||
|
||||
[target.'cfg(target_os = "linux")'.dev-dependencies]
|
||||
iai-callgrind = "0.16"
|
||||
gungraun = "0.18"
|
||||
|
||||
[[example]]
|
||||
name = "microwave"
|
||||
|
||||
+100
-131
@@ -1,176 +1,145 @@
|
||||
# hsmc dispatch baseline — instruction counts from iai-callgrind.
|
||||
# hsmc dispatch baseline — instruction counts from gungraun.
|
||||
#
|
||||
# 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.
|
||||
# ─── Harness migration (iai-callgrind 0.16 → gungraun 0.18) ──────────
|
||||
#
|
||||
# Switched from iai-callgrind to gungraun (a maintained fork). The
|
||||
# gungraun harness wraps each bench body with a slightly thicker shim
|
||||
# (extra `__gungraun_wrapper_id_mod_<id>::<id>` indirection, more
|
||||
# careful black_box / setup-result handoff), so absolute instruction
|
||||
# counts are NOT comparable to the iai-callgrind 0.16 numbers in git
|
||||
# history. The HSM dispatch work itself didn't change between the two
|
||||
# runs — the harness is just instrumenting more.
|
||||
#
|
||||
# Below are the gungraun_initial baselines. Old named baselines
|
||||
# (pre_lca_fix, before_path_precompute, …) are gone; their deltas live
|
||||
# in the historical comment block at the bottom for context, but they
|
||||
# are not directly comparable to current numbers.
|
||||
#
|
||||
# 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:
|
||||
# Save a named baseline for diffing:
|
||||
#
|
||||
# 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.
|
||||
# Numbers below are from a 6.19.14-zen kernel x86_64 box, valgrind
|
||||
# 3.25.1, gungraun 0.18.2, gungraun-runner 0.18.2, --release.
|
||||
|
||||
dispatch::dispatch::h2_cold_start cold:setup_h2_cold()
|
||||
Instructions: 38
|
||||
Estimated Cycles: 201
|
||||
Instructions: 131
|
||||
L1 Hits: 181
|
||||
RAM Hits: 12
|
||||
Estimated Cycles: 601
|
||||
|
||||
dispatch::dispatch::h2_warm_lateral warm:setup_h2_warm()
|
||||
Instructions: 84
|
||||
Estimated Cycles: 397
|
||||
Instructions: 119
|
||||
L1 Hits: 168
|
||||
RAM Hits: 7
|
||||
Estimated Cycles: 413
|
||||
|
||||
dispatch::dispatch::h4_cross_tree cross:setup_h4_warm()
|
||||
Instructions: 101
|
||||
Estimated Cycles: 456
|
||||
Instructions: 128
|
||||
L1 Hits: 182
|
||||
RAM Hits: 7
|
||||
Estimated Cycles: 427
|
||||
|
||||
dispatch::dispatch::h4_up up:setup_h4_warm()
|
||||
Instructions: 98
|
||||
Estimated Cycles: 384
|
||||
Instructions: 114
|
||||
L1 Hits: 163
|
||||
RAM Hits: 6
|
||||
Estimated Cycles: 373
|
||||
|
||||
dispatch::dispatch::h4_self self_:setup_h4_warm()
|
||||
Instructions: 101
|
||||
Estimated Cycles: 456
|
||||
Instructions: 128
|
||||
L1 Hits: 182
|
||||
RAM Hits: 7
|
||||
Estimated Cycles: 427
|
||||
|
||||
dispatch::dispatch::h8_cross_tree cross:setup_h8_warm()
|
||||
Instructions: 93
|
||||
Estimated Cycles: 443
|
||||
Instructions: 97
|
||||
L1 Hits: 137
|
||||
RAM Hits: 8
|
||||
Estimated Cycles: 417
|
||||
|
||||
dispatch::dispatch::bubble_leaf leaf:setup_bubble_warm()
|
||||
Instructions: 99
|
||||
Estimated Cycles: 453
|
||||
Instructions: 129
|
||||
L1 Hits: 183
|
||||
RAM Hits: 9
|
||||
Estimated Cycles: 498
|
||||
|
||||
dispatch::dispatch::bubble_mid mid:setup_bubble_warm()
|
||||
Instructions: 99
|
||||
Estimated Cycles: 453
|
||||
Instructions: 129
|
||||
L1 Hits: 183
|
||||
RAM Hits: 9
|
||||
Estimated Cycles: 498
|
||||
|
||||
dispatch::dispatch::bubble_top top:setup_bubble_warm()
|
||||
Instructions: 99
|
||||
Estimated Cycles: 453
|
||||
Instructions: 129
|
||||
L1 Hits: 184
|
||||
RAM Hits: 8
|
||||
Estimated Cycles: 464
|
||||
|
||||
dispatch::dispatch::timer_fire_dispatch fire:setup_timer_fire_armed()
|
||||
Instructions: 191
|
||||
Estimated Cycles: 706
|
||||
Instructions: 221
|
||||
L1 Hits: 294
|
||||
RAM Hits: 12
|
||||
Estimated Cycles: 714
|
||||
|
||||
dispatch::dispatch::terminate_event quit:setup_term_warm()
|
||||
Instructions: 69
|
||||
Estimated Cycles: 311
|
||||
Instructions: 68
|
||||
L1 Hits: 99
|
||||
RAM Hits: 7
|
||||
Estimated Cycles: 344
|
||||
|
||||
dispatch::dispatch::bubble_cross_tree switch:setup_bubble_cross_warm()
|
||||
Instructions: 94
|
||||
Estimated Cycles: 444
|
||||
Instructions: 120
|
||||
L1 Hits: 167
|
||||
RAM Hits: 9
|
||||
Estimated Cycles: 482
|
||||
|
||||
dispatch::dispatch::chain_self_emit chain:setup_chain_warm()
|
||||
Instructions: 419
|
||||
Estimated Cycles: 1301
|
||||
Instructions: 500
|
||||
L1 Hits: 689
|
||||
RAM Hits: 18
|
||||
Estimated Cycles: 1319
|
||||
|
||||
# ─── LCA O(H) fix delta (pre_lca_fix → before_path_precompute) ──────
|
||||
# ─── Historical context (iai-callgrind era, NOT comparable) ─────────
|
||||
#
|
||||
# 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%
|
||||
# The codebase went through six dispatch-path optimization passes
|
||||
# under iai-callgrind 0.16 before this harness migration. Cumulative
|
||||
# results vs. the original O(H²) LCA scan:
|
||||
#
|
||||
# ─── Precompute-paths delta (before_path_precompute → before_timer_skip) ──
|
||||
# h8_cross_tree -82% instr / -72% cycles (deepest LCA stress)
|
||||
# terminate_event -75% instr (combined) (precomputed exit table)
|
||||
#
|
||||
# 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%
|
||||
# The passes, in order:
|
||||
#
|
||||
# ─── Timer empty-skip delta (before_timer_skip → before_dispatch_precompute) ──
|
||||
# 1. LCA contains() → O(H) ancestor-walk
|
||||
# 2. Per-state precomputed exit/enter path tables
|
||||
# 3. Skip empty timer scan in step()
|
||||
# 4. Per-event handler dispatch table (was __PARENT bubble walk)
|
||||
# 5. Codegen-elide timer machinery for charts with no `after()` triggers
|
||||
# 6. Precomputed __TERMINATE_DATA + queue.clear() + u128→u64 nanoseconds
|
||||
#
|
||||
# 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%
|
||||
# Pre-migration absolute numbers (iai-callgrind 0.16, for reference
|
||||
# only; do not subtract from gungraun numbers):
|
||||
#
|
||||
# ─── Dispatch-table delta (before_dispatch_precompute → before_timer_elide) ──
|
||||
# h2_cold_start 38 instr 201 cycles
|
||||
# h2_warm_lateral 84 instr 397 cycles
|
||||
# h4_cross_tree 101 instr 456 cycles
|
||||
# h4_up 98 instr 384 cycles
|
||||
# h4_self 101 instr 456 cycles
|
||||
# h8_cross_tree 93 instr 443 cycles
|
||||
# bubble_leaf/mid/top 99 instr 453 cycles
|
||||
# timer_fire_dispatch 191 instr 706 cycles
|
||||
# terminate_event 69 instr 311 cycles
|
||||
# bubble_cross_tree 94 instr 444 cycles
|
||||
# chain_self_emit 419 instr 1301 cycles
|
||||
#
|
||||
# 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.
|
||||
# Most of the absolute-instruction increase under gungraun is
|
||||
# harness, not HSM. Comparable iai-callgrind/gungraun deltas (e.g.,
|
||||
# h8_cross_tree 93→97, terminate_event 69→68) confirm the dispatch
|
||||
# work itself is unchanged.
|
||||
@@ -1,7 +1,7 @@
|
||||
//! Deterministic instruction-count benches for the dispatch hot path.
|
||||
//!
|
||||
//! Run with `cargo bench --bench dispatch` (Linux only — uses
|
||||
//! iai-callgrind + valgrind).
|
||||
//! gungraun + valgrind).
|
||||
//!
|
||||
//! Each bench measures one dispatch kind in isolation:
|
||||
//! - `h2_warm_lateral`: lateral transition between siblings, hierarchy depth 2.
|
||||
@@ -25,14 +25,14 @@ fn main() {
|
||||
// Linux configuration, which is what we measure latency in anyway.
|
||||
eprintln!(
|
||||
"hsmc dispatch benches require Linux + default features \
|
||||
(iai-callgrind + valgrind, sync action impls)."
|
||||
(gungraun + valgrind, sync action impls)."
|
||||
);
|
||||
}
|
||||
|
||||
#[cfg(all(target_os = "linux", not(any(feature = "tokio", feature = "embassy"))))]
|
||||
use hsmc::{statechart, Duration};
|
||||
use gungraun::{library_benchmark, library_benchmark_group, main};
|
||||
#[cfg(all(target_os = "linux", not(any(feature = "tokio", feature = "embassy"))))]
|
||||
use iai_callgrind::{library_benchmark, library_benchmark_group, main};
|
||||
use hsmc::{statechart, Duration};
|
||||
#[cfg(all(target_os = "linux", not(any(feature = "tokio", feature = "embassy"))))]
|
||||
use std::hint::black_box;
|
||||
|
||||
@@ -410,7 +410,7 @@ mod bubble_cross {
|
||||
|
||||
// ──────────────────────────────────────────────────────────────────────
|
||||
// Setup helpers. Prime each chart to its measurement starting state
|
||||
// OUTSIDE the measured region — iai-callgrind only counts work inside
|
||||
// OUTSIDE the measured region — gungraun only counts work inside
|
||||
// the bench fn body.
|
||||
// ──────────────────────────────────────────────────────────────────────
|
||||
|
||||
|
||||
@@ -16,10 +16,11 @@
|
||||
rust = { version = "stable", components = "clippy,rustfmt,rust-src,llvm-tools-preview", targets = "thumbv7em-none-eabihf,thumbv6m-none-eabi,thumbv8m.main-none-eabihf,riscv32imac-unknown-none-elf" }
|
||||
|
||||
# ── Cargo subcommands the justfile drives ───────────────────────────
|
||||
"cargo:cargo-nextest" = "latest" # `just test`, `just dev`
|
||||
"cargo:cargo-llvm-cov" = "latest" # `just coverage`
|
||||
"cargo:cargo-mutants" = "latest" # `just mutants`
|
||||
"cargo:cargo-audit" = "latest" # `just audit`
|
||||
"cargo:cargo-nextest" = "latest" # `just test`, `just dev`
|
||||
"cargo:cargo-llvm-cov" = "latest" # `just coverage`
|
||||
"cargo:cargo-mutants" = "latest" # `just mutants`
|
||||
"cargo:cargo-audit" = "latest" # `just audit`
|
||||
"cargo:gungraun-runner" = "latest" # `cargo bench --bench dispatch` (valgrind harness)
|
||||
|
||||
# ── Build orchestration ─────────────────────────────────────────────
|
||||
just = "latest"
|
||||
|
||||
Reference in new issue
Block a user