use petir::mathf::RealMath;
use super::*;
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 sample_vertices(n: usize, seed: u64) -> Vec<f64> {
let mut rng = Xorshift::new(seed);
(0..n).map(|_| rng.next_in(-3.0, 3.0)).collect()
}
#[test]
fn golden_section_matches_analytic_minima() {
let vertices = sample_vertices(64, 0x51ce_9a11);
let problems: Vec<MinProblem> = (0..64).map(|_| MinProblem::new(-5.0, 5.0)).collect();
let batch = golden_section_batch(
&problems,
Sense::Minimise,
MinSettings::default(),
ComputeBackend::Serial,
|i, x| 1.0 + (x - vertices[i]) * (x - vertices[i]),
);
let located = batch.extrema().expect("every lane converges");
let values = batch.extremal_values().expect("every lane converges");
let mut worst_x = 0.0_f64;
let mut worst_f = 0.0_f64;
for i in 0..64 {
worst_x = worst_x.max((located[i] - vertices[i]).abs());
worst_f = worst_f.max((values[i] - 1.0).abs());
}
let iterations: Vec<u32> = batch.solutions().iter().map(|s| s.iterations()).collect();
let min_it = iterations.iter().copied().min().unwrap();
let max_it = iterations.iter().copied().max().unwrap();
println!("quadratic: worst |x - x0| = {worst_x:.6e}");
println!("quadratic: worst |f(x*) - 1| = {worst_f:.6e}");
println!("quadratic: iterations = {min_it}..={max_it}");
println!("quadratic: sqrt(eps) = {SQRT_EPSILON:.6e}");
assert!(
worst_x <= 1e-6,
"worst abscissa error {worst_x:.6e} exceeds 1e-6"
);
}
#[test]
fn flat_minimum_exposes_the_sqrt_eps_limit() {
let vertices = sample_vertices(64, 0x51ce_9a11);
let problems: Vec<MinProblem> = (0..64).map(|_| MinProblem::new(-5.0, 5.0)).collect();
let settings = MinSettings {
x_tol_abs: 1e-15,
x_tol_rel: 0.0,
max_iterations: 300,
};
let run = |f: &(dyn Fn(usize, f64) -> f64 + Sync)| {
let batch = golden_section_batch(
&problems,
Sense::Minimise,
settings,
ComputeBackend::Serial,
|i, x| f(i, x),
);
assert!(batch.all_converged());
let located = batch.extrema().expect("every lane converges");
let worst = (0..64).fold(0.0_f64, |acc, i| acc.max((located[i] - vertices[i]).abs()));
let iterations = batch
.solutions()
.iter()
.map(|s| s.iterations())
.max()
.unwrap();
(worst, iterations)
};
let (worst_quadratic, it_quadratic) = run(&|i, x| {
let d = x - vertices[i];
1.0 + d * d
});
let (worst_quartic, it_quartic) = run(&|i, x| {
let d = x - vertices[i];
1.0 + d * d * d * d
});
let (worst_zero_baseline, it_zero) = run(&|i, x| {
let d = x - vertices[i];
d * d
});
println!("1 + (x-x0)^2 : worst |x - x0| = {worst_quadratic:.6e} (max it {it_quadratic})");
println!("1 + (x-x0)^4 : worst |x - x0| = {worst_quartic:.6e} (max it {it_quartic})");
println!(" (x-x0)^2 : worst |x - x0| = {worst_zero_baseline:.6e} (max it {it_zero})");
println!(
"quartic / quadratic degradation = {:.0}x",
worst_quartic / worst_quadratic
);
println!(
"sqrt(eps) = {SQRT_EPSILON:.6e}, eps^(1/4) = {:.6e}",
f64::EPSILON.r_powf(0.25)
);
assert!(
worst_quadratic <= 1e-6,
"offset quadratic worst {worst_quadratic:.6e}"
);
assert!(
worst_quartic <= 1e-2,
"offset quartic worst {worst_quartic:.6e}"
);
assert!(
worst_quartic / worst_quadratic >= 100.0,
"the flat minimum must be orders of magnitude worse, got {:.1}x",
worst_quartic / worst_quadratic
);
assert!(
worst_zero_baseline < worst_quadratic,
"the zero-baseline control must beat the offset quadratic, \
since it has no order-unity value to lose precision against"
);
}
#[test]
fn golden_section_matches_analytic_transcendental_minimum() {
let x_star = std::f64::consts::E.recip();
let f_star = -x_star;
let problems = [MinProblem::new(0.05, 3.0)];
let batch = golden_section_batch(
&problems,
Sense::Minimise,
MinSettings::default(),
ComputeBackend::Serial,
|_, x| x * x.r_ln(),
);
let s = batch.solutions()[0];
let dx = (s.extremum().expect("converged") - x_star).abs();
let df = (s.extremal_value().expect("converged") - f_star).abs();
println!("x ln x: |x - 1/e| = {dx:.6e}");
println!("x ln x: |f - (-1/e)| = {df:.6e}");
println!("x ln x: iterations = {}", s.iterations());
assert!(dx <= 1e-6, "abscissa error {dx:.6e}");
assert!(df <= 1e-12, "value error {df:.6e}");
}
#[test]
fn maximise_matches_analytic_sine_peak() {
let x_star = std::f64::consts::FRAC_PI_2;
let problems = [MinProblem::new(0.0, std::f64::consts::PI)];
let batch = golden_section_batch(
&problems,
Sense::Maximise,
MinSettings::default(),
ComputeBackend::Serial,
|_, x| x.r_sin(),
);
let s = batch.solutions()[0];
let dx = (s.extremum().expect("converged") - x_star).abs();
let df = (s.extremal_value().expect("converged") - 1.0).abs();
println!("sin max: |x - pi/2| = {dx:.6e}");
println!("sin max: |f - 1| = {df:.6e}");
println!("sin max: iterations = {}", s.iterations());
assert!(dx <= 1e-6, "abscissa error {dx:.6e}");
assert!(df <= 1e-12, "value error {df:.6e}");
assert!(
s.extremal_value().unwrap() > 0.0,
"the caller's sign must be preserved; nothing is negated internally"
);
}
#[test]
fn bracket_contracts_at_the_golden_ratio() {
let w0 = 10.0_f64;
let mut worst = 0.0_f64;
let mut worst_small_k = 0.0_f64;
for k in [1_u32, 2, 5, 10, 20, 30, 40, 50, 60, 80] {
let settings = MinSettings {
x_tol_abs: 0.0,
x_tol_rel: 0.0,
max_iterations: k,
};
let problems = [MinProblem::new(-5.0, 5.0)];
let batch = golden_section_batch(
&problems,
Sense::Minimise,
settings,
ComputeBackend::Serial,
|_, x| x * x,
);
let s = batch.solutions()[0];
assert_eq!(
s.status(),
MinStatus::MaxIterations,
"a zero tolerance cannot be met"
);
assert_eq!(s.iterations(), k);
let expected = w0 * GOLDEN_RATIO.r_powi(k as i32);
let deviation = (s.bracket_width() / expected - 1.0).abs();
worst = worst.max(deviation);
if k <= 20 {
worst_small_k = worst_small_k.max(deviation);
}
println!(
"k = {k:>2}: width = {:.17e}, W0*gr^k = {expected:.17e}, rel dev = {deviation:.6e}",
s.bracket_width()
);
}
println!("worst relative deviation, k <= 20 = {worst_small_k:.6e}");
println!("worst relative deviation, all k = {worst:.6e}");
assert!(
worst_small_k <= 1e-13,
"contraction rate deviates by {worst_small_k:.6e} for k <= 20"
);
assert!(
worst <= 1e-5,
"contraction rate deviates by {worst:.6e} over the whole range"
);
}
#[test]
fn multimodal_bracket_returns_a_local_extremum_without_warning() {
let f = |x: f64| {
let narrow = -(-100.0 * (x + 1.5) * (x + 1.5)).r_exp();
let wide = -0.5 * (-(x - 1.0) * (x - 1.0)).r_exp();
narrow + wide
};
let problems = [MinProblem::new(-2.0, 2.0)];
let batch = golden_section_batch(
&problems,
Sense::Minimise,
MinSettings::default(),
ComputeBackend::Serial,
|_, x| f(x),
);
let s = batch.solutions()[0];
println!(
"multimodal: status = {}, x = {:.9e}, f = {:.16e}, iterations = {}",
s.status().label(),
s.last_iterate(),
s.last_value(),
s.iterations()
);
println!(
"multimodal: true global minimum is near x = -1.5 with f = {:.6}",
f(-1.5)
);
assert_eq!(s.status(), MinStatus::Converged);
let x = s.extremum().expect("it converges -- that is the problem");
assert!(
(x - 1.0).abs() < 1e-6,
"expected the shallow local minimum near +1.0, got {x}"
);
assert!(
(x + 1.5).abs() > 1.0,
"the deep global minimum near -1.5 must have been missed"
);
assert!(
s.extremal_value().unwrap() > f(-1.5),
"the located minimum must be shallower than the global one"
);
}
#[test]
fn monotone_bracket_walks_to_the_endpoint() {
let problems = [MinProblem::new(0.0, 1.0)];
for (sense, want) in [(Sense::Minimise, 0.0_f64), (Sense::Maximise, 1.0)] {
let batch = golden_section_batch(
&problems,
sense,
MinSettings::default(),
ComputeBackend::Serial,
|_, x| x,
);
let s = batch.solutions()[0];
let got = s.extremum().expect("converged");
println!("monotone {}: located {got:.12e}", sense.label());
assert!(
(got - want).abs() < 1e-6,
"{}: got {got}, want {want}",
sense.label()
);
}
}
fn imbalanced_problems(n: usize) -> (Vec<MinProblem>, Vec<f64>) {
let mut rng = Xorshift::new(0xabcd_0f0f);
let mut problems = Vec::with_capacity(n);
let mut vertices = Vec::with_capacity(n);
for i in 0..n {
let x0 = rng.next_in(-3.0, 3.0);
if i % 2 == 0 {
problems.push(MinProblem::new(x0 - 1e-6, x0 + 1e-6));
} else {
problems.push(MinProblem::new(-1.0e6, 1.0e6));
}
vertices.push(x0);
}
(problems, vertices)
}
fn assert_bitwise_equal(a: &MinBatch, b: &MinBatch, 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.last_value().to_bits(),
y.last_value().to_bits(),
"{what}: lane {i} value"
);
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, vertices) = imbalanced_problems(2048);
let settings = MinSettings::default();
for sense in [Sense::Minimise, Sense::Maximise] {
let serial = golden_section_batch_min(
&problems,
sense,
settings,
ComputeBackend::Serial,
0,
|i, x| (x - vertices[i]) * (x - vertices[i]),
);
let multi = golden_section_batch_min(
&problems,
sense,
settings,
ComputeBackend::CpuMulti,
0,
|i, x| (x - vertices[i]) * (x - vertices[i]),
);
assert_bitwise_equal(&serial, &multi, sense.label());
}
}
#[cfg(feature = "parallel")]
#[test]
fn bitwise_identical_across_thread_counts() {
let (problems, vertices) = imbalanced_problems(2048);
let settings = MinSettings::default();
let serial = golden_section_batch_min(
&problems,
Sense::Minimise,
settings,
ComputeBackend::Serial,
0,
|i, x| (x - vertices[i]) * (x - vertices[i]),
);
for threads in [1_usize, 2, 4, 8] {
let pool = rayon::ThreadPoolBuilder::new()
.num_threads(threads)
.build()
.expect("thread pool");
pool.install(|| {
let multi = golden_section_batch_min(
&problems,
Sense::Minimise,
settings,
ComputeBackend::CpuMulti,
0,
|i, x| (x - vertices[i]) * (x - vertices[i]),
);
assert_bitwise_equal(&serial, &multi, &format!("minimise @ {threads} threads"));
});
}
}
#[test]
fn lane_order_is_preserved() {
let n = 512;
let problems: Vec<MinProblem> = (0..n).map(|_| MinProblem::new(-1.0, 600.0)).collect();
for backend in [ComputeBackend::Serial, ComputeBackend::CpuMulti] {
let batch = golden_section_batch_min(
&problems,
Sense::Minimise,
MinSettings::default(),
backend,
0,
|i, x| {
let d = x - i as f64;
d * d
},
);
let located = batch.extrema().expect("every lane converges");
for (i, x) in located.iter().enumerate() {
assert!(
(x - i as f64).abs() < 1e-5,
"{}: lane {i} located {x}",
backend.label()
);
}
}
}
#[test]
fn max_iterations_is_reported() {
let settings = MinSettings {
x_tol_abs: 0.0,
x_tol_rel: 0.0,
max_iterations: 5,
};
let problems = [MinProblem::new(-5.0, 5.0)];
let batch = golden_section_batch(
&problems,
Sense::Minimise,
settings,
ComputeBackend::Serial,
|_, x| x * x,
);
let s = batch.solutions()[0];
assert_eq!(s.status(), MinStatus::MaxIterations);
assert_eq!(s.iterations(), 5);
assert!(s.extremum().is_none(), "a failed lane yields no extremum");
assert!(s.extremal_value().is_none());
assert!(s.last_iterate().is_finite());
assert!(s.bracket_width() > 0.0);
assert!(batch.extrema().is_err());
}
#[test]
fn invalid_bracket_is_reported() {
let problems = [
MinProblem::new(f64::NAN, 1.0),
MinProblem::new(0.0, f64::INFINITY),
MinProblem::new(f64::NEG_INFINITY, f64::INFINITY),
];
let batch = golden_section_batch(
&problems,
Sense::Minimise,
MinSettings::default(),
ComputeBackend::Serial,
|_, x| x * x,
);
for (i, s) in batch.solutions().iter().enumerate() {
assert_eq!(s.status(), MinStatus::InvalidBracket, "lane {i}");
assert!(s.last_iterate().is_nan(), "lane {i} must report NaN");
assert!(s.extremum().is_none(), "lane {i}");
assert_eq!(s.iterations(), 0, "lane {i}");
}
let err = batch.extrema().expect_err("all three lanes fail");
assert_eq!(err.failure_count, 3);
assert_eq!(err.total, 3);
assert_eq!(err.first_index, 0);
assert_eq!(err.first_status, MinStatus::InvalidBracket);
}
#[test]
fn not_finite_objective_is_reported() {
let problems = [MinProblem::new(-1.0, 1.0)];
let batch = golden_section_batch(
&problems,
Sense::Minimise,
MinSettings::default(),
ComputeBackend::Serial,
|_, _| f64::NAN,
);
let s = batch.solutions()[0];
assert_eq!(s.status(), MinStatus::NotFinite);
assert_eq!(s.iterations(), 0);
assert!(s.last_value().is_nan());
assert!(s.extremum().is_none());
let batch = golden_section_batch(
&problems,
Sense::Minimise,
MinSettings::default(),
ComputeBackend::Serial,
|_, x| if x.abs() < 0.1 { f64::INFINITY } else { x * x },
);
let s = batch.solutions()[0];
assert_eq!(s.status(), MinStatus::NotFinite);
assert!(s.iterations() > 0, "the failure must be mid-iteration");
assert!(s.last_value().is_infinite());
assert!(s.extremum().is_none());
}
#[test]
fn partial_failure_in_a_large_batch_is_reported() {
let n = 10_000_usize;
let bad = [17_usize, 4_242, 9_999];
let problems: Vec<MinProblem> = (0..n)
.map(|i| {
if bad.contains(&i) {
MinProblem::new(f64::NAN, 1.0)
} else {
MinProblem::new(-5.0, 5.0)
}
})
.collect();
let batch = golden_section_batch(
&problems,
Sense::Minimise,
MinSettings::default(),
ComputeBackend::CpuMulti,
|_, x| x * x,
);
assert!(!batch.all_converged());
assert_eq!(batch.failure_count(), 3);
assert_eq!(batch.first_failure().map(|(i, _)| i), Some(17));
assert_eq!(
batch.failures().iter().map(|(i, _)| *i).collect::<Vec<_>>(),
bad.to_vec()
);
let err = batch.extrema().expect_err("three lanes failed");
assert_eq!(err.total, n);
assert_eq!(err.failure_count, 3);
assert_eq!(err.first_index, 17);
let text = err.to_string();
assert!(text.contains("3 of 10000"), "message was: {text}");
assert!(text.contains("lane 17"), "message was: {text}");
assert!(batch.solutions()[0].extremum().is_some());
assert!(batch.solutions()[17].extremum().is_none());
}
#[test]
fn empty_batch_is_vacuously_converged() {
use std::sync::atomic::{AtomicU32, Ordering};
let calls = AtomicU32::new(0);
let batch = golden_section_batch(
&[],
Sense::Minimise,
MinSettings::default(),
ComputeBackend::Serial,
|_, x: f64| {
calls.fetch_add(1, Ordering::Relaxed);
x
},
);
assert!(batch.is_empty());
assert_eq!(batch.len(), 0);
assert!(batch.all_converged());
assert_eq!(batch.extrema().unwrap(), Vec::<f64>::new());
assert_eq!(calls.load(Ordering::Relaxed), 0);
assert!(batch.get(0).is_none());
}
#[test]
fn reversed_bracket_is_normalised() {
let forward = golden_section_batch(
&[MinProblem::new(-5.0, 5.0)],
Sense::Minimise,
MinSettings::default(),
ComputeBackend::Serial,
|_, x| (x - 1.5) * (x - 1.5),
);
let reversed = golden_section_batch(
&[MinProblem::new(5.0, -5.0)],
Sense::Minimise,
MinSettings::default(),
ComputeBackend::Serial,
|_, x| (x - 1.5) * (x - 1.5),
);
assert_eq!(forward, reversed, "orientation must not change the answer");
assert!((forward.extrema().unwrap()[0] - 1.5).abs() < 1e-6);
}
#[test]
fn degenerate_bracket_converges_immediately() {
let batch = golden_section_batch(
&[MinProblem::new(2.0, 2.0)],
Sense::Minimise,
MinSettings::default(),
ComputeBackend::Serial,
|_, x| (x - 1.5) * (x - 1.5),
);
let s = batch.solutions()[0];
assert_eq!(s.status(), MinStatus::Converged);
assert_eq!(s.iterations(), 0);
assert_eq!(s.extremum(), Some(2.0));
assert_eq!(s.bracket_width(), 0.0);
assert_eq!(s.extremal_value(), Some(0.25));
}
#[test]
fn dispatch_respects_the_size_floor() {
assert_eq!(
minimise_backend_for(ComputeBackend::CpuMulti, MINIMISE_BATCH_MIN_PROBLEMS - 1),
ComputeBackend::Serial
);
assert_eq!(
minimise_backend_for(ComputeBackend::Serial, 1 << 20),
ComputeBackend::Serial
);
assert_ne!(
minimise_backend_for(ComputeBackend::Gpu, 1 << 20),
ComputeBackend::Gpu
);
assert!(minimise_backend_for(ComputeBackend::CpuMulti, 1 << 20).is_available());
let expected_multi = cfg!(feature = "parallel");
assert_eq!(
minimise_backend_for(ComputeBackend::CpuMulti, MINIMISE_BATCH_MIN_PROBLEMS)
== ComputeBackend::CpuMulti,
expected_multi
);
}
#[test]
fn public_entry_point_agrees_across_requested_backends() {
let vertices = sample_vertices(1024, 0x0bad_c0de);
let problems: Vec<MinProblem> = (0..1024).map(|_| MinProblem::new(-8.0, 8.0)).collect();
let run = |backend| {
golden_section_batch(
&problems,
Sense::Minimise,
MinSettings::default(),
backend,
|i, x| (x - vertices[i]) * (x - vertices[i]),
)
};
let serial = run(ComputeBackend::Serial);
assert_bitwise_equal(&serial, &run(ComputeBackend::CpuMulti), "public cpu-multi");
assert_bitwise_equal(&serial, &run(ComputeBackend::Gpu), "public gpu-request");
}
#[test]
#[ignore = "measurement, not a correctness check. Run with --ignored --nocapture"]
fn minimise_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!("MINIMISE_BATCH_MIN_PROBLEMS = {MINIMISE_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 = MinSettings::default();
for n in [16_usize, 32, 64, 128, 256, 512, 1024, 4096, 16_384, 65_536] {
let vertices = sample_vertices(n, 0x1234_5678);
let problems: Vec<MinProblem> = (0..n).map(|_| MinProblem::new(-5.0, 5.0)).collect();
let cheap = |i: usize, x: f64| (x - vertices[i]) * (x - vertices[i]);
let costly = |i: usize, x: f64| {
let d = x - vertices[i];
let s = (1.0 + d * d).r_ln().r_exp().sqrt();
s * s - 1.0
};
let time = |backend: ComputeBackend, costly_mode: bool| -> f64 {
let run = || {
if costly_mode {
golden_section_batch_min(
&problems,
Sense::Minimise,
settings,
backend,
0,
costly,
)
} else {
golden_section_batch_min(
&problems,
Sense::Minimise,
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
);
}
}
#[cfg(feature = "parallel")]
#[test]
#[ignore = "measurement, not a correctness check. Run with --ignored --nocapture"]
fn minimise_batch_thread_scaling_benchmark() {
use std::time::Instant;
let n = 65_536_usize;
let (problems, vertices) = imbalanced_problems(n);
let settings = MinSettings::default();
let f = |i: usize, x: f64| (x - vertices[i]) * (x - vertices[i]);
let reference = golden_section_batch_min(
&problems,
Sense::Minimise,
settings,
ComputeBackend::Serial,
0,
f,
);
let mut serial_us = f64::INFINITY;
for _ in 0..7 {
let t = Instant::now();
let out = golden_section_batch_min(
&problems,
Sense::Minimise,
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, golden section, 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 = golden_section_batch_min(
&problems,
Sense::Minimise,
settings,
ComputeBackend::CpuMulti,
0,
f,
);
let identical = first == reference;
let mut best = f64::INFINITY;
for _ in 0..7 {
let t = Instant::now();
let out = golden_section_batch_min(
&problems,
Sense::Minimise,
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");
}
}