Course contentsShow
← Rust
Lesson 1510 of 1,91012. Unsafe Rust and Low-Level AccessFree lesson

Clock Cycle Benchmarking

You will measure code execution time using internal hardware performance monitoring registers.

In this lesson, you will learn how to precisely measure the performance of your code by reading internal hardware performance monitoring registers (PMUs) to count exact CPU clock cycles.

Measuring Time at the Micro-Architectural Level

While high-level timers are useful for wall-clock time, they often suffer from OS overhead and context-switching jitter. To benchmark a specific function or instruction sequence with absolute accuracy, we interface directly with the CPU's internal time-stamp counter (TSC).

On x86_64 architectures, the RDTSC (Read Time-Stamp Counter) instruction provides a cycle count that starts from zero at processor reset and increments at every clock cycle. Because this is an assembly-level operation, we use Rust's core::arch intrinsics to access it safely.

Implementation

To benchmark a block of code, we read the register immediately before and after execution. Crucially, we must ensure the processor isn't reordering instructions across our measurement window, which would invalidate the results. We use a serialization instruction like CPUID or a memory fence to force the CPU to finish all previous instructions before reading the register.

use core::arch::x86_64::{_rdtsc, __cpuid};

pub unsafe fn measure_cycles<F: Fn()>(f: F) -> u64 {
    // Ensure previous instructions finish
    __cpuid(0);
    let start = _rdtsc();
    
    f();
    
    // Ensure the function finishes before reading the end time
    __cpuid(0);
    let end = _rdtsc();
    
    end - start
}

Critical Considerations

When using cycle counts, remember that "cycles" are not "time." If your CPU supports Dynamic Frequency Scaling (Turbo Boost), your clock frequency might change while your code runs. Additionally, the RDTSC counter counts ticks, not instructions. If you are measuring a loop that executes multiple instructions per cycle, you are observing the throughput of your pipeline rather than the latency of a single operation.

Always run your benchmarks multiple times and calculate a statistical average or median to account for pipeline stalls, branch mispredictions, and cache misses that can introduce variance between runs.

Key Takeaway: Benchmarking with internal registers provides the highest possible timing resolution, but you must use serialization instructions to prevent the CPU's out-of-order execution engine from skewing your measurements.