Measure Precise Time with clock_gettime in C

The POSIX clock_gettime function provides a standardized, high-resolution interface for benchmarking and timekeeping in C, capable of measuring intervals down to the nanosecond level. This article explains how clock_gettime achieves this precision by pairing low-overhead kernel mechanisms like vDSO with modern CPU hardware counters, outlines the specific clock IDs required for stable interval measurement, and demonstrates the exact math required to compute elapsed times without precision loss.

The timespec Structure and Resolution

The precision of clock_gettime stems from its output structure, struct timespec, defined in <time.h>:

struct timespec {
    time_t   tv_sec;        /* Seconds */
    long     tv_nsec;       /* Nanoseconds [0, 999999999] */
};

Unlike legacy functions such as gettimeofday (which measures in microseconds using struct timeval), clock_gettime natively reports time in seconds and integer nanoseconds.

The theoretical resolution of the function depends on the underlying system. You can inspect the actual resolution supported by the operating system by calling clock_getres:

struct timespec res;
clock_getres(CLOCK_MONOTONIC, &res);

On modern platforms, res.tv_nsec often reports a resolution of 1 nanosecond.

Hardware Timers and the Role of vDSO

Achieving sub-microsecond accuracy requires both high-speed hardware and minimal software overhead.

1. Hardware Counters (TSC and Generic Timers)

Modern x86 processors contain an internal register known as the Time Stamp Counter (TSC), while ARM architectures provide the Generic Timer (such as the CNTVCT register). In modern processors with an "invariant" or "constant" TSC, the counter increments at a fixed frequency independent of CPU frequency scaling, sleep states, or thermal throttling. The operating system kernel reads these high-speed counter ticks and converts them into standard time units.

2. vDSO (Virtual Dynamic Shared Object)

A traditional system call (syscall) incurs a context switch between user space and kernel space, which introduces a latency penalty of hundreds of nanoseconds. To eliminate this overhead, modern operating systems (such as Linux) map a portion of kernel memory directly into user space via the vDSO.

When you call clock_gettime, the dynamic linker invokes the vDSO implementation. This user-space routine reads the CPU's hardware register (e.g., via the RDTSC or RDTSCP instruction on x86) and performs the multiplication and shifting logic to calculate nanoseconds directly in user memory, executing in tens of nanoseconds rather than microseconds.

Selecting the Right Clock Source

The first argument of clock_gettime specifies the clock source (clockid_t). Selecting the correct clock is essential for accurate elapsed time measurement:

  • CLOCK_MONOTONIC: The standard choice for interval benchmarking. It represents monotonic time since an unspecified starting point (usually system boot). It cannot be set manually and does not jump backward if the system administrator or Network Time Protocol (NTP) updates the wall-clock time. It may, however, be slewed (gradually adjusted in frequency) by NTP to correct clock drift.
  • CLOCK_MONOTONIC_RAW: Available on Linux, this provides hardware-based monotonic time that is completely immune to NTP frequency slewing. It represents the raw hardware counter ticks converted directly to time.
  • CLOCK_REALTIME: Represents system wall-clock time (time since the Unix Epoch). It is unsuitable for interval timing because it can step backward or forward unpredictably due to manual adjustments or NTP synchronization.

Computing Elapsed Intervals Correctly

When measuring elapsed intervals, calculate the difference between two timespec structures. Because tv_nsec values reset to 0 every second, you must account for nanosecond underflow when subtracting:

#define _POSIX_C_SOURCE 199309L
#include <stdio.h>
#include <time.h>
#include <unistd.h>

void get_elapsed_time(struct timespec start, struct timespec end, struct timespec *diff) {
    if ((end.tv_nsec - start.tv_nsec) < 0) {
        diff->tv_sec = end.tv_sec - start.tv_sec - 1;
        diff->tv_nsec = 1000000000L + end.tv_nsec - start.tv_nsec;
    } else {
        diff->tv_sec = end.tv_sec - start.tv_sec;
        diff->tv_nsec = end.tv_nsec - start.tv_nsec;
    }
}

int main(void) {
    struct timespec start, end, elapsed;

    // Capture starting timestamp
    clock_gettime(CLOCK_MONOTONIC, &start);

    // Code block or workload to measure
    usleep(1500); // Sleep for 1500 microseconds

    // Capture ending timestamp
    clock_gettime(CLOCK_MONOTONIC, &end);

    // Calculate difference
    get_elapsed_time(start, end, &elapsed);

    // Convert to microseconds and nanoseconds
    long long total_nanoseconds = (elapsed.tv_sec * 1000000000LL) + elapsed.tv_nsec;
    double microseconds = (double)total_nanoseconds / 1000.0;

    printf("Elapsed: %lld ns (%.3f µs)\n", total_nanoseconds, microseconds);

    return 0;
}

By using CLOCK_MONOTONIC alongside the vDSO-backed clock_gettime call, C applications avoid kernel context switches and wall-clock shifts, yielding deterministic, high-resolution measurements suitable for microsecond and nanosecond benchmarking.