One for the toolbox: Stopwatch

Since I’ll be doing lots of time measurements going forward, an easy to use time measurement thingie could be good to have, to minimize the risk of doing bad measurements and to make the code clean enough to not have the measurements distracting us from what is important.

With the C# Stopwatch class as inspiration, I added my own C++ stopwatch class to the toolbox. Its public interface bears resemblance with the aforementioned C# class:

class stopwatch final
{
    using clock = std::chrono::high_resolution_clock;
    clock::time_point m_start;
    clock::time_point m_stop;

public:

    stopwatch() = default;
    static stopwatch start_new();

    void start();
    void stop();

    std::chrono::microseconds::rep us() const;
    std::chrono::milliseconds::rep ms() const;
};

Internally it just uses the std::chrono library and its high_resolution_clock to get two time_point instances to subtract and convert to either microseconds or milliseconds. However, I felt uneasy about the surprising level of state that had to be managed, in such a seemingly simple class, to make starting and stopping it at various times both possible and consistent.

Usage-wise, stopwatch is a lot easier on the eyes than using raw std::chrono calls:

using clock = std::chrono::high_resolution_clock;
const auto t1 = clock::now();
something_measurable();
const auto t2 = clock::now();
const auto us = std::chrono::duration_cast<std::chrono::microseconds>
                (clock::now() - m_start).count();

vs

const auto sw = stopwatch::start_new();
something_measurable();
const auto us = sw.us();

or (for example, to not force stopwatch reinstantiation in tight loops)

stopwatch sw;
... // start loop
sw.start();
something_measurable();
sw.stop();
const auto us = sw.us();
... // iterate

But the ability to instantiate stopwatch in a non-started state and start/stop it repeatedly during its lifetime and still guarantee some kind of measurement consistency made the implementation unnecessarily complex. The added complexity took its toll on the code generation side; the resulting assembly did way more than I wanted or anticipated – I could see the underlying library implementation locking critical sections when I as a user of the class already knew that I had done two safe sequential time readings.

Therefore I decided that the start/stop functionality in stopwatch was cruft. I removed all unnecessary state and only supported one usage scenario: instantiate stopwatch in a “started” state and let each call to us() or ms() just give the elapsed time since instantiation:

class stopwatch final
{
    using clock = std::chrono::high_resolution_clock;
    clock::time_point m_start = clock::now();

public:

    std::chrono::microseconds::rep us() const;
    std::chrono::milliseconds::rep ms() const;
};

Now the stopwatch interface is much cleaner, it’s practically impossible to use it in the wrong way and it has a pretty trivial implementation.

Friends of order might now instead object with “What about measuring time in a tight loop? With the previous stopwatch interface we could instantiate it outside the loop and inside the loop we could start/stop at will without needing to reinstantiate the stopwatch on each iteration. Surely the new stopwatch must be sub-optimal in comparison?!”

Guess what? It Really Doesn’t Matter.

The following three ways of measuring the time taken by 1000 invocations of something_measurable() will generate almost identical assembly.

Raw std::chrono usage:

using clock = std::chrono::high_resolution_clock;

for (size_t i = 0; i < 1000; ++i)
{
    const auto t1 = clock::now();
    something_measurable();
    const auto t2 = clock::now();
    const auto us = std::chrono::duration_cast
                    <std::chrono::microseconds>(t2 - t1).count();
    ...

“Old” stopwatch usage:

stopwatch sw;

for (size_t i = 0; i < 1000; ++i)
{
    sw.start();
    something_measurable();
    sw.stop();
    const auto us = sw.us();
    ...

“New” stopwatch usage:

for (size_t i = 0; i < 1000; ++i)
{
    const stopwatch sw;
    something_measurable();
    const auto us = sw.us();
    ...

Of those three alternatives, given that the generated code is almost identical and that it really doesn’t matter that we’re instantiating stopwatch on each iteration, I choose the last one in a heartbeat because there are

  • fewer lines of usage,
  • fewer lines of underlying implementation,
  • fewer things that can go wrong and hence
  • fewer things to test.

And with that we have the first toolbox item for the road ahead:

Advertisements