feat(backmp11): introduce flat_fold as default dispatch strategy

This commit is contained in:
Christian Granzin
2026-02-21 17:56:53 -05:00
committed by Christian Granzin
parent 515ff4f8d7
commit d0cf98bd39
7 changed files with 271 additions and 155 deletions
@@ -5,44 +5,37 @@ It is currently in **experimental** stage, thus some details about the compatibi
It is named after the metaprogramming library Boost Mp11, the main contributor to the optimizations.
Usages of MPL are replaced with Mp11 to get rid of the costly C{plus}{plus}03 emulation of variadic templates.
The new back-end has the following goals:
- reduce compilation time and RAM usage
- reduce state machine runtime
- provide new features and customization options
It offers a significant reduction in compilation time and RAM usage, as can be seen in these benchmarks:
It offers a significant improvement in runtime and memory usage, as can be seen in these benchmarks:
*Large state machine*
[width=80%, cols="3,1,1,1", options="header"]
|=======================================================================
| | Compile / sec | RAM / MB | Runtime / sec
| back | 17 | 961 | 2.8
| back_favor_compile_time | 22 | 1006 | 3.5
| back11 | 40 | 2802 | 2.7
| backmp11 | 3 | 218 | 1.1
| backmp11_favor_compile_time | 3 | 206 | 6.0
| sml | 6 | 273 | 0.5
|=======================================================================
[%autowidth, options="header"]
|=======================================================================================================
| | Compile time / sec | Compile RAM / MB | Binary size / kB | Runtime / sec
| back | 14 | 815 | 68 | 2.8
| back_favor_compile_time | 17 | 775 | 226 | 3.5
| back11 | 37 | 2682 | 84 | 2.8
| backmp11 | 3 | 209 | 29 | 0.7
| backmp11_favor_compile_time | 3 | 193 | 43 | 6.0
| sml | 5 | 234 | 57 | 0.3
|=======================================================================================================
*Large hierarchical state machine*
[width=80%, cols="3,1,1,1", options="header"]
|================================================================================
| | Compile / sec | RAM / MB | Runtime / sec
| back | 64 | 2902 | 13.2
| back_favor_compile_time | 75 | 2612 | > 300
| backmp11 | 9 | 370 | 5.2
| backmp11_favor_compile_time | 6 | 282 | 20.3
| backmp11_favor_compile_time_multi_cu | 4 | ~917 | 20.7
| sml | 24 | 706 | 5.7
|================================================================================
[%autowidth, options="header"]
|================================================================================================================
| | Compile time / sec | Compile RAM / MB | Binary size / kB | Runtime / sec
| back | 49 | 2165 | 230 | 13.2
| back_favor_compile_time | 55 | 1704 | 911 | > 300
| backmp11 | 8 | 351 | 85 | 3.3
| backmp11_favor_compile_time | 5 | 256 | 100 | 20.4
| backmp11_favor_compile_time_multi_cu | 5 | ~863 | 100 | 20.8
| sml | 18 | 543 | 422 | 5.4
|================================================================================================================
The full code with the benchmarks and more information about them is available https://github.com/chandryan/fsm-benchmarks[in this repository].
There are still a couple more optimizations to come, the tables in the repository frequently get updated with the latest results.
The full code with the benchmarks and more information about them is available https://github.com/chandryan/fsm-benchmarks[in this repository]. The tables in the repository are frequently updated with results from the latest development branches of the benchmarked libraries.
== Deprecation information
@@ -546,57 +539,65 @@ The implementation of these two features depends on mpl_graph, which induces hig
Not needed with the functor front-end and was already deprecated, thus removed in `backmp11`.
== How to use it
== Compile policies
The back-end with both its compile policies `favor_runtime_speed` and `favor_compile_time` should be mostly compatible with existing code.
Like `back`, this back-end supports 2 compile policies. In case of hierarchical state machines all state machines must be configured with the same compile policy.
Required replacements to try it out:
=== `favor_runtime_speed`
- for the state machine use `boost::msm::backmp11::state_machine` in place of `boost::msm::back::state_machine` and
- for configuring the compile policy and more use `boost::msm::backmp11::state_machine_config`
- if you encounter API-incompatibilities please check the <<_other_changes_with_respect_to_back,details above>> for reference
This policy favors runtime speed over compile time, it evaluates all transitions and generates the dispatch table at compile time.
The dispatch strategy can be tuned by inheriting from `favor_runtime_speed` and adapting the using directive:
```cpp
struct favor_runtime_speed
{
// Dispatch strategy for processing events.
// Supported strategies:
// - flat_fold (default)
// - function_pointer_array
using dispatch_strategy = dispatch_strategy::flat_fold;
};
```
It currently supports two dispatch strategies:
[%autowidth, options="header"]
|===
| Strategy | Description
| `flat_fold` (default)
| Generates a flat fold of inline comparison branches. +
All transition logic is visible to the optimizer. +
Usually results in optimal runtime performance and executable size.
| `function_pointer_array`
| Generates an array of function pointers. +
Guarantees O(1) dispatch, but the function pointers cannot be inlined. +
Usually results in worse runtime performance and larger executable size, but slightly better compile time.
|===
Since the back-end should compile very fast for most machines, the manual generation of state machines with the `favor_compile_time` policy has become an opt-in feature.
If you want to build your state machine across multiple compilation units, you need to do the following:
=== `favor_compile_time`
- set up a preprocessor define `BOOST_MSM_BACKMP11_MANUAL_GENERATION` before including `msm/backmp11/favor_compile_time.hpp`
- then generate your state machine(s) in the compilation units with the macro `BOOST_MSM_BACKMP11_GENERATE_STATE_MACHINE(<smname>)`
This policy favors compile time over runtime speed. It evaluates transitions lazily and generates the dispatch table at runtime. Like its counterpart in `back`, it does not support Kleene events.
Events are wrapped into a `std::any` when they enter event processing to reduce the number of necessary templates instances required to generate the state machine.
The policy utilizes a hash map for dispatch, with the type index of each event as the key and an array of function pointers to the matching transitions as the value.
This policy allows compiling a state machine across multiple translation units (TUs) with the help of a preprocessor macro.
Since the back-end should compile very fast for most state machines, this is an opt-in feature.
If you want to build your state machine across multiple TUs, you need to do the following:
- define `BOOST_MSM_BACKMP11_MANUAL_GENERATION` before including `msm/backmp11/favor_compile_time.hpp`
- then generate your state machine back-end(s) with the macro `BOOST_MSM_BACKMP11_GENERATE_STATE_MACHINE(<sm_type>)`
You can find an example for this in the https://github.com/boostorg/msm/blob/develop/test/Backmp11Visitor.cpp[visitor test].
== Applied optimizations
== How to use it
Below you can find some insights how the compile-time and runtime optimizations were achieved.
The back-end should be mostly compatible with existing code. Required replacements to try it out:
- Replacement of CPU-intensive calls (due to C{plus}{plus}03 recursion from MPL) with Mp11
- Replaced O(N) algorithms with O(1) alternatives (using additional dispatch tables)
- Added more type filters prior to template instantiations
- Applied type punning where useful (to reduce template instantiations, e.g. `std::deque` & other things around the dispatch table)
=== `favor_runtime_speed` policy
Summary:
- Unconditionally default-initialized everything first and afterwards only the row-related transition cells
- Optimized cell initialization with initializer arrays (to reduce template instantiations)
=== `favor_compile_time` policy
Once an event is given to the FSM for processing, it is immediately wrapped with `std::any` and processing continues with this `any` event.
The structure of the dispatch table has been reworked, one dispatch table is created per state as a hash map.
The state dispatch tables are designed to directly work with the `any` event, they use the event's type index via its `type()` function as hash value.
This mechanism enables SMs to forward events to sub-SMs without requiring additional template instantiations just for forwarding as was needed with the `process_any_event` mechanism.
The new mechanism enables **forwarding of events to sub-SMs in O(1) order instead of O(N)**.
Summary:
- Use one dispatch table per state to reduce compiler processing time
- The algorithms for processing the STT and states are optimized to go through rows and states only once
- These dispatch tables are hash tables with type_id as key
- Apply type erasure with `std::any` as early as possible and do further processing only with any events
- each dispatch table only has to cover the events it's handling, no template instantiations required for forwarding events to sub-SMs
- for the state machine use `boost::msm::backmp11::state_machine` in place of `boost::msm::back::state_machine` and
- for configuring the compile policy and more use `boost::msm::backmp11::state_machine_config`
- if you encounter API-incompatibilities please check the <<_other_changes_with_respect_to_back,details above>> for reference
@@ -11,6 +11,7 @@
* fix(backmp11): Completion events fire too often (https://github.com/boostorg/msm/issues/166[#166])
* feat(backmp11): Small Object Optimization for events in the event pool (https://github.com/boostorg/msm/issues/172[#172])
* feat(backmp11): Improve support for the `deferred_events` property in hierarchical state machines (https://github.com/boostorg/msm/issues/173[#173])
* feat(backmp11): Improve runtime performance with a `flat_fold` dispatch strategy (https://github.com/boostorg/msm/issues/180[#180])
== Boost 1.90
@@ -182,6 +182,9 @@ class deferred_event : public event_occurrence
Event m_event;
};
template <typename Policy, typename = void>
struct compile_policy_impl;
} // namespace detail
} // namespace boost::msm::backmp11
@@ -21,15 +21,22 @@
namespace boost { namespace msm { namespace backmp11
{
struct favor_runtime_speed {};
struct favor_runtime_speed
{
// Dispatch strategy for processing events.
// Supported strategies:
// - flat_fold (default)
// - function_pointer_array
using dispatch_strategy = dispatch_strategy::flat_fold;
};
namespace detail
{
template <typename Policy>
struct compile_policy_impl;
template <>
struct compile_policy_impl<favor_runtime_speed>
struct compile_policy_impl<
Policy,
std::enable_if_t<std::is_base_of_v<favor_runtime_speed, Policy>>>
{
template <typename Event>
constexpr static const Event& normalize_event(const Event& event)
@@ -147,8 +154,8 @@ struct compile_policy_impl<favor_runtime_speed>
if constexpr (has_transitions::value ||
has_forward_transitions::value)
{
const dispatch_table_impl& self = dispatch_table_impl::instance();
return self.dispatch(sm, region_id, event);
using table = dispatch_impl<typename Policy::dispatch_strategy>;
return table::dispatch(sm, region_id, event);
}
return process_result::HANDLED_FALSE;
}
@@ -235,61 +242,8 @@ struct compile_policy_impl<favor_runtime_speed>
using has_forward_transitions =
mp11::mp_not<mp11::mp_empty<submachines_with_forward_transitions>>;
class dispatch_table_impl
class dispatch_base
{
public:
static const dispatch_table_impl& instance() {
static dispatch_table_impl table;
return table;
}
process_result dispatch(
StateMachine& sm, int region_id, const Event& event) const
{
const int state_id = sm.m_active_state_ids[region_id];
const cell_t& cell = m_cells[state_id];
if (cell)
{
return cell(sm, region_id, event);
}
return process_result::HANDLED_FALSE;
}
private:
dispatch_table_impl()
{
using initial_map_items = mp11::mp_transform<
initial_map_item,
submachines_with_forward_transitions>;
using filtered_transitions_by_state_map = mp11::mp_fold<
filtered_transition_table,
initial_map_items,
map_updater>;
using merged_transitions = mp11::mp_transform<
merge_transitions,
filtered_transitions_by_state_map>;
using init_cell_constants = mp11::mp_transform<
get_init_cell_constant,
merged_transitions>;
dispatch_table_initializer::execute(
reinterpret_cast<generic_cell*>(m_cells),
reinterpret_cast<const generic_init_cell_value*>(
value_array<init_cell_constants>),
mp11::mp_size<init_cell_constants>::value);
}
template <typename Transition>
static process_result convert_event_and_execute(StateMachine& sm,
int region_id,
Event const& evt)
{
typename Transition::transition_event kleene_event{evt};
return Transition::execute(sm, region_id, kleene_event);
}
template <size_t index, cell_t cell>
using init_cell_constant = init_cell_constant<index, cell_t, cell>;
// Build a map with key=state/value=[matching_transitions]
// from the filtered transition table.
template <typename M, typename Key, typename Value>
@@ -326,7 +280,6 @@ struct compile_policy_impl<favor_runtime_speed>
template <typename Map, typename Transition>
using map_updater = typename map_updater_impl<Map, Transition>::type;
private:
// Pseudo-transition used to forward event processing to a submachine.
template <typename Submachine>
struct forward_transition
@@ -375,34 +328,174 @@ struct compile_policy_impl<favor_runtime_speed>
mp11::mp_first<StateAndTransitions>,
mp11::mp_second<StateAndTransitions>>::type;
// Convert the merged transitions into
// initializer cell constants for the dispatch table.
using initial_map_items =
mp11::mp_transform<initial_map_item,
submachines_with_forward_transitions>;
using filtered_transitions_by_state_map =
mp11::mp_fold<filtered_transition_table,
initial_map_items,
map_updater>;
public:
using merged_transitions =
mp11::mp_transform<merge_transitions,
filtered_transitions_by_state_map>;
template <typename Transition>
static process_result convert_event_and_execute(StateMachine& sm,
int region_id,
Event const& evt)
{
typename Transition::transition_event kleene_event{evt};
return Transition::execute(sm, region_id, kleene_event);
}
};
template <typename Strategy, typename NotExplicit = void>
class dispatch_impl;
template <typename NotExplicit>
class dispatch_impl<dispatch_strategy::flat_fold, NotExplicit>
: public dispatch_base
{
using base = dispatch_base;
public:
template <typename... Ts>
struct mp_indexed_dispatch_impl
{
using list = mp11::mp_list<Ts...>;
static constexpr std::size_t size = sizeof...(Ts);
template <typename F>
static constexpr process_result invoke(int state_id, F&& func)
{
return dispatch(state_id, func,
std::make_index_sequence<size>{});
}
private:
// For position I in the list, get the corresponding state_id
// at compile time.
template <std::size_t I>
static constexpr int state_id_at =
StateMachine::template get_state_id<
typename mp11::mp_at_c<list, I>::current_state_type>();
// Single case handler: invoke func with the I-th transition
template <std::size_t I, typename F>
static constexpr process_result handle_case(F& func)
{
return func(mp11::mp_at_c<list, I>{});
}
// Generate branches comparing state_id against compile-time
// state_id_at<I> for each position I in the list.
template <typename F, std::size_t... Is>
static constexpr process_result dispatch(int state_id, F& func,
std::index_sequence<Is...>)
{
process_result result = process_result::HANDLED_FALSE;
// Each branch compares the runtime state_id against the
// compile-time state_id of the I-th transition.
// State IDs may be non-contiguous (e.g., 0, 2, 5).
(void)((state_id == state_id_at<Is> &&
(result = handle_case<Is>(func), true)) || ...);
return result;
}
};
template <typename L, typename F>
static constexpr process_result mp_indexed_dispatch(int state_id, F&& func)
{
return mp11::mp_apply<mp_indexed_dispatch_impl, L>::invoke(
state_id, std::forward<F>(func));
}
static inline process_result dispatch(StateMachine& sm, int region_id,
const Event& event)
{
const int state_id = sm.m_active_state_ids[region_id];
return mp_indexed_dispatch<typename base::merged_transitions>(state_id,
[&sm, region_id, &event](auto transition) -> process_result
{
using Transition = decltype(transition);
using TransitionEvent =
typename Transition::transition_event;
if constexpr (!is_kleene_event<TransitionEvent>::value)
{
return Transition::execute(sm, region_id, event);
}
else
{
return base::template
convert_event_and_execute<Transition>(
sm, region_id, event);
}
});
}
};
template <typename NotExplicit>
class dispatch_impl<dispatch_strategy::function_pointer_array, NotExplicit>
: public dispatch_base
{
using base = dispatch_base;
public:
static process_result dispatch(
StateMachine& sm, int region_id, const Event& event)
{
const int state_id = sm.m_active_state_ids[region_id];
constexpr auto& cells = m_cells;
const cell_t cell = cells[state_id];
if (cell)
{
return cell(sm, region_id, event);
}
return process_result::HANDLED_FALSE;
}
private:
// Convert a transition to its function pointer.
template <typename Transition,
bool IsKleeneEvent = is_kleene_event<
typename Transition::transition_event>::value>
struct get_init_cell_constant_impl;
struct get_cell;
template <typename Transition>
struct get_init_cell_constant_impl<Transition, /*IsKleeneEvent=*/false>
struct get_cell<Transition, /*IsKleeneEvent=*/false>
{
using type = init_cell_constant<
// Offset into the entries array
StateMachine::template get_state_id<typename Transition::current_state_type>(),
// Address of the execute function
&Transition::execute>;
static constexpr cell_t value = &Transition::execute;
};
template <typename Transition>
struct get_init_cell_constant_impl<Transition, /*IsKleeneEvent=*/true>
struct get_cell<Transition, /*IsKleeneEvent=*/true>
{
using type = init_cell_constant<
// Offset into the entries array
StateMachine::template get_state_id<typename Transition::current_state_type>(),
// Address of the execute function
&convert_event_and_execute<Transition>>;
static constexpr cell_t value =
&base::template convert_event_and_execute<Transition>;
};
template <typename Transition>
using get_init_cell_constant = typename get_init_cell_constant_impl<Transition>::type;
static constexpr cell_t cell_v = get_cell<Transition>::value;
cell_t m_cells[max_state]{};
struct cell_table
{
cell_t data[max_state]{};
constexpr cell_t operator[](int i) const { return data[i]; }
};
// Build the cell table by traversing merged_transitions exactly once.
// Each transition knows its own state_id, so we just assign directly.
template <typename... Transitions>
static constexpr cell_table make_cells_from(mp11::mp_list<Transitions...>)
{
cell_table table{};
// Fold expression: one assignment per transition, no searching.
((table.data[
StateMachine::template get_state_id<
typename Transitions::current_state_type>()] =
get_cell<Transitions>::value), ...);
return table;
}
static constexpr cell_table m_cells =
make_cells_from(typename base::merged_transitions{});
};
// "Dispatch" for a specific event (sm-internal).
@@ -51,8 +51,6 @@ struct favor_compile_time
namespace detail
{
template <typename Policy>
struct compile_policy_impl;
template <>
struct compile_policy_impl<favor_compile_time>
{
+2 -2
View File
@@ -207,7 +207,7 @@ class state_machine_base : public FrontEnd
template <typename StateMachine>
friend struct transition_table_impl;
template <typename Policy>
template <typename Policy, typename>
friend struct detail::compile_policy_impl;
template <typename, typename, visit_mode, bool,
@@ -916,7 +916,7 @@ class state_machine_base : public FrontEnd
}
// Consider anything except "only deferred" to be a processed event.
if (result != process_result::HANDLED_DEFERRED)
if (*result != process_result::HANDLED_DEFERRED)
{
processed_events++;
if (processed_events == max_events)
@@ -25,7 +25,7 @@ struct config_tag {};
template <class T>
using is_config = std::is_same<typename T::internal::tag, config_tag>;
}
} // namespace detail
// Config for the default compile policy
// (runtime over compile time).
@@ -80,6 +80,26 @@ struct default_state_machine_config
using state_machine_config = default_state_machine_config;
} // boost::msm::backmp11
// Configuration parameters to select how events are dispatched.
namespace dispatch_strategy
{
// Generates a flat fold of inline comparison branches.
// The code can be optimized to a jump table.
// + Best executable size and runtime speed for most compilers.
// + No indirection — fully inlinable.
// - O(n) comparisons in the worst case.
struct flat_fold {};
// Generates an array of function pointers.
// + O(1) dispatch.
// + Slightly better compile times.
// - Indirect call through function pointer — not inlinable.
// - Larger executable size (one pointer per state per event type).
struct function_pointer_array {};
} // namespace dispatch_strategy
} // namespace boost::msm::backmp11
#endif // BOOST_MSM_BACKMP11_STATE_MACHINE_CONFIG_H