Time flies like an arrow
-- Common example showing ambiguities in natural languages (how many interpretations does it have?)
There is only one reason why I'm writing this blog entry: I simply think it's annoying to see well designed C++ code interspersed with POSIX or Windows/Mac specific functions used for timing various parts of the code. A much better alternative is to use the chrono library from either boost or C++11. So to prove to myself and to others that it is simple to use the chrono library I'll put together a simple, here I really mean dead simple, old fashioned stopwatch driven by the chrono library.
Since I'm using C++11 I can either use the C++11 chrono library or the boost version of the library. Here I'll use the boost version for the simple reason that it has a larger selection of clocks for driving my stopwatch.
Why a stopwatch? Well, a stopwatch needs to get hold of the current time, it will have to calculate the difference between two points in time, it should be able to give back results in various units of time and it should preferably be able to return time measurements in a type chosen by the user. Additionally it would be good if it also could be configured to use different types of clocks for measuring time. All in all a stopwatch will show the ease or difficulty in using the chrono library.
At a first glance the chrono library looks a little too complicated for something that should be relatively intuitive. However, a few minutes down in the documentation, it's clear that the chrono facility is based on three basic concepts making it very simple to build devices measuring time:
· clock
· timepoint
· duration
The concepts mean exactly what the intuitive meaning of their respective name is. A timepoint is, well, a point in time. A duration is exactly what the name implies - a time span. Having two timepoints I can create a duration by subtracting them. Finally, a clock gives me access to the current time. There is a little more to it than that but it's enough to get started building things that measure time.
Because modern C++ is what it is, the timepoint and duration are obviously templates. This is most probably a good thing since hopefully many computations will be done at compile time which may be important when measuring time. For right now I'll mostly ignore the template aspect of the chrono library since it distracts from the main ideas. The implementation at the end of this blog entry shows how the template parameters fit into the picture.
With the three concepts – timepoint, duration and clock - together with the ratio facility I should be able to pound out a simple old fashioned stopwatch class in a few lines of code. The ratio facility is needed since duration represents a time span as a count and a period where the period is expressed using a ratio which in turn represents the number of seconds in each period. For example, std::ratio<3600*24*7> corresponds to one week and std::ratio<1,1000> corresponds to a millisecond.
The design is simple. The stopwatch has two buttons – a button that starts and stops the watch when clicked and one button that resets the clock. By having a single button for starting and stopping I don't have to torture my brain with what to do when, say a start button is pushed twice in a row. Just as with an old fashioned stopwatch I can reset the watch both when it's ticking and when it is stopped. If resetting the watch while it's ticking the watch will move back to time zero and continue ticking.
The interface, ignoring constructors and destructors, would then look something like:
// R – return type for getting measured time
class StopWatch{
…
// start/stop and reset
void click();
void reset();
// get elapsed time
R getElapsedTimeNs()const;
R getElapsedTimeUs()const;
R getElapsedTimeMs()const;
R getElapsedTimeSec()const;
R getElapsedTimeMin()const;
R getElapsedTimeHours()const;
R getElapsedTimeDays()const;
R getElapsedTimeWeeks()const;
// get state
bool isRunning()const noexcept;
bool isStopped()const noexcept;
…
};
So far, nothing from the chrono library has been introduced. The interface, with the exception of the template parameter, looks the way I could have designed a stopwatch based on any time library. This is a good thing since if things work out OK the chrono library does not interfere too much with how time measurement devices are put together.
Now, since the start/stop button and the reset button must change the internal state of the stopwatch a simple way of implementing most of the functionality is to code the click() and reset() functions as a small finite state machine. Since the watch is either ticking or stopped only two states are needed. OK, maybe it's overkill to use a state machine, but it makes for a clean design.
Calculating the current time is done outside the state machine to avoid const issue (the methods reading the current time are const whereas the click and reset methods are non-const).
In order to make it simple to use the stopwatch I've typedefed a few, what I believe, should be commonly used stopwatches:
using SystemStopWatch=StopWatch<std::chrono::system_clock,double>;
using HRStopWatch =StopWatch<std::chrono:: high_resolution_clock,double>;
using SteadyStopWatch=StopWatch<std::chrono::steady_clock,double>;
The system_clock is a clock that represents system time. This means that there is no guarantee that a timepoint returned from the clock will not be ahead of a timepoint retrieved at some later time. After all, someone could have changed the system time. The high_resolution_clock is a clock that has the highest precision of any clock on the machine. Finally, the steady_clock is a clock where that never returns a lower timepoint than a previously returned one plus, it ticks following the real time
A few more stopwatches are needed when measuring process cpu timings:
using ProcRealCpuStopWatch=StopWatch<boost::chrono::process_real_cpu_clock,double>;
using ProcUserCpuStopWatch=StopWatch<boost::chrono::process_user_cpu_clock,double>;
using ProcSystemCpuStopWatch=StopWatch<boost::chrono::process_system_cpu_clock,double>;
using ProcCpuStopWatch=StopWatch<boost::chrono::process_cpu_clock,double>;
Take a look at the documentation of the boost::chrono library to see the exact definition of the clocks. Or better, look through the implementation which is very clear and shows how the time is measured on POSIX, Windows or MAC systems.
The last clock measures time at the thread level:
using ThreadStopWatch=StopWatch<boost::chrono::thread_clock,double>;
Again, to best understand the difference between the clocks, the boost chrono documentation or implementations are the best sources of information.
Using one of the stopwatches is simple enough:
…
SteadyStopWatch sw;
sw.click();
…
// do something
…
sw.click();
cerr<<"duration: "<< sw.getElapsedTimeUs()<<": "<<"us"<<endl;
The full implementation is shown at the end of this blog entry.
The implementation gathers most of the use of the chrono library inside a state machine implementation. It could be debated if the R template parameter should be part of the class or not since it does not serve much purpose except making the stopwatch slightly simpler to use. It could also be argued if the getter functions should not return a duration. Here I've rather arbitrary chosen to keep the interface free from chrono specific types.
Well, the time for judgement has come. Let's see, I can easily and transparently use different types of clocks within my stopwatch. I can, as the implementation below shows, transparently represent elapsed time using time periods of my own choice. Getting hold of the current time is simple. The code also uses specific types for time points and durations – that is, time points and duractions are not simply integer values. Also important, many properties of the stopwatch are evaluated at compile time which is exactly what I want when measuring time. Finally, concepts behind the chrono library are far more intuitive than most platform specific time functions.
Here is the implementation:
#include <boost/chrono.hpp>
#include <boost/ratio.hpp>
#ifdef BOOST_CHRONO_HAS_PROCESS_CLOCKS
#include <boost/chrono/process_cpu_clocks.hpp>
#endif
#ifdef BOOST_CHRONO_HAS_THREAD_CLOCK
#include <boost/chrono/thread_clock.hpp>
#endif
// old fashioned stopwatch
template<typename C,typename R=double>
class StopWatch{
public:
// ctor, assign, dtor
StopWatch()=default;
StopWatch(StopWatch const&)=default;
StopWatch(StopWatch&&)=default;
StopWatch&operator=(StopWatch const&)=default;
StopWatch&operator=(StopWatch&&)=default;
~StopWatch()=default;
// start/top and reset
void click(){fsm_(Event::click);}
void reset(){fsm_(Event::reset);}
// get elapsed time
R getElapsedTimeNs()const{return getElapsedTime<boost::chrono::duration<R,boost::nano>>();}
R getElapsedTimeUs()const{return getElapsedTime<boost::chrono::duration<R,boost::micro>>();}
R getElapsedTimeMs()const{return getElapsedTime<boost::chrono::duration<R,boost::milli>>();}
R getElapsedTimeSec()const{return getElapsedTime<boost::chrono::duration<R,boost::ratio<1>>>();}
R getElapsedTimeMin()const{return getElapsedTime<boost::chrono::duration<R,boost::ratio<60>>>();}
R getElapsedTimeHours()const{return getElapsedTime<boost::chrono::duration<R,boost::ratio<3600>>>();}
R getElapsedTimeDays()const{return getElapsedTime<boost::chrono::duration<R,boost::ratio<3600*24>>>();}
R getElapsedTimeWeeks()const{return getElapsedTime<boost::chrono::duration<R,boost::ratio<3600*24*7>>>();}
// get state
bool isRunning()const noexcept{return state_==State::running;}
bool isStopped()const noexcept{return !isRunning();}
private:
// typedefs and states
using TP=typename C::time_point;
using DURATION=typename C::duration;
enum class Event:int{click=0,reset=1};
enum class State:int{running=0,stopped=1};
// state of stop watch
C clock_;
TP startTime_=TP();
DURATION accumDuration_=DURATION::zero();
State state_=State::stopped;
// general method to read elapsed time expressed in type R units of duration D
template<typename D>
R getElapsedTime()const{
DURATION currDuration{accumDuration_};
if(state_==State::running)currDuration+=clock_.now()-startTime_;
return boost::chrono::duration_cast<D>(currDuration).count();
}
// state machine driving clock - returns current duration
void fsm_(Event evnt){
if(state_==State::running){
if(evnt==Event::click){
accumDuration_+=(clock_.now()-startTime_);
state_=State::stopped;
}else{
accumDuration_=DURATION::zero();
startTime_=clock_.now();
}
}else{
if(evnt==Event::click){
state_=State::running;
startTime_=clock_.now();
}else{
accumDuration_=DURATION::zero();
startTime_=TP();
}
}
}
};
// commonly used stop watches
using SystemStopWatch= StopWatch<boost::chrono::system_clock,double>;
using HRStopWatch=StopWatch<boost::chrono::high_resolution_clock,double>;
using SteadyStopWatch=StopWatch<boost::chrono::steady_clock,double>;
#ifdef BOOST_CHRONO_HAS_PROCESS_CLOCKS
using ProcRealCpuStopWatch=StopWatch<boost::chrono::process_real_cpu_clock,double>;
using ProcUserCpuStopWatch=StopWatch<boost::chrono::process_user_cpu_clock,double>;
using ProcSystemCpuStopWatch=StopWatch<boost::chrono::process_system_cpu_clock,double>;
using ProcCpuStopWatch=StopWatch<boost::chrono::process_cpu_clock,double>;
#endif
#ifdef BOOST_CHRONO_HAS_THREAD_CLOCK
using ThreadStopWatch=StopWatch<boost::chrono::thread_clock,double>;
#endif