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 :
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.tv_nsec
- it gives the time aftertv_sec
, in nanoseconds since the time source - that was specified while callingclock_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 int
s / float
s. 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::duration
types. 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!
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.