How to use the std::chrono API
[ad_1]
Keeping track of time is a very important aspect of computer programs. Some common use cases are:
- 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.
- Synchronize tasks between different components of software
and many more…
This article will guide you through how you can measure time in modern C++.
Prerequisites
Common Ways to Track Time in C++
This article covers how you can keep track of time in C++. In C, on UNIX like systems, you can use the clock_gettime() function to keep track of time. It returns 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
, which 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 istime_t
which is usually an integral value.tv_nsec
, which 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 can happen when you’re dealing with projects that have thousands of lines of code.
So what’s the solution?
The std::chrono API
C++11 introduced the std::chrono API, which can help you avoid some of these problems.
There are 3 important parts of the API.
1. 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 you write:
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 milliseconds (1/1000th of a second). This count is expressed as an integer. As you might be able to 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’s 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 you instead try to pass in any non-std::chrono::duration
type, you’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
, and so on.
You 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, you need 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 why std::chrono::duration
is useful, let’s move on. The next section explores std::chrono::time_point
.
2. std::chrono::time_point
std::chrono::time_point
is a way of expressing a particular point in time – surprise, surprise!
If you think about it, how can you 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 this:
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 you want to measure time in terms of real-world times. It’s important to note that the system time can usually be changed on any system, so you 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 we discussed in the previous section. A time_point
needs to be associated with a duration
type since that’s what is being 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 – that is, the 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.
3. Clock Types
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 how you can tie together duration
, time_point
, and Clock
. Let’s say you want to measure the time that looping 100,000,000 times takes in nanoseconds, and you 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;
}
The output of the program is the following:
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 thanks to its strongly typed system. It also helps maintain support for convenient conversions between different ‘types’ of time points.
[ad_2]
Source link