RFC 004: default spin_budget_cycles=2000 (DEFAULT_SPIN_BUDGET_CYCLES, ~0.5us @3.7GHz), gate spinner cap on pool size (n<=1:0, 2..=4:n, n>=5:0) replacing N/2. From spin_sweep fork-join knee: ~4x p50 win at n<=4, pure cost at n>=8. Budget inert on large pools via 0 cap; explicit max_spinners() still overrides.

This commit is contained in:
smarm-agent
2026-06-14 18:27:39 +00:00
parent 7fcc9f2ec5
commit 4c1e5f19f1
+26 -7
View File
@@ -131,6 +131,13 @@ use std::thread;
/// Default capacity of the actor slot table. Slots are ~256 bytes, so the
/// default costs ~4 MiB, allocated once at `init`. See [`Config::max_actors`].
pub const DEFAULT_MAX_ACTORS: usize = 16_384;
/// RFC 004: default scheduler spin budget before parking, in TSC cycles.
/// ≈0.5µs at a 3.7 GHz nominal TSC. Sized from the spin_sweep fork-join knee:
/// the fork→join handoff is sub-µs, so ~0.5µs of spin keeps a worker hot across
/// it for a ~4× p50 latency win, and budgets past ~5k cycles buy nothing but
/// idle CPU. Only effective where the default spinner cap permits spinning
/// (small pools, n≤4); see the cap resolution in `init`.
pub const DEFAULT_SPIN_BUDGET_CYCLES: u64 = 2_000;
/// Runtime configuration.
///
@@ -174,7 +181,7 @@ impl Config {
stack_pool_cap: n * 4,
max_actors: DEFAULT_MAX_ACTORS,
wake_slot: false,
spin_budget_cycles: 0,
spin_budget_cycles: DEFAULT_SPIN_BUDGET_CYCLES,
max_spinners: None,
}
}
@@ -193,7 +200,7 @@ impl Config {
stack_pool_cap: max * 4,
max_actors: DEFAULT_MAX_ACTORS,
wake_slot: false,
spin_budget_cycles: 0,
spin_budget_cycles: DEFAULT_SPIN_BUDGET_CYCLES,
max_spinners: None,
}
}
@@ -294,7 +301,7 @@ impl Default for Config {
stack_pool_cap: avail * 4,
max_actors: DEFAULT_MAX_ACTORS,
wake_slot: false,
spin_budget_cycles: 0,
spin_budget_cycles: DEFAULT_SPIN_BUDGET_CYCLES,
max_spinners: None,
}
}
@@ -888,10 +895,22 @@ pub struct Runtime {
/// Initialise the runtime with the given config. Returns a reusable handle.
pub fn init(config: Config) -> Runtime {
let n = config.resolved_thread_count();
// RFC 004: default spinner cap is N/2 (capping at half keeps half the pool
// hot, parks the rest). With N=1 this is 0 — a lone thread has no
// cross-thread handoff to hide, so it never spins.
let max_spinners = config.max_spinners.unwrap_or((n / 2) as u32);
// RFC 004: default spinner cap is gated on pool size, from the spin_sweep
// fork-join data. Spinning is a ~4× p50 latency win at n≤4 workers but pure
// cost at n≥8 (large pools stay hot on their own, so a spin budget only
// steals CPU from workers doing real drain). So:
// n == 1 → 0 : a lone thread has no cross-thread handoff to hide.
// 2..=4 → n : the win regime; let every worker spin its budget.
// n >= 5 → 0 : park immediately, exactly the historical behaviour —
// the non-zero DEFAULT_SPIN_BUDGET_CYCLES is inert here
// because no worker is permitted to enter the spin path.
// An explicit Config::max_spinners(_) overrides this (e.g. a future autotune
// utility that picks budget+cap from measured load).
let max_spinners = config.max_spinners.unwrap_or(match n {
0 | 1 => 0,
2..=4 => n as u32,
_ => 0,
});
Runtime {
inner: RuntimeInner::new(
n,