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:
+26
-7
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user