Keyboard shortcuts

Press ← or → to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

The CPU Counter

Counter reads the CPU’s free-running counter in one instruction that touches no memory: cntvct_el0 on aarch64, the time-stamp counter on x86_64. A reading is a Tickstamp, the difference of two a Tickdelta, and the counter’s TickRate turns that into a Timedelta.

Ticks and Rates

A tick’s length is the counter’s: Apple silicon’s counter runs at 24 MHz, an Arm core’s from Armv8.6 on at 1 GHz, which is TickRate::GIGAHERTZ, and an x86_64 core’s at the rate its time-stamp counter is rated for. A reading’s origin is the hardware’s own and means nothing, so only a span converts: Tickdelta::to_timedelta and Timedelta::to_tickdelta, each at a rate.

fn a_rate_turns_ticks_into_a_span_and_back() {
    use t2t::{TickRate, Tickdelta, Tickstamp, Timedelta};

    // Apple silicon's counter runs at 24 MHz.
    let rate = TickRate::from_hertz(24_000_000).expect("a rate above zero");
    let (start, end) = (Tickstamp::from_ticks(1_000_000), Tickstamp::from_ticks(1_000_024));

    let ticks = end - start;
    assert_eq!(ticks, Tickdelta::from_ticks(24), "a reading minus a reading is a span of ticks");
    assert_eq!(ticks.to_timedelta(rate), Timedelta::MICROSECOND, "worth a microsecond at 24 MHz");
    assert_eq!(Timedelta::SECOND.to_tickdelta(rate), Tickdelta::from_ticks(24_000_000), "and back");
    assert_eq!(TickRate::from_hertz(0), None, "a counter that never ticks has no rate");
}

Discovering the Counter

Counter::discover finds the counter’s rate, or refuses with a CounterError that says why there is none to read as time:

  • On aarch64 it reads cntfrq_el0, the rate the firmware wrote.
  • On x86_64 it refuses a time-stamp counter that is not invariant, by CPUID leaf 0x8000_0007. It takes the rate from CPUID leaf 0x15, a crystal’s frequency times a ratio; where that has none, from the hypervisor’s leaf 0x4000_0010; and where neither has one, with std on 64-bit Linux or macOS, it measures the rate over 10 ms against RawMonotonicClock.
  • On either, a rate outside 1 MHz to 10 GHz is refused.
RefusalWhenWhy
NotInvariantthe time-stamp counter’s rate follows the core’sits ticks have no fixed length, so no rate makes them time
NoRatethe CPU reports no rate, and none could be measurednothing says what a tick is worth
ImplausibleRatethe rate read or measured is outside 1 MHz to 10 GHzbelow a tick a microsecond a counter measures nothing worth measuring, and none runs past 10 GHz, so a rate outside is taken for wrong

NoRate comes only on x86_64, where CPUID reports no rate and none could be measured, as without std, or off 64-bit Linux and macOS. Each refusal’s message says which it is: counter error: a rate of 42 Hz is outside 1 MHz to 10 GHz.

fn the_counter_is_discovered_or_refused_with_its_reason() {
    use t2t::Timedelta;
    use t2t::clock::{Clock, Counter};

    match Counter::discover() {
        Ok(counter) => {
            let start = counter.now();
            let ticks = counter.now() - start;
            let took = ticks.to_timedelta(counter.rate());
            println!("two reads {ticks} apart at {}: {took}", counter.rate());
            assert!(took < Timedelta::from_millis(1), "two reads back to back");
        },
        // A virtual machine may promise no invariant counter, and a CPU may report no rate.
        Err(refusal) => println!("no counter to time with: {refusal}"),
    }
}

A measurement takes 10 ms, so a program discovers its counter once, at start-up, and keeps it: a Counter is Copy, and holds only its rate.

Virtual Machines

A hypervisor chooses the CPUID bits its guest sees. One that does not promise an invariant counter makes discover refuse with NotInvariant, even where the host’s counter is invariant: the Intel macOS virtual machines t2t’s CI runs on are such guests, and refuse. One that reports the counter’s rate in leaf 0x4000_0010 spares the guest the measurement.

Where the rate is known, from a config or from a discovery made elsewhere, Counter::new takes it and checks nothing: neither that the counter is invariant, nor that the rate is plausible. The caller vouches for both. Where discover refuses and no rate is known, read MonotonicClock instead.

fn a_counter_at_a_known_rate_needs_no_discovery() {
    use t2t::TickRate;
    use t2t::clock::{Clock, Counter};

    // A rate taken from elsewhere: a config, or a discovery another process made.
    let rate: TickRate = "1000000000 Hz".parse().expect("a rate above zero");
    let counter = Counter::new(rate);

    let start = counter.now();
    assert!(counter.now() >= start, "the counter never runs backwards");
    assert_eq!(counter.rate(), TickRate::GIGAHERTZ, "and runs at the rate it was given");
}

One Counter for the Machine

Every core and every process on a machine reads the same counter, so a Tickstamp one process takes, another may subtract from. t2t-clock’s test counter_cross_process starts a child process, and checks that the reading the child prints lies between two the parent took around it. A reading crosses as its spelling, 1000 ticks, or as its count. Since only spans convert, two processes that measured slightly different rates still agree on every reading; they differ only in what a span is worth.

The read is not ordered against the instructions around it, which is right for a stamp, and for timing anything much longer than the read itself. Clock::now on a Counter is #[inline(always)], since a call around the read would move it.

The Conversion

TickRate::from_hertz works out, once, a factor and a shift for each direction, so a conversion is a 128-bit multiply and a shift on the count’s magnitude, with its sign put back, and no division. For a ratio of to , to the rate for ticks to nanoseconds and the rate to the other way:

The shift is the largest whose factor still fits 64 bits, so the factor carries every bit of precision it can. Rounding it up makes an exact multiple of the ratio convert exactly, and any other count land within one unit of its exact quotient, which a property test checks against an i128 division, on counts drawn from every i64 and rates from every u64 above zero. A result past the range saturates.

fn a_conversion_is_within_one_unit_of_the_exact_quotient() {
    use t2t::{TickRate, Tickdelta, Timedelta};

    // 3 GHz: a nanosecond is three ticks, and a tick a third of a nanosecond.
    let rate = TickRate::from_hertz(3_000_000_000).expect("a rate above zero");
    let exact = Tickdelta::from_ticks(3_000).to_timedelta(rate);
    assert_eq!(exact, Timedelta::MICROSECOND, "a multiple of the ratio converts exactly");

    // 1,000 ticks are 333.33 ns: the result is 333 or 334, never further off.
    let nanos = Tickdelta::from_ticks(1_000).to_timedelta(rate).as_nanos();
    assert!((333..=334).contains(&nanos), "within a nanosecond of 333.33: {nanos}");

    // A count whose span is past what nanoseconds hold saturates, as every operator does.
    let slow = TickRate::from_hertz(1).expect("a rate above zero");
    assert_eq!(Tickdelta::MAX.to_timedelta(slow), Timedelta::MAX, "a tick a second, for ever");
}

A conversion takes about 1.2 ns from ticks to nanoseconds and 1.0 ns the other way, at 24 MHz on a Graviton4: 1,024 of them in 1.206 µs and 1.034 µs, in the run under crates/t2t-core/benches/results/2026-10-02T23-14Z-9776c95-span-divisors/.