Keeping Time in C++: How to use the std::chrono API

Why is it useful to keep track of time in programs?

Time is a very important aspect of programs. Some common use cases for keeping track of time would be :

  • To Measure/profile the performance of certain parts of code.

  • Do work at certain periods of time, from within a program. Detect whether threads are in a deadlock / taking too long to complete an operation. And many more…

What are the typical ways in which time can be tracked?

This article covers how one can keep track of time in C++. In C, on UNIX like systems one can use the clock_gettime() function to keep track of time. They gave us time in a structured way through the timespec struct. The clock_gettime() /gettimeofday function gives us back a filled timespec struct which has two fields :

  1. tv_sec - it gives us the time in seconds since the time source - CLOCK_REALTIME / CLOCK_MONOTONIC that was passed into clock_gettime. The 'type' of this field is time_t which is usually an integral value.

  2. tv_nsec - it gives the time after tv_sec, in nanoseconds since the time source - that was specified while calling clock_gettime(). The type of this field is a long int

So why is clock_gettime() not good enough ? The answer is that the members of struct timespec can easily be passed to functions as they're really just ints / floats. They're not strongly typed. It's also easy to forget about the units in which they represent time while passing information around to functions. This does happen when one is dealing with projects having thousands of lines of code.

The std::chrono API

C++11 introduced the std::chrono API, using which one can avoid some of these problems.

There are 3 important parts of the API.

std::chrono::duration

As its name suggests, std::chrono::duration is a type that represents a time interval. The official C++ reference mentions that std::chrono::duration is a templated type with the following signature

template<
    class Rep,
    class Period = std::ratio<1>
> class duration;

Here the Rep template parameter represents the type that is used to count 'ticks' of time. A tick is just a unit of time which is a given fraction of a second. Period - the second parameter, defines what exactly that fraction is.

So, for example, if one writes

using my_ms_type = std::chrono::duration<int, std::ratio<1, 1000>>

my_ms_type duration_ms duration = 3; // error: cannot convert from int
my_ms_type duration_ms duration_ok{3} // OK, can construct from int

my_ms_type is a type that has been defined, which counts in units of 1/1000th of a second - and this count is expressed as an integer. As you could guess, the Rep template parameter is int and Period is std::ratio<1,1000> (which really is a way of saying 1/1000).

Now that it is clear how durations are represented, let's see what we can and cannot do with these.

If there is a function that takes in a my_ms_type duration and one instead tries to pass in any non std::chrono::duration type, they'll get a compiler error.

It is possible to implicitly convert between different types of std::chrono::duration as long as information isn't lost with the type of Rep, since the standard library can compute the relationship between two std::chrono::durationtypes. It is not possible to implicitly convert if there is a loss of information. For example

#include<chrono>

using namespace std::chrono;
using my_type_ms = std::chrono::duration<int, std::ratio<1, 1000>>;
using my_type_ms_f = std::chrono::duration<float, std::ratio<1, 1000>>;
using my_type_hundredth_s = std::chrono::duration<int, std::ratio<1, 100>>;
void f(my_type_ms millis) {}
int main()
{
   int duration = 2;
   my_type_ms_f duration_f{2.5};
   my_type_hundredth_s duration_compatible{100};

   f(duration); // error: could not convert 'duration' from 'int' to 'my_type_ms'

   f(duration_f) //error: since float -> int will lose information

   f(duration_compatible) // OK since no information is lost
}

The standard library also has some predefined std::chrono::duration template specializations for common time durations such as std::chrono::duration::seconds, milliseconds, microseconds etc.

One can also get the 'count' value contained in a duration by using the count method in a duration.

std::chrono::seconds duration{3};
// Prints: 'Duration count: 3 seconds'
std::cout << "Duration count: " << duration.count() << " seconds";

Interestingly, converting from a unit with higher precision like nanosecond to something with a lower precision such as millisecond may also lead to a loss of information - for these specific cases one needs to use an explicit cast for conversion. This is called duration_cast. For example :

nanoseconds durationInNs = 3000000000;
seconds ms = duration_cast<seconds>(durationInNs); //OK 3s
durationInNs = 3500000000;
ms = duration_cast<nanoseconds>(durationInNs); // OK 3s - truncates down

Now that we know std::chrono::duration is useful. The next section explores. std::chrono::time_point

std::chrono::time_point

std::chrono::time_point is a way of expressing a particular point in time - surprise, surprise! If one thinks about it - how can one logically define a point in time ? We need to have a reference starting point and a duration from the starting point. This is exactly what std::chrono::time_point does. The class declaration looks like

template<
    class Clock,
    class Duration = typename Clock::duration
> class time_point;

There are two template parameters here :

The first one is Clock which represents a reference clock relative to which the point in time is being measured. For now, some examples of clocks are

  • system_clock - This represents a real-world wall clock. It's useful when one wants to measure time in terms of real-world times. It is important to note that the system time can usually be changed on any system, so one shouldn't depend on this clock to calculate time periods between tasks / performance profiling.

  • steady_clock - This represents a monotonically increasing clock. It's useful when you need stop-watch like clock accounting.

The second template parameter is Duration which is what was discussed in the previous section. A time_point needs to be associated with a duration type since that's what is be used to measure ticks since the 'epoch' of the Clock. Epoch is just a way of saying a reference point in time - while there's no mandate for which reference to use, Unix Time - i.e., time since 00:00:00 Coordinated Universal Time (UTC), Thursday, 1 January 1970 is a common one.

Time points based on the same clock can be subtracted - and not added. For example:

auto tp1 = std::chrono::system_clock::now();
...
auto tp2 = std::chrono::system_clock::now()
auto tp3 = std::chrono::steady_clock::now();

auto diff = tp2 - tp1; // OK
auto add = tp1 + tp2; // Not Ok
auto add = tp3 - tp2; // Not Ok - based on different clocks

Let's now see what clocks are :

Clocks

A Clock is a type that ties together std::chrono::duration and std::chrono::time_point - it has a function now() that returns the current time_point. The formal requirements for a type to be a Clock can be found in the C++ spec here.

As mentioned before system_clock and steady_clock are two popular clocks provided by the standard library. Each clock has its own associated duration as well.

Each time_point is associated with some clock - since it really has to be relative to some given reference.

Finally, let's see some examples of duration, time_point and Clock can be tied together. Let's say one wants to measure the time a looping 100000000 times takes in nanoseconds and we also want to print out the current wall time :

#include <chrono>
#include <iostream>
#include <ratio>
#include <thread>
 #include <ctime>

using namespace std::chrono;
constexpr size_t kIterations = 100000000;
void testFunction () {
    for (size_t i = 0; i < kIterations; i++) {
    }
}

int main()
{
    auto tStartSteady = std::chrono::steady_clock::now();
    std::time_t startWallTime = system_clock::to_time_t(system_clock::now());
    std::cout << "Time start = " << std::ctime(&startWallTime) << " \n";
    testFunction();
    auto tEndSteady = std::chrono::steady_clock::now();
    nanoseconds diff = tEndSteady - tStartSteady;
    std::time_t endWallTime = system_clock::to_time_t(system_clock::now());
    std::cout << "Time end = " << std::ctime(&endWallTime) << " \n";
    std::cout << "Time taken = " << diff.count() << " ns";
    return 0; 
}
Output:
// This can of course vary from system to system
Time start = Tue Nov  7 07:11:13 2023

Time end = Tue Nov  7 07:11:13 2023

Time taken = 50998885 ns

Summary

This article explored various facets of the std::chrono API in C++. The std::chrono API allows C++ programmers to safely keep track of time - due to its strongly typed system, while maintaining support for convenient conversions between different 'types' of time points.

That is it! I hope std::chrono is useful to the readers!

1
Subscribe to my newsletter

Read articles from Jayant Chowdhary directly inside your inbox. Subscribe to the newsletter, and don't miss out.

Written by

Jayant Chowdhary
Jayant Chowdhary

I am a software engineer working on Android OS Camera Software.