Stopwatch¶
Anybody curious about application performance needs a way to time sections of code without using expensive (in overhead)
tools like Callgrind/KCachegrind.
This article is about building a stopwatch
class that works well in multithread-ready and exception-safe code.
template <typename TClock>
class stopwatch
{
The reason for the TClock
type parameter is because there are an array of clocks available to use with the C++
standard, each with different guarantees.
The most relevant for our purposes is high_resolution_clock
, but you might find steady_clock
or some other
future clock type interesting.
public:
typedef TClock clock;
typedef typename clock::time_point time_point;
typedef typename clock::duration duration;
typedef typename duration::rep tick_type;
Fairly standard member type declarations to pull in the member types from TClock
.
private:
std::atomic<tick_type> _ticks;
}
The only member variable that our stopwatch
class has is _ticks
, which is used to keep track of how many times
the clock has ticked.
We define it as one of those fancy atomics (tick_type
is just a number) so we can get atomic addition and whatnot
without having to use the vendor-specific implementations or TBB.
This is a great step in the direction of thread-safety.
stopwatch() :
_acquired(0)
{ }
duration total() const
{
return duration(_ticks);
}
Those functions are fairly uninteresting...initialize the time to 0 and a way to get the total elapsed time.
Making the Stopwatch Go the Wrong Way¶
That class lacks a very important bit of functionality – a way to add time.
In a few implementations, people add start
and stop
as member functions and code to use them would look like
this:
watch.start();
do_some_operation();
watch.stop();
There are two problems with that code
The first problem is that it is not exception-safe.
What happens if the call to do_some_operation
throws an exception?
In this case, nothing – the execution would skip right over watch.stop()
.
Making this exception-safe would be painful, too, since C++ does not have a finally
.
What happens next time we call start
without stopping first?
The second problem is that the code is not thread-safe.
Implementing the member functions start
and stop
would be painful to make thread-safe.
What happens when multiple threads call start
at the same time?
It would be pretty easy to only log one thread at a time – just grab the current thread ID when you start and only
listen to stop
calls on that same thread.
That plays horribly with exception-safety, though; what if that thread never tells you to stop because of an exception?
You could do thread-local tracking of all outstanding start
calls without a stop
, but then you have to deal with
destruction issues (thread-local storage and C++ classes have classically not played well together).
Making the Stopwatch Go the Right Way¶
Hopefully that short parade of horrors convinced you that start
and stop
member functions are not the way to go.
The real problem is that giving stopwatch
functions to start and stop it violates the object-oriented
Single Responsibility Principle.
The only responsibility of stopwatch
should be to keep track of how many times it has ticked, not control the
actual ticking.
So, the real function we need is this extremely simple one:
void add(const duration& tm)
{
_ticks.fetch_add(tm.count(), std::memory_order_relaxed);
}
Thanks to the power of std::atomic
, the code is thread-safe.
How could it not be?
So how do we call add
in an exception-safe manner?
Easy, wrap the logic of timing and reporting into the lifetime of a class!
A decent name for this is ticker
, which is an internal class of stopwatch
.
class ticker
{
public:
ticker() :
_owner(nullptr)
{ }
explicit ticker(stopwatch* owner) :
_owner(owner),
_starttime(clock::now())
{ }
~ticker()
{
if (_owner)
{
duration tick_time = clock::now() - _starttime;
_owner->add(tick_time);
}
}
private:
stopwatch* _owner;
time_point _starttime;
};
We are not quite done here.
The first problem is copying.
If someone takes a ticker
and makes a copy of it, the destructor for both ticker
instances will be called and
the duration will be reported double.
Copying is not desirable for this class, so let’s just disable it:
ticker(const ticker&) = delete;
ticker& operator =(const ticker&) = delete;
To use this code, you have to say something like:
{
stopwatch_type::ticker(&watch);
do_some_operation();
}
That does not look too bad, but I am super lazy and want my code to look more like this:
{
auto ticker = watch.start();
do_some_operation();
}
Thanks to the power of C++ move semantics, you can do just that!
In stopwatch
, add this member function:
ticker start()
{
return ticker(this);
}
And add a move constructor to ticker
:
ticker(ticker&& source) :
_owner(source._owner),
_starttime(source._starttime)
{
source._owner = nullptr;
}
That function looks a lot like a copy constructor, with the added bonus of being able to manipulate the contents of
source
.
It is a good thing we can, since source
will still be destructed.
Setting source._owner
to null lets the destructor know not to do anything later.
If we did not do this, we would face the same problem of time from a ticker
being reported twice.
A Slight Problem¶
One problem that was immediately pointed out to me was: What if somebody wrote this reasonable-looking bit of code:
{
watch.start();
do_some_operation();
}
The problem is that the ticker
would be destroyed immediately after being returned from watch.start()
and
watch
would forever remain at zero (or really close to it).
Unfortunately, there is not a good way to solve this issue (not even the GCC attribute warn_unused_result
is capable
of producing a warning message here).
Since they haven’t read the documentation, my best hope is that they are confused by the lack of a function named
stop
and consult it, but probably not.
There might be a better name for the function (like create_ticker
).
Overall, this problem fits into the “possibly dodgy behaviour that still makes some sense” on the behavior spectrum, so
we have to live with it.
More Info¶
Source Code¶
Get the source code for the stopwatch
class here: stopwatch.hpp
.
It is Apache licensed, so use it under those terms (just put the header in your code).
There is also a (tiny) demo program: stopwatch.cpp
.
It has been compiled and tested with g++ 4.6.2, but any compiler with good support for the C++0x standard should be
perfectly fine with it.