use std::sync::OnceLock;
use std::time::Duration;
#[cfg(target_arch = "x86_64")]
#[inline(always)]
fn read_cycles() -> u64 {
unsafe {
let mut _aux: u32 = 0;
std::arch::x86_64::__rdtscp(&mut _aux)
}
}
#[cfg(target_arch = "aarch64")]
#[inline(always)]
fn read_cycles() -> u64 {
let cnt: u64;
unsafe {
std::arch::asm!("mrs {}, cntvct_el0", out(reg) cnt, options(nostack, nomem));
}
cnt
}
#[cfg(not(any(target_arch = "x86_64", target_arch = "aarch64")))]
#[inline(always)]
fn read_cycles() -> u64 {
0
}
pub const HAS_CYCLE_COUNTER: bool = cfg!(target_arch = "x86_64") || cfg!(target_arch = "aarch64");
#[derive(Debug, Clone, Copy)]
pub struct Instant {
instant: std::time::Instant,
tsc: u64,
}
impl Instant {
#[inline(always)]
pub fn now() -> Self {
let tsc = read_cycles();
Self {
instant: std::time::Instant::now(),
tsc,
}
}
#[inline(always)]
pub fn elapsed(&self) -> Duration {
self.instant.elapsed()
}
#[inline(always)]
pub fn cycles(&self) -> u64 {
self.tsc
}
}
pub struct Timer {
start: Instant,
cycles_start: u64,
}
impl Timer {
#[inline(always)]
pub fn start() -> Self {
let cycles_start = read_cycles();
Self {
start: Instant::now(),
cycles_start,
}
}
#[inline(always)]
pub fn stop(&self) -> (u64, u64) {
let elapsed = self.start.elapsed();
let nanos = elapsed.as_nanos() as u64;
let cycles = read_cycles().saturating_sub(self.cycles_start);
(nanos, cycles)
}
}
const CALIBRATION_PAIRS: usize = 1_000;
const CALIBRATION_BATCHES: usize = 20;
static TIMER_OVERHEAD: OnceLock<(f64, f64)> = OnceLock::new();
fn calibrate_timer() -> (f64, f64) {
let mut best_ns = f64::MAX;
let mut best_cycles = f64::MAX;
for _ in 0..CALIBRATION_BATCHES {
let outer = Timer::start();
for _ in 0..CALIBRATION_PAIRS {
let timer = Timer::start();
std::hint::black_box(std::hint::black_box(&timer).stop());
}
let (ns, cycles) = outer.stop();
best_ns = best_ns.min(ns as f64 / CALIBRATION_PAIRS as f64);
best_cycles = best_cycles.min(cycles as f64 / CALIBRATION_PAIRS as f64);
}
(best_ns, best_cycles)
}
pub fn timer_overhead_ns() -> f64 {
TIMER_OVERHEAD.get_or_init(calibrate_timer).0
}
pub fn cycles_overhead() -> f64 {
TIMER_OVERHEAD.get_or_init(calibrate_timer).1
}
#[cfg(target_os = "linux")]
pub fn pin_to_cpu(cpu: usize) -> Result<(), std::io::Error> {
use std::mem::MaybeUninit;
unsafe {
let mut set = MaybeUninit::<libc::cpu_set_t>::zeroed();
let set_ref = set.assume_init_mut();
libc::CPU_ZERO(set_ref);
libc::CPU_SET(cpu, set_ref);
let result = libc::sched_setaffinity(0, std::mem::size_of::<libc::cpu_set_t>(), set_ref);
if result == 0 {
Ok(())
} else {
Err(std::io::Error::last_os_error())
}
}
}
#[cfg(not(target_os = "linux"))]
pub fn pin_to_cpu(_cpu: usize) -> Result<(), std::io::Error> {
Ok(())
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_instant_elapsed() {
let start = Instant::now();
std::thread::sleep(Duration::from_millis(10));
let elapsed = start.elapsed();
assert!(elapsed >= Duration::from_millis(5));
assert!(elapsed < Duration::from_millis(100));
}
#[test]
fn test_timer() {
let timer = Timer::start();
std::thread::sleep(Duration::from_millis(10));
let (nanos, _cycles) = timer.stop();
assert!(nanos >= 5_000_000);
}
#[test]
fn timer_overhead_calibrated_positive_and_small() {
let ns = timer_overhead_ns();
assert!(ns > 0.0, "timer overhead is {ns} ns");
assert!(ns < 1000.0, "timer overhead is {ns} ns");
assert_eq!(ns, timer_overhead_ns());
assert!(cycles_overhead() >= 0.0);
}
#[test]
fn test_cycle_counter() {
if HAS_CYCLE_COUNTER {
let a = read_cycles();
let b = read_cycles();
assert!(b >= a, "cycle counter should be monotonic");
}
}
}