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:
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:
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:
Recommended by LinkedIn
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.