Time and clocks

C has two kinds of time: the calendar time people read, and the elapsed time a program measures. The first is a number of seconds since 1970 and needs a conversion to a date; the second should come from a clock that never jumps. Mixing them is the classic timing bug.

Wall clock and formatting

time_t counts seconds since the Unix epoch. gmtime_r fills a struct tm with UTC fields, and strftime formats them. The example uses a fixed value so the output does not depend on when it runs; the same calls with time(NULL) give the current moment.

#define _POSIX_C_SOURCE 200809L #include <stdio.h> #include <time.h> int main(void) { time_t moment = 1700000000; struct tm parts; char text[64]; if (gmtime_r(&moment, &parts) == NULL) { return 1; } if (strftime(text, sizeof text, "%Y-%m-%d %H:%M:%S", &parts) == 0) { return 1; } printf("%s\n", text); printf("weekday: %d\n", parts.tm_wday); return 0; }

gcc -Wall -Wextra stamp.c -o stamp
./stamp

2023-11-14 22:13:20
weekday: 2

The weekday is 2, which is Tuesday in the C numbering where Sunday is 0. localtime_r would use the machine's time zone instead of UTC; the _r variants are the thread-safe forms and the ones to use in new code.

A monotonic clock

Wall clock time can jump when the system adjusts its clock, so it is the wrong tool for measuring duration. CLOCK_MONOTONIC only moves forward and is the clock for elapsed time. clock_gettime returns seconds and nanoseconds in a struct timespec.

#define _POSIX_C_SOURCE 199309L #include <stdio.h> #include <time.h> int main(void) { struct timespec start; struct timespec end; long total = 0; clock_gettime(CLOCK_MONOTONIC, &start); for (int i = 0; i < 1000000; i++) { total += i; } clock_gettime(CLOCK_MONOTONIC, &end); long long ns = (end.tv_sec - start.tv_sec) * 1000000000LL + (end.tv_nsec - start.tv_nsec); printf("total: %ld\n", total); printf("elapsed: %lld ns\n", ns); return 0; }

gcc -Wall -Wextra monotonic.c -o monotonic
./monotonic

total: 499999500000
elapsed: 8118109 ns

The sum is the closed form 999999 × 1000000 / 2, and the elapsed time is about eight milliseconds on this machine. The number changes with the load and the compiler; the structure of the measurement is the point. The multiplication by a billion is done in long long so the nanoseconds fit.

Processor time

CLOCK_PROCESS_CPUTIME_ID counts only the time the process spent on a processor, so it ignores the moments when it waited. That is the clock for comparing two implementations of the same work, while the monotonic clock measures the wall time a user feels.

#define _POSIX_C_SOURCE 199309L #include <stdio.h> #include <time.h> int main(void) { struct timespec start; struct timespec end; volatile long total = 0; clock_gettime(CLOCK_PROCESS_CPUTIME_ID, &start); for (int i = 0; i < 1000000; i++) { total += i; } clock_gettime(CLOCK_PROCESS_CPUTIME_ID, &end); long long ns = (end.tv_sec - start.tv_sec) * 1000000000LL + (end.tv_nsec - start.tv_nsec); printf("cpu: %lld ns\n", ns); return 0; }

gcc -Wall -Wextra cpu.c -o cpu
./cpu

cpu: 3677683 ns

The volatile stops the compiler from removing the loop whose result is unused; without it the optimiser is allowed to delete the whole computation and the measurement becomes meaningless. The value is close to the monotonic one here because the process did not wait.

Sleeping for a while

nanosleep pauses the process for a timespec and returns early only for a signal. The program measures the pause with the monotonic clock, so the result is the real wait, not the requested one.

#define _POSIX_C_SOURCE 199309L #include <stdio.h> #include <time.h> int main(void) { struct timespec delay = {0, 20000000}; struct timespec start; struct timespec end; clock_gettime(CLOCK_MONOTONIC, &start); nanosleep(&delay, NULL); clock_gettime(CLOCK_MONOTONIC, &end); long long ms = (end.tv_sec - start.tv_sec) * 1000LL + (end.tv_nsec - start.tv_nsec) / 1000000; printf("slept: %lld ms\n", ms); return 0; }

gcc -Wall -Wextra sleep.c -o sleep
./sleep

slept: 20 ms

The requested delay is twenty milliseconds, written as 20000000 nanoseconds, and the measured pause is twenty milliseconds after rounding down. A sleep is a lower bound: the process may wake later, never earlier, which is why the measurement compares with the request rather than with zero.

A time_t in an int slot

time_t is an arithmetic type whose width is not fixed by the standard; on this machine it is a long. Printing it with %d is a mismatch that the compiler reports, and the suggested conversion is %ld. The cast to long long and %lld is the portable choice.

#include <stdio.h> #include <time.h> int main(void) { time_t now = time(NULL); printf("now: %d\n", now); return 0; }

gcc -Wall -Wextra badformat.c -o badformat

badformat.c: In function 'main':
badformat.c:8:19: warning: format '%d' expects argument of type 'int', but argument 2 has type 'time_t' {aka 'long int'} [-Wformat=]
8 | printf("now: %d\n", now);
| ~^ ~~~
| | |
| int time_t {aka long int}

The program is not run here: on a 32-bit int the high half of the value is lost and the printed date would be wrong, while this build happens to print the right number because both types pass the value in the same register. The warning is the portable answer, not the run.

Exercises

  1. Print the current date and time with time, localtime_r and strftime in two formats.
  2. Measure the time of a loop with both clocks and explain the difference when a nanosleep is inside the loop.
  3. Write a function that returns the number of milliseconds between two timespec values, and test it with a sleep.

Questions

A new set of questions in a new order every time. Choose one answer for each, then check.

  1. What does a time_t value count?

    Answer

    Seconds since the Unix epoch

    The value is calendar time in seconds since 1970, and the tm struct breaks it into date fields.

  2. Which clock is right for measuring elapsed time?

    Answer

    A monotonic clock that only moves forward

    Wall clock adjustments can jump, while CLOCK_MONOTONIC measures a steady interval.

  3. What does CLOCK_PROCESS_CPUTIME_ID measure?

    Answer

    Time the process spent on a processor

    It counts processor time rather than wall time, so it is useful for comparing implementations of the same work.

  4. What does nanosleep guarantee about the pause?

    Answer

    It waits at least the requested time

    The process may wake later than requested, never earlier, so the requested delay is a lower bound.

  5. What date and weekday does the fixed timestamp print?

    Answer

    2023-11-14 and weekday 2

    The epoch value 1700000000 is 14 November 2023, a Tuesday, which the C numbering reports as 2.

  6. What total does the monotonic loop print?

    Answer

    499999500000

    The loop adds 0 through 999999, which is 999999 times 1000000 divided by 2.

  7. A timespec delay of {0, 20000000} is passed to nanosleep. How long is that?

    Answer

    Twenty milliseconds

    The second field is nanoseconds, so 20,000,000 ns is twenty milliseconds.

  8. What does GCC warn about in printf("now: %d", now) for a time_t?

    Answer

    That %d expects int, but the value is time_t

    time_t is a long here, so the format should be %ld or the value cast to a matching type.

  9. Why is the wall clock a poor choice for a duration?

    Answer

    Its value can jump when the system clock is set

    An adjustment moves the wall clock forward or backward, so the difference may be negative or too large.

  10. Why is volatile used on the total in the CPU-time loop?

    Answer

    So the optimiser cannot delete the unused loop

    Without a visible use, the optimiser may remove the whole computation and make the measurement meaningless.

The code on this page was run with gcc (Alpine 15.2.0) 15.2.0 on Alpine Linux 3.24.3 (x86_64) in a Docker container.

Article author: Arthur Isaev