diff --git a/src/runtime.rs b/src/runtime.rs index 6bd8a21..5eca535 100644 --- a/src/runtime.rs +++ b/src/runtime.rs @@ -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,