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.