use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, Serialize, Deserialize)]
#[non_exhaustive]
pub struct Calibration {
pub integer_ns: f64,
pub memory_bw_gibps: f64,
pub memory_lat_ns: f64,
}
pub fn run_calibration() -> Calibration {
let integer_ns = calibrate_integer();
let memory_bw_gibps = calibrate_memory_bandwidth();
let memory_lat_ns = calibrate_memory_latency();
Calibration {
integer_ns,
memory_bw_gibps,
memory_lat_ns,
}
}
const REP_BUDGET_NS: f64 = 25_000_000.0;
fn budgeted_count(probed_ns_per_iter: f64, min_count: u64, max_count: u64) -> u64 {
if probed_ns_per_iter <= 0.0 {
return max_count;
}
let fit = (REP_BUDGET_NS / probed_ns_per_iter) as u64;
fit.clamp(min_count, max_count)
}
fn calibrate_integer() -> f64 {
fn run_iters(iters: u64) -> f64 {
let start = std::time::Instant::now();
let mut v = 0u64;
for i in 0..iters {
v = v.wrapping_add(std::hint::black_box(i));
}
std::hint::black_box(v);
start.elapsed().as_nanos() as f64 / iters as f64
}
let probe_ns = run_iters(200_000);
let iters = budgeted_count(probe_ns, 1_000_000, 10_000_000);
let mut best_ns = f64::MAX;
for _ in 0..5 {
let per_iter = run_iters(iters);
if per_iter < best_ns {
best_ns = per_iter;
}
}
best_ns
}
fn calibrate_memory_bandwidth() -> f64 {
let size = 1024 * 1024; let buf: Vec<u64> = vec![1u64; size / 8];
let mut best_gibps = 0.0_f64;
for _ in 0..5 {
let start = std::time::Instant::now();
let mut sum = 0u64;
for &val in &buf {
sum = sum.wrapping_add(val);
}
std::hint::black_box(sum);
let elapsed_s = start.elapsed().as_secs_f64();
let gibps = (size as f64) / elapsed_s / (1024.0 * 1024.0 * 1024.0);
if gibps > best_gibps {
best_gibps = gibps;
}
}
best_gibps
}
fn calibrate_memory_latency() -> f64 {
let n_elements = 4 * 1024 * 1024 / 8; let mut chain: Vec<usize> = (0..n_elements).collect();
let mut rng_state = 0xDEAD_BEEF_CAFE_BABEu64;
for i in (1..n_elements).rev() {
rng_state = rng_state.wrapping_mul(6364136223846793005).wrapping_add(1);
let j = (rng_state >> 33) as usize % (i + 1);
chain.swap(i, j);
}
fn run_steps(chain: &[usize], n_elements: usize, steps: u64) -> f64 {
let start = std::time::Instant::now();
let mut idx = 0usize;
for _ in 0..steps {
idx = chain[idx % n_elements];
}
std::hint::black_box(idx);
start.elapsed().as_nanos() as f64 / steps as f64
}
let probe_ns = run_steps(&chain, n_elements, 50_000);
let steps = budgeted_count(probe_ns, 250_000, 1_000_000);
let mut best_ns = f64::MAX;
for _ in 0..3 {
let per_step = run_steps(&chain, n_elements, steps);
if per_step < best_ns {
best_ns = per_step;
}
}
best_ns
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn calibration_values_are_sane() {
let cal = run_calibration();
assert!(
cal.integer_ns > 0.0 && cal.integer_ns < 100.0,
"integer_ns should be 0-100, got {}",
cal.integer_ns
);
assert!(
cal.memory_bw_gibps > 0.1 && cal.memory_bw_gibps < 200.0,
"memory_bw should be 0.1-200 GiB/s, got {}",
cal.memory_bw_gibps
);
assert!(
cal.memory_lat_ns > 0.1 && cal.memory_lat_ns < 500.0,
"memory_lat should be 0.1-500 ns, got {}",
cal.memory_lat_ns
);
}
#[test]
fn calibration_is_fast() {
let start = std::time::Instant::now();
let _cal = run_calibration();
assert!(
start.elapsed() < std::time::Duration::from_secs(5),
"calibration should complete in < 5s"
);
}
}