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
- Print the current date and time with time, localtime_r and strftime in two formats.
- Measure the time of a loop with both clocks and explain the difference when a nanosleep is inside the loop.
- 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.
-
What does a
time_tvalue count?Answer
Seconds since the Unix epoch
The value is calendar time in seconds since 1970, and the
tmstruct breaks it into date fields. -
Which clock is right for measuring elapsed time?
Answer
A monotonic clock that only moves forward
Wall clock adjustments can jump, while
CLOCK_MONOTONICmeasures a steady interval. -
What does
CLOCK_PROCESS_CPUTIME_IDmeasure?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.
-
What does
nanosleepguarantee 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.
-
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.
-
What total does the monotonic loop print?
Answer
499999500000
The loop adds 0 through 999999, which is 999999 times 1000000 divided by 2.
-
A
timespecdelay of{0, 20000000}is passed tonanosleep. How long is that?Answer
Twenty milliseconds
The second field is nanoseconds, so 20,000,000 ns is twenty milliseconds.
-
What does GCC warn about in
printf("now: %d", now)for atime_t?Answer
That
%dexpectsint, but the value istime_ttime_tis a long here, so the format should be%ldor the value cast to a matching type. -
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.
-
Why is
volatileused 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