use petir::mathf::RealMath;
use super::*;
use crate::polynomial::RootType;
struct Xorshift(u64);
impl Xorshift {
fn new(seed: u64) -> Self {
Self(seed | 1)
}
fn next_u64(&mut self) -> u64 {
let mut x = self.0;
x ^= x >> 12;
x ^= x << 25;
x ^= x >> 27;
self.0 = x;
x.wrapping_mul(0x2545_f491_4f6c_dd1d)
}
fn next_unit(&mut self) -> f64 {
(self.next_u64() >> 11) as f64 / (1_u64 << 53) as f64
}
fn next_in(&mut self, lo: f64, hi: f64) -> f64 {
lo + (hi - lo) * self.next_unit()
}
}
fn real_roots_of(r: &Roots<3>) -> Vec<f64> {
let mut v: Vec<f64> = (0..3)
.filter(|&k| r.root_type(k) == RootType::Real)
.map(|k| r.get(k))
.collect();
v.sort_by(f64::total_cmp);
v
}
fn sample_cubics(n: usize, seed: u64) -> (Vec<CubicEqn>, Vec<[f64; 3]>) {
let mut rng = Xorshift::new(seed);
let mut eqns = Vec::with_capacity(n);
let mut roots = Vec::with_capacity(n);
for _ in 0..n {
let mut r = [
rng.next_in(-8.0, 8.0),
rng.next_in(-8.0, 8.0),
rng.next_in(-8.0, 8.0),
];
r.sort_by(f64::total_cmp);
let b = -(r[0] + r[1] + r[2]);
let c = r[0] * r[1] + r[0] * r[2] + r[1] * r[2];
let d = -(r[0] * r[1] * r[2]);
eqns.push(CubicEqn::new(1.0, b, c, d));
roots.push(r);
}
(eqns, roots)
}
fn sample_quadratics(n: usize, seed: u64) -> Vec<QuadraticEqn> {
let mut rng = Xorshift::new(seed);
(0..n)
.map(|_| {
let r0 = rng.next_in(-8.0, 8.0);
let r1 = rng.next_in(-8.0, 8.0);
QuadraticEqn::new(1.0, -(r0 + r1), r0 * r1)
})
.collect()
}
#[test]
fn bracketed_matches_analytic_cubic_roots() {
let cubic = |x: f64| (x - 1.0) * (x - 2.0) * (x - 3.0);
let problems = [
RootProblem::new(0.5, 1.5),
RootProblem::new(1.5, 2.5),
RootProblem::new(2.5, 3.5),
];
let analytic = [1.0, 2.0, 3.0];
for method in [RootMethod::Bisection, RootMethod::Brent] {
let batch = solve_bracketed_batch(
&problems,
method,
RootSettings::default(),
ComputeBackend::CpuMulti,
|_, x| cubic(x),
);
let roots = batch.roots().unwrap_or_else(|e| panic!("{method:?}: {e}"));
let mut worst = 0.0_f64;
for (got, want) in roots.iter().zip(analytic) {
worst = worst.max((got - want).abs());
}
println!("{method:?}: worst |x - x_analytic| = {worst:.6e}");
assert!(
worst <= 1e-12,
"{method:?} worst error {worst:.6e} exceeds 1e-12"
);
}
}
#[test]
fn brent_matches_analytic_sqrt() {
let problems: Vec<RootProblem> = (0..64).map(|_| RootProblem::new(0.0, 65.0)).collect();
let batch = solve_bracketed_batch(
&problems,
RootMethod::Brent,
RootSettings::default(),
ComputeBackend::CpuMulti,
|i, x| x * x - (i as f64 + 1.0),
);
let roots = batch.roots().expect("all 64 lanes converge");
let mut worst = 0.0_f64;
for (i, got) in roots.iter().enumerate() {
worst = worst.max((got - (i as f64 + 1.0).sqrt()).abs());
}
println!("brent sqrt: worst |x - sqrt(k)| = {worst:.6e}");
assert!(worst <= 1e-12, "worst error {worst:.6e}");
}
#[test]
fn newton_matches_analytic_roots() {
let problems: Vec<RootProblem> = (0..64).map(|_| RootProblem::new(0.0, 65.0)).collect();
let batch = solve_newton_batch(
&problems,
RootSettings::default(),
ComputeBackend::CpuMulti,
|i, x| (x * x - (i as f64 + 1.0), 2.0 * x),
);
let roots = batch.roots().expect("all 64 sqrt lanes converge");
let mut worst_sqrt = 0.0_f64;
for (i, got) in roots.iter().enumerate() {
worst_sqrt = worst_sqrt.max((got - (i as f64 + 1.0).sqrt()).abs());
}
let cubic_problems = [
RootProblem::new(0.5, 1.5),
RootProblem::new(1.5, 2.5),
RootProblem::new(2.5, 3.5),
];
let cubic_batch = solve_newton_batch(
&cubic_problems,
RootSettings::default(),
ComputeBackend::CpuMulti,
|_, x| {
let v = (x - 1.0) * (x - 2.0) * (x - 3.0);
let d = 3.0 * x * x - 12.0 * x + 11.0;
(v, d)
},
);
let cubic_roots = cubic_batch.roots().expect("all 3 cubic lanes converge");
let mut worst_cubic = 0.0_f64;
for (got, want) in cubic_roots.iter().zip([1.0, 2.0, 3.0]) {
worst_cubic = worst_cubic.max((got - want).abs());
}
println!(
"newton: worst |x - sqrt(k)| = {worst_sqrt:.6e}, worst cubic error = {worst_cubic:.6e}"
);
assert!(worst_sqrt <= 1e-12, "sqrt worst {worst_sqrt:.6e}");
assert!(worst_cubic <= 1e-12, "cubic worst {worst_cubic:.6e}");
}
#[test]
fn newton_beats_brent_on_iterations() {
let problems: Vec<RootProblem> = (0..64).map(|_| RootProblem::new(0.0, 65.0)).collect();
let newton = solve_newton_batch(
&problems,
RootSettings::default(),
ComputeBackend::Serial,
|i, x| (x * x - (i as f64 + 1.0), 2.0 * x),
);
let brent = solve_bracketed_batch(
&problems,
RootMethod::Brent,
RootSettings::default(),
ComputeBackend::Serial,
|i, x| x * x - (i as f64 + 1.0),
);
let mean = |b: &RootBatch| -> f64 {
b.solutions()
.iter()
.map(|s| s.iterations() as f64)
.sum::<f64>()
/ b.len() as f64
};
let (mn, mb) = (mean(&newton), mean(&brent));
println!("mean iterations: newton {mn:.2}, brent {mb:.2}");
assert!(newton.all_converged() && brent.all_converged());
assert!(mn < mb, "newton {mn:.2} should beat brent {mb:.2}");
}
#[test]
fn cubic_batch_matches_analytic_construction() {
let (eqns, wanted) = sample_cubics(4096, 0x5eed_1234);
let batch = cubic_roots_batch(&eqns, ComputeBackend::CpuMulti);
let scalar: Vec<Roots<3>> = eqns.iter().map(CubicEqn::roots).collect();
let mut worst_residual = 0.0_f64;
let mut worst_distance = 0.0_f64;
let mut real_count = 0_usize;
for (lane, (r, eq)) in batch.iter().zip(eqns.iter()).enumerate() {
for k in 0..3 {
assert_eq!(
r.get(k).to_bits(),
scalar[lane].get(k).to_bits(),
"lane {lane} slot {k} differs from the scalar solver"
);
assert_eq!(r.root_type(k), scalar[lane].root_type(k));
}
for x in real_roots_of(r) {
real_count += 1;
worst_residual = worst_residual.max(eq.value(x).abs());
let d = wanted[lane]
.iter()
.map(|w| (x - w).abs())
.fold(f64::INFINITY, f64::min);
worst_distance = worst_distance.max(d);
}
}
println!(
"cubic batch: {real_count} real roots, worst residual {worst_residual:.6e}, \
worst distance to constructed root {worst_distance:.6e}"
);
assert!(worst_residual <= 1e-6, "residual {worst_residual:.6e}");
assert!(worst_distance <= 1e-6, "distance {worst_distance:.6e}");
}
#[test]
fn quadratic_batch_residuals_are_analytic() {
let eqns = sample_quadratics(4096, 0x1357_9bdf);
let batch = quadratic_roots_batch(&eqns, ComputeBackend::CpuMulti);
let scalar: Vec<Roots<2>> = eqns.iter().map(QuadraticEqn::roots).collect();
let mut worst = 0.0_f64;
let mut count = 0_usize;
for (lane, (r, eq)) in batch.iter().zip(eqns.iter()).enumerate() {
for k in 0..2 {
assert_eq!(r.get(k).to_bits(), scalar[lane].get(k).to_bits());
assert_eq!(r.root_type(k), scalar[lane].root_type(k));
if r.root_type(k) == RootType::Real {
count += 1;
worst = worst.max(eq.value(r.get(k)).abs());
}
}
}
println!("quadratic batch: {count} real roots, worst residual {worst:.6e}");
assert!(worst <= 1e-9, "residual {worst:.6e}");
}
#[test]
fn linear_batch_matches_scalar_including_degenerate() {
let eqns = [
LinearEqn::new(2.0, -4.0),
LinearEqn::new(3.0, 9.0),
LinearEqn::new(0.0, 1.0),
];
let batch = linear_roots_batch(&eqns, ComputeBackend::CpuMulti);
for (lane, eq) in eqns.iter().enumerate() {
let s = eq.roots();
assert_eq!(batch[lane].get(0).to_bits(), s.get(0).to_bits());
assert_eq!(batch[lane].root_type(0), s.root_type(0));
}
assert_eq!(batch[0].get(0), 2.0);
assert_eq!(batch[1].get(0), -3.0);
assert_eq!(batch[2].root_type(0), RootType::Nan);
}
fn imbalanced_problems(n: usize) -> (Vec<RootProblem>, Vec<f64>) {
let mut rng = Xorshift::new(0xabcd_0f0f);
let mut problems = Vec::with_capacity(n);
let mut targets = Vec::with_capacity(n);
for i in 0..n {
let k = rng.next_in(1.0, 3.0);
let root = k.sqrt();
if i % 2 == 0 {
problems.push(RootProblem::new(root - 1e-6, root + 1e-6));
} else {
problems.push(RootProblem::new(0.0, 1.0e6));
}
targets.push(k);
}
(problems, targets)
}
fn assert_bitwise_equal(a: &RootBatch, b: &RootBatch, what: &str) {
assert_eq!(a.len(), b.len(), "{what}: length");
for (i, (x, y)) in a.solutions().iter().zip(b.solutions()).enumerate() {
assert_eq!(x.status(), y.status(), "{what}: lane {i} status");
assert_eq!(
x.last_iterate().to_bits(),
y.last_iterate().to_bits(),
"{what}: lane {i} iterate {} vs {}",
x.last_iterate(),
y.last_iterate()
);
assert_eq!(
x.residual().to_bits(),
y.residual().to_bits(),
"{what}: lane {i} residual"
);
assert_eq!(
x.bracket_width().to_bits(),
y.bracket_width().to_bits(),
"{what}: lane {i} bracket width"
);
assert_eq!(
x.iterations(),
y.iterations(),
"{what}: lane {i} iterations"
);
}
}
#[test]
fn bitwise_identical_across_backends() {
let (problems, targets) = imbalanced_problems(2048);
let settings = RootSettings::default();
for method in [RootMethod::Bisection, RootMethod::Brent] {
let serial = solve_bracketed_batch_min(
&problems,
method,
settings,
ComputeBackend::Serial,
0,
|i, x| x * x - targets[i],
);
let multi = solve_bracketed_batch_min(
&problems,
method,
settings,
ComputeBackend::CpuMulti,
0,
|i, x| x * x - targets[i],
);
assert_bitwise_equal(&serial, &multi, &format!("{method:?}"));
}
let serial = solve_newton_batch_min(&problems, settings, ComputeBackend::Serial, 0, |i, x| {
(x * x - targets[i], 2.0 * x)
});
let multi = solve_newton_batch_min(&problems, settings, ComputeBackend::CpuMulti, 0, |i, x| {
(x * x - targets[i], 2.0 * x)
});
assert_bitwise_equal(&serial, &multi, "newton");
}
#[cfg(feature = "parallel")]
#[test]
fn bitwise_identical_across_thread_counts() {
let (problems, targets) = imbalanced_problems(2048);
let settings = RootSettings::default();
let serial = solve_bracketed_batch_min(
&problems,
RootMethod::Brent,
settings,
ComputeBackend::Serial,
0,
|i, x| x * x - targets[i],
);
let serial_newton =
solve_newton_batch_min(&problems, settings, ComputeBackend::Serial, 0, |i, x| {
(x * x - targets[i], 2.0 * x)
});
for threads in [1_usize, 2, 4, 8] {
let pool = rayon::ThreadPoolBuilder::new()
.num_threads(threads)
.build()
.expect("thread pool");
pool.install(|| {
let multi = solve_bracketed_batch_min(
&problems,
RootMethod::Brent,
settings,
ComputeBackend::CpuMulti,
0,
|i, x| x * x - targets[i],
);
assert_bitwise_equal(&serial, &multi, &format!("brent @ {threads} threads"));
let multi_newton =
solve_newton_batch_min(&problems, settings, ComputeBackend::CpuMulti, 0, |i, x| {
(x * x - targets[i], 2.0 * x)
});
assert_bitwise_equal(
&serial_newton,
&multi_newton,
&format!("newton @ {threads} threads"),
);
});
}
}
#[test]
fn poly_batch_bitwise_identical_across_backends() {
let (eqns, _) = sample_cubics(4096, 0x9999_1111);
let serial = cubic_roots_batch_min(&eqns, ComputeBackend::Serial, 0);
let check = |multi: &[Roots<3>], what: &str| {
for (lane, (a, b)) in serial.iter().zip(multi.iter()).enumerate() {
for k in 0..3 {
assert_eq!(
a.get(k).to_bits(),
b.get(k).to_bits(),
"{what}: lane {lane} slot {k}"
);
assert_eq!(
a.root_type(k),
b.root_type(k),
"{what}: lane {lane} tag {k}"
);
}
}
};
check(
&cubic_roots_batch_min(&eqns, ComputeBackend::CpuMulti, 0),
"ambient pool",
);
#[cfg(feature = "parallel")]
for threads in [1_usize, 2, 4, 8] {
let pool = rayon::ThreadPoolBuilder::new()
.num_threads(threads)
.build()
.expect("thread pool");
pool.install(|| {
check(
&cubic_roots_batch_min(&eqns, ComputeBackend::CpuMulti, 0),
&format!("{threads} threads"),
);
});
}
}
#[test]
fn lane_order_is_preserved() {
let n = 512;
let problems: Vec<RootProblem> = (0..n).map(|_| RootProblem::new(0.0, 1024.0)).collect();
let batch = solve_bracketed_batch_min(
&problems,
RootMethod::Brent,
RootSettings::default(),
ComputeBackend::CpuMulti,
0,
|i, x| x * x - (i as f64 + 1.0),
);
let roots = batch.roots().expect("all lanes converge");
for (i, r) in roots.iter().enumerate() {
assert!(
(r - (i as f64 + 1.0).sqrt()).abs() < 1e-9,
"lane {i} got {r}"
);
}
}
#[test]
fn not_bracketed_is_reported_and_not_clamped() {
let problems = [RootProblem::new(3.0, 4.0)];
let batch = solve_bracketed_batch(
&problems,
RootMethod::Brent,
RootSettings::default(),
ComputeBackend::Serial,
|_, x| x * x - 2.0,
);
let s = batch.solutions()[0];
assert_eq!(s.status(), RootStatus::NotBracketed);
assert!(s.root().is_none());
assert!(
s.last_iterate().is_nan(),
"a non-bracketed lane must not report an endpoint as a root; got {}",
s.last_iterate()
);
assert_ne!(s.last_iterate().to_bits(), 3.0_f64.to_bits());
assert_ne!(s.last_iterate().to_bits(), 4.0_f64.to_bits());
}
#[test]
fn max_iterations_is_reported() {
let settings = RootSettings {
max_iterations: 3,
..RootSettings::default()
};
let problems = [RootProblem::new(0.0, 1.0e12)];
let batch = solve_bracketed_batch(
&problems,
RootMethod::Bisection,
settings,
ComputeBackend::Serial,
|_, x| x - 1.0,
);
let s = batch.solutions()[0];
assert_eq!(s.status(), RootStatus::MaxIterations);
assert_eq!(s.iterations(), 3);
assert!(s.root().is_none());
assert!(s.last_iterate().is_finite(), "diagnostic iterate is kept");
}
#[test]
fn invalid_bracket_is_reported() {
let problems = [
RootProblem::new(f64::NAN, 1.0),
RootProblem::new(0.0, f64::INFINITY),
];
let batch = solve_bracketed_batch(
&problems,
RootMethod::Brent,
RootSettings::default(),
ComputeBackend::Serial,
|_, _| panic!("must not be evaluated on an invalid bracket"),
);
for s in batch.solutions() {
assert_eq!(s.status(), RootStatus::InvalidBracket);
assert!(s.last_iterate().is_nan());
}
}
#[test]
fn not_finite_residual_is_reported() {
let problems = [RootProblem::new(-1.0, 1.0)];
let batch = solve_bracketed_batch(
&problems,
RootMethod::Brent,
RootSettings::default(),
ComputeBackend::Serial,
|_, x| if x < -0.5 { f64::NAN } else { x },
);
assert_eq!(batch.solutions()[0].status(), RootStatus::NotFinite);
assert!(batch.solutions()[0].root().is_none());
}
#[test]
fn partial_failure_in_a_large_batch_is_reported() {
let n = 10_000;
let mut problems: Vec<RootProblem> = (0..n).map(|_| RootProblem::new(0.0, 4.0)).collect();
for &bad in &[17_usize, 4_242, 9_999] {
problems[bad] = RootProblem::new(3.0, 4.0);
}
let batch = solve_bracketed_batch(
&problems,
RootMethod::Brent,
RootSettings::default(),
ComputeBackend::CpuMulti,
|_, x| x * x - 2.0,
);
assert!(!batch.all_converged());
assert_eq!(batch.failure_count(), 3);
assert_eq!(
batch.failures().iter().map(|(i, _)| *i).collect::<Vec<_>>(),
vec![17, 4_242, 9_999]
);
let err = batch.roots().expect_err("three lanes failed");
assert_eq!(err.total, n);
assert_eq!(err.failure_count, 3);
assert_eq!(err.first_index, 17);
assert_eq!(err.first_status, RootStatus::NotBracketed);
println!("failure report: {err}");
assert_eq!(
batch
.solutions()
.iter()
.filter(|s| s.root().is_some())
.count(),
n - 3
);
}
#[test]
fn root_on_endpoint_converges_immediately() {
let problems = [RootProblem::new(2.0, 5.0), RootProblem::new(-5.0, 2.0)];
let batch = solve_bracketed_batch(
&problems,
RootMethod::Brent,
RootSettings::default(),
ComputeBackend::Serial,
|_, x| x - 2.0,
);
for s in batch.solutions() {
assert_eq!(s.status(), RootStatus::Converged);
assert_eq!(s.root(), Some(2.0));
assert_eq!(s.iterations(), 0);
}
}
#[test]
fn empty_batch_is_vacuously_converged() {
let batch = solve_bracketed_batch(
&[],
RootMethod::Brent,
RootSettings::default(),
ComputeBackend::CpuMulti,
|_, _: f64| panic!("must not be called"),
);
assert!(batch.is_empty());
assert!(batch.all_converged());
assert_eq!(batch.roots().expect("vacuously ok"), Vec::<f64>::new());
assert!(cubic_roots_batch(&[], ComputeBackend::CpuMulti).is_empty());
}
#[test]
fn newton_survives_zero_derivative() {
let problems = [RootProblem::with_guess(-1.0, 2.0, 0.0)];
let batch = solve_newton_batch(
&problems,
RootSettings {
x_tol_abs: 1e-8,
..RootSettings::default()
},
ComputeBackend::Serial,
|_, x| (x * x * x, 3.0 * x * x),
);
let s = batch.solutions()[0];
assert_eq!(s.status(), RootStatus::Converged, "{s:?}");
assert!(s.root().expect("converged").abs() < 1e-6, "{s:?}");
}
#[test]
fn newton_ignores_an_out_of_bracket_guess() {
let settings = RootSettings::default();
let f = |_: usize, x: f64| (x * x - 2.0, 2.0 * x);
let good = solve_newton_batch(
&[RootProblem::new(0.0, 4.0)],
settings,
ComputeBackend::Serial,
f,
);
for guess in [-1.0e9, 1.0e9, f64::NAN, f64::INFINITY] {
let batch = solve_newton_batch(
&[RootProblem::with_guess(0.0, 4.0, guess)],
settings,
ComputeBackend::Serial,
f,
);
assert_bitwise_equal(&good, &batch, &format!("guess {guess}"));
}
}
#[test]
fn both_methods_agree_on_a_kinked_residual() {
let f = |_: usize, x: f64| (x - 1.25_f64).abs() - 0.5;
let problems = [RootProblem::new(1.3, 3.0)];
let settings = RootSettings::default();
let a = solve_bracketed_batch(
&problems,
RootMethod::Bisection,
settings,
ComputeBackend::Serial,
f,
);
let b = solve_bracketed_batch(
&problems,
RootMethod::Brent,
settings,
ComputeBackend::Serial,
f,
);
let (ra, rb) = (
a.roots().expect("bisection converges")[0],
b.roots().expect("brent converges")[0],
);
assert!((ra - 1.75).abs() < 1e-9, "bisection got {ra}");
assert!((rb - 1.75).abs() < 1e-9, "brent got {rb}");
}
#[test]
fn dispatch_respects_the_size_floors() {
assert_eq!(
root_batch_backend_for(ComputeBackend::CpuMulti, ROOT_BATCH_MIN_PROBLEMS - 1),
ComputeBackend::Serial
);
assert_eq!(
root_batch_backend_for(ComputeBackend::CpuMulti, ROOT_BATCH_MIN_PROBLEMS),
if cfg!(feature = "parallel") {
ComputeBackend::CpuMulti
} else {
ComputeBackend::Serial
}
);
assert_eq!(
poly_roots_backend_for(ComputeBackend::CpuMulti, POLY_ROOTS_MIN_EQUATIONS - 1),
ComputeBackend::Serial
);
assert_ne!(
root_batch_backend_for(ComputeBackend::Gpu, 1 << 20),
ComputeBackend::Gpu
);
assert!(root_batch_backend_for(ComputeBackend::Gpu, 1 << 20).is_available());
assert_eq!(
root_batch_backend_for(ComputeBackend::Serial, 1 << 20),
ComputeBackend::Serial
);
}
#[test]
fn public_entry_points_agree_across_requested_backends() {
let n = ROOT_BATCH_MIN_PROBLEMS * 2;
let problems: Vec<RootProblem> = (0..n).map(|_| RootProblem::new(0.0, 1.0e4)).collect();
let f = |i: usize, x: f64| x * x - (i as f64 + 1.0);
let settings = RootSettings::default();
let serial = solve_bracketed_batch(
&problems,
RootMethod::Brent,
settings,
ComputeBackend::Serial,
f,
);
for backend in [ComputeBackend::CpuMulti, ComputeBackend::Gpu] {
let other = solve_bracketed_batch(&problems, RootMethod::Brent, settings, backend, f);
assert_bitwise_equal(&serial, &other, &format!("{backend:?}"));
}
}
#[test]
#[ignore = "measurement, not a correctness check; ~0.9 s wall clock. Run with --ignored --nocapture"]
fn root_batch_crossover_benchmark() {
use std::time::Instant;
let cores = std::thread::available_parallelism()
.map(|n| n.get())
.unwrap_or(1);
println!("available_parallelism() = {cores}");
println!("parallel feature enabled = {}", cfg!(feature = "parallel"));
println!("ROOT_BATCH_MIN_PROBLEMS = {ROOT_BATCH_MIN_PROBLEMS}");
println!(
"{:>10} {:>14} {:>14} {:>9} {:>15} {:>15} {:>9}",
"problems",
"cheap ser[us]",
"cheap mul[us]",
"speedup",
"costly ser[us]",
"costly mul[us]",
"speedup"
);
let settings = RootSettings::default();
for n in [16_usize, 32, 64, 128, 256, 512, 1024, 4096, 16_384, 65_536] {
let mut rng = Xorshift::new(0x1234_5678);
let targets: Vec<f64> = (0..n).map(|_| rng.next_in(1.0, 3.0)).collect();
let problems: Vec<RootProblem> = (0..n).map(|_| RootProblem::new(0.0, 4.0)).collect();
let cheap = |i: usize, x: f64| x * x - targets[i];
let costly = |i: usize, x: f64| {
let s = (1.0 + x * x).r_ln().r_exp().sqrt();
s * s - 1.0 - targets[i]
};
let time = |backend: ComputeBackend, costly_mode: bool| -> f64 {
let run = || {
if costly_mode {
solve_bracketed_batch_min(
&problems,
RootMethod::Brent,
settings,
backend,
0,
costly,
)
} else {
solve_bracketed_batch_min(
&problems,
RootMethod::Brent,
settings,
backend,
0,
cheap,
)
}
};
std::hint::black_box(run());
let mut best = f64::INFINITY;
for _ in 0..7 {
let t = Instant::now();
let out = run();
let dt = t.elapsed().as_secs_f64() * 1.0e6;
std::hint::black_box(&out);
best = best.min(dt);
}
best
};
let cs = time(ComputeBackend::Serial, false);
let cm = time(ComputeBackend::CpuMulti, false);
let xs = time(ComputeBackend::Serial, true);
let xm = time(ComputeBackend::CpuMulti, true);
println!(
"{n:>10} {cs:>14.2} {cm:>14.2} {:>9.2} {xs:>15.2} {xm:>15.2} {:>9.2}",
cs / cm,
xs / xm
);
}
}
#[test]
#[ignore = "measurement, not a correctness check; ~0.7 s wall clock. Run with --ignored --nocapture"]
fn poly_batch_crossover_benchmark() {
use std::time::Instant;
let cores = std::thread::available_parallelism()
.map(|n| n.get())
.unwrap_or(1);
println!("available_parallelism() = {cores}");
println!("parallel feature enabled = {}", cfg!(feature = "parallel"));
println!("POLY_ROOTS_MIN_EQUATIONS = {POLY_ROOTS_MIN_EQUATIONS}, POLY_BLOCK = {POLY_BLOCK}");
println!(
"{:>10} {:>14} {:>14} {:>9}",
"cubics", "serial [us]", "cpumulti [us]", "speedup"
);
for n in [256_usize, 512, 1024, 2048, 4096, 16_384, 65_536, 262_144] {
let (eqns, _) = sample_cubics(n, 0x7777_3333);
let time = |backend: ComputeBackend| -> f64 {
std::hint::black_box(cubic_roots_batch_min(&eqns, backend, 0));
let mut best = f64::INFINITY;
for _ in 0..7 {
let t = Instant::now();
let out = cubic_roots_batch_min(&eqns, backend, 0);
let dt = t.elapsed().as_secs_f64() * 1.0e6;
std::hint::black_box(&out);
best = best.min(dt);
}
best
};
let s = time(ComputeBackend::Serial);
let m = time(ComputeBackend::CpuMulti);
println!("{n:>10} {s:>14.2} {m:>14.2} {:>9.2}", s / m);
}
}
#[cfg(feature = "parallel")]
#[test]
#[ignore = "measurement, not a correctness check; ~0.5 s wall clock. Run with --ignored --nocapture"]
fn root_batch_thread_scaling_benchmark() {
use std::time::Instant;
let n = 65_536_usize;
let (problems, targets) = imbalanced_problems(n);
let settings = RootSettings::default();
let f = |i: usize, x: f64| x * x - targets[i];
let reference = solve_bracketed_batch_min(
&problems,
RootMethod::Brent,
settings,
ComputeBackend::Serial,
0,
f,
);
let t = Instant::now();
std::hint::black_box(solve_bracketed_batch_min(
&problems,
RootMethod::Brent,
settings,
ComputeBackend::Serial,
0,
f,
));
let mut serial_us = t.elapsed().as_secs_f64() * 1.0e6;
for _ in 0..6 {
let t = Instant::now();
let out = solve_bracketed_batch_min(
&problems,
RootMethod::Brent,
settings,
ComputeBackend::Serial,
0,
f,
);
std::hint::black_box(&out);
serial_us = serial_us.min(t.elapsed().as_secs_f64() * 1.0e6);
}
println!(
"available_parallelism() = {}",
std::thread::available_parallelism()
.map(|c| c.get())
.unwrap_or(1)
);
println!("batch = {n} imbalanced lanes, Brent, best of 7");
println!(
"{:>8} {:>14} {:>9} {:>10}",
"threads", "time [us]", "speedup", "bitwise"
);
println!(
"{:>8} {serial_us:>14.2} {:>9.2} {:>10}",
0, 1.0, "reference"
);
for threads in [1_usize, 2, 4, 8] {
let pool = rayon::ThreadPoolBuilder::new()
.num_threads(threads)
.build()
.expect("thread pool");
let (best, identical) = pool.install(|| {
let first = solve_bracketed_batch_min(
&problems,
RootMethod::Brent,
settings,
ComputeBackend::CpuMulti,
0,
f,
);
let identical = first == reference;
let mut best = f64::INFINITY;
for _ in 0..7 {
let t = Instant::now();
let out = solve_bracketed_batch_min(
&problems,
RootMethod::Brent,
settings,
ComputeBackend::CpuMulti,
0,
f,
);
std::hint::black_box(&out);
best = best.min(t.elapsed().as_secs_f64() * 1.0e6);
}
(best, identical)
});
println!(
"{threads:>8} {best:>14.2} {:>9.2} {:>10}",
serial_us / best,
if identical { "identical" } else { "DIFFERENT" }
);
assert!(identical, "thread count {threads} changed the result");
}
}