diff --git a/tests/preempt.rs b/tests/preempt.rs index 3a04288..1f3e43c 100644 --- a/tests/preempt.rs +++ b/tests/preempt.rs @@ -64,3 +64,72 @@ fn check_is_a_noop_when_timeslice_not_expired() { }); assert_eq!(count.load(Ordering::Relaxed), 1_000); } + +// --------------------------------------------------------------------------- +// XMM-not-saved assumption (context.rs): the context switch saves no SSE +// state. The justification is that every yield crosses a Rust `call` boundary +// (SysV AMD64: XMM0-15 caller-saved), so the compiler spills live XMM before +// the switch and reloads after. The non-obvious path is PREEMPTION: `check!()` +// inlines `maybe_preempt`, so the yield can fire mid-floating-point-loop, not +// at a syntactic call site. This is adversarial because the yield is buried +// inside the actor's own hot loop — but `switch_to_scheduler` is still an +// `extern "C"` call, so the spill still happens. Verified by disasm: the FP +// accumulators are spilled to (%rsp) immediately before the preempt path and +// reloaded after. This test guards against a future change to the yield path +// that bypasses that call boundary (which WOULD require saving XMM). +#[test] +fn live_xmm_survives_preemptive_switch() { + // A floating-point loop with several live f64 accumulators the compiler + // wants resident in XMM across iterations. We force a timeslice expiry on + // a chosen iteration so a preemptive switch_to_scheduler fires while XMM + // is live, then compare against the same workload run with no preemption. + #[inline(never)] + fn fp_workload(preempt_at: u64) -> f64 { + let mut a = 1.0000001_f64; + let mut b = 0.9999999_f64; + let mut acc = 0.0_f64; + for i in 0..50_000u64 { + a = a * 1.0000003 + 0.0000001; + b = b * 0.9999997 + 0.0000002; + acc += a - b + (i as f64) * 1e-9; + if i == preempt_at { + smarm::preempt::expire_timeslice_for_test(); + } + smarm::check!(); + } + acc + } + + // Reference: a preempt_at past the loop end => check!() never yields. + let reference = fp_workload(u64::MAX); + + let got = Arc::new(AtomicU64::new(0)); + let g = got.clone(); + run(move || { + // A second actor so the scheduler has somewhere to switch on yield. + let _other = spawn(|| { + for _ in 0..8 { + smarm::preempt::expire_timeslice_for_test(); + smarm::check!(); + } + }); + let h = spawn(move || { + // Yield from several distinct points so the switch lands on + // different live-XMM states. + let mut total = 0.0_f64; + for k in 0..8 { + total += fp_workload(k * 6_000 + 100); + } + g.store(total.to_bits(), Ordering::SeqCst); + }); + h.join().unwrap(); + }); + + let reference_total: f64 = (0..8).map(|_| reference).sum(); + let got = f64::from_bits(got.load(Ordering::SeqCst)); + assert_eq!( + got.to_bits(), + reference_total.to_bits(), + "XMM state diverged across preemptive switch: got {got:.12}, want {reference_total:.12}" + ); +}