StateBench

StateBench

While developing software, from large to small scale, you can use state machines to reduce code complexity and help teams to concentrate on separate parts of the functionality. Imagine the simple scenario for developing a video player. The following diagram, at one glance, describes the software functionality:

No alt text provided for this image

Later on, adding features is straightforward because the scope of the change and side effects are obvious. For instance, integrating the rewind feature will look like this:

No alt text provided for this image

With each state being able to have its own set of internal states.

Another benefit that will be more apparent over time is having the application's logic in one place rather than being scattered across the code base. 

In C++, we have two dominant implementations for state machines. One is Boost’s Statechart and the other is Qt's SCXML. I have put both of them to the test and compared ease of use and performance. For benchmarking purposes, I am going to create a circular transition between three states and measure, on average, how many round trips each implementation completes.

Qt benefits from a more clear syntax. So I start from there. The following shows state definitions:

QStateMachine machine;
QState starting(&machine);
QState running(&machine);
QState first(&running);
QState second(&running);
QState third(&running);
QState stopping(&machine);

running.setInitialState(&first);
machine.setInitialState(&starting);        

And after that, we add transitions:

starting.addTransition(&running);
first.addTransition(&first, &QState::entered, &second);
second.addTransition(&second, &QState::entered, &third);
third.addTransition(&third, &QState::entered, &first);
running.addTransition(&timer, &QTimer::timeout, &stopping);        

Finally, we declare what actions take place in each state.

QObject::connect(&starting, &QState::exited, [&]() {
  timer.start(10000);
  startTime = high_resolution_clock::now();
});
QObject::connect(&third, &QState::entered, [&]() {
  QCoreApplication::processEvents();
  iterations += 1;
});
QObject::connect(&stopping, &QState::entered, [&]() {
  auto duration{high_resolution_clock::now() - startTime};
  auto msecs{duration_cast<milliseconds>(duration)};
  std::cout << double(iterations) / (double(msecs.count()) / 1000)
            << " (iterations/sec)" << std::endl;
  QCoreApplication::instance()->exit();
});        

We do the same for Boost implementation as well. But it is not going to be as readable as QT’s. First, we declare two events:

namespace sc = boost::statechart;
struct ProceedEvent : sc::event<ProceedEvent> {};
struct TimeoutEvent : sc::event<TimeoutEvent> {};        

Then we add states:

struct Starting;
struct Running;
struct Stopping;
struct Machine : sc::state_machine<Machine, Starting> {};
struct Starting : sc::simple_state<Starting, Machine> {};
struct First;
struct Second;
struct Third;
struct Running : sc::simple_state<Running, Machine, First> {};
struct First : sc::simple_state<First, Running> {};
struct Second : sc::simple_state<Second, Running> {};
struct Third : sc::simple_state<Third, Running> {};
struct Stopping : sc::simple_state<Stopping, Machine> {};        

Then we put transitions in place:

struct Starting : sc::simple_state<Starting, Machine> {
  typedef sc::transition<ProceedEvent, Running> reactions;
};
struct Running : sc::simple_state<Running, Machine, First> {
  typedef sc::transition<TimeoutEvent, Stopping> reactions;
};
struct First : sc::simple_state<First, Running> {
  typedef sc::transition<ProceedEvent, Second> reactions;
};
struct Second : sc::simple_state<Second, Running> {
  typedef sc::transition<ProceedEvent, Third> reactions;
};
struct Third : sc::simple_state<Third, Running> {
  typedef sc::transition<ProceedEvent, First> reactions;
};        

And finally, we complete the work by defining what to do at each state:

struct Starting : sc::simple_state<Starting, Machine> {
  typedef sc::transition<ProceedEvent, Running> reactions;
  ~Starting() {
    StartTime = std::chrono::high_resolution_clock::now();
    Iterations = 0;
  }
};
struct Third : sc::simple_state<Third, Running> {
  typedef sc::transition<ProceedEvent, First> reactions;
  ~Third() { Iterations += 1; }
};
struct Stopping : sc::simple_state<Stopping, Machine> {
  ~Stopping() {
    auto duration{std::chrono::high_resolution_clock::now() - StartTime};
    auto msecs{std::chrono::duration_cast<std::chrono::milliseconds>(duration)};
    std::cout << double(Iterations) / (double(msecs.count()) / 1000)
              << " (iterations/sec)" << std::endl;
  }
};        

As you see, the Qt implementation is more straightforward. In addition to that, since the Boost implementation relies heavily upon templates, you may often get cryptic error messages.

QStates live through the machine's lifetime while Statechart states are constantly constructed and destructed. Both libraries support state-local storage. Qt support that through its property system for all QObjects and Statechart uses dynamic casts. 

Performance-wise, as you may have guessed, Statechart blows Qt away. It is five times faster. That is because Qt relies heavily on its signal-slot mechanism. On the other hand, if you store enough state-local variables in each Statechart state, you may witness some performance drops. 

You can find the benchmarking code in my GitHub repository. I will be glad to hear about your experience working with state machines in other frameworks. 


To view or add a comment, sign in

More articles by Mohammad Rahimi

  • Programming Policies

    Picture this situation: you've got a continuous flow of data coming in, and your task is to process it and present the…

  • Exception x Exception

    In my previous post, we demystified std::forward. In this one, we'll explore what happens when you encounter another…

  • Forward Demystified

    In this post I have shed some light on one of the darker corners of the C++ language: std::forward. The code snippets…

  • Maintainer's Dream

    The distributed workflow for Linux source code development is called Benevolent Dictator. Some maintainers collect…

    2 Comments
  • FTowerX

    A few years ago, I was developing a program for remotely controlling a signal generator. I wanted to create a simple…

    1 Comment
  • GitCheat

    In my previous post, I introduced a long-existing tool that helps you migrate your workflow from Perforce to Git. Here,…

  • Perforce meets Git

    Perforce is a Centralised Version Control System founded in 1995 and used by many companies. Git came into existence…

  • In Mail IP

    I was planning to run a home server and access it remotely. But there was an issue.

    2 Comments
  • Dot Bash History

    I have prepared a GitHub repository where you can find many useful commands and scripts. The following explains how you…

  • C++ and Benchmarking

    It may sound extreme, but I believe benchmarking too should get the special treatment of testing in a project. There…

Others also viewed

Explore content categories