编写高性能的C程序的时候经常需要设计状态机,通过设计状态机可以完成复杂系统的业务逻辑编程实现,状态机可以管理状态和转换,使代码更容易理解和维护,不过很多时候,大家编写的状态机程序都存在一些性能问题,本文聚焦高性能C语言状态机的实现进行说明
状态机也称有限状态机(Finite State Machine, FSM),其最初是一种数学模型,用于描述系统在不同状态之间根据输入进行转换的行为。
假设需要设计一个开关灯的程序,可以通过如下方式来说明状态机
+------------------+ | | v Toggle | +------+ +------+ | Off | -------->| On | +------+ <--------+------+ ^ | | Toggle | +------------------+
根据上面这个最简单的状态机可以总结如下几个必要的要素
针对这些必要条件,下面列出高性能状态机的几个细节,基于C语言
高性能的状态机的必要添加之一就是枚举,通过枚举来实现状态集和相比于代码中的幻数而言是最轻松最容易管理的。 一个简单的状态集和可以如下
typedef enum { STATE_INIT, STATE_RUNNING, STATE_STOPPED, STATE_ERROR } State; State currentState = STATE_INIT;
大部分不关注性能的状态机喜欢使用if来进行判断,例如
if(state == STATE_INIT) { # TODO } else if(state == STATE_RUNNING) { # TODO }
但是if通常对于cpu的预测而言并不友好,这样在处理状态转换时候会有极大性能损失。可以考虑使用switch-case结构。这种方法通常更快更高效。编译器在处理switch语句上具有最高的性能
void handleState(State state) { switch (state) { case STATE_INIT: // Initialization logic break; case STATE_RUNNING: // Running logic break; case STATE_STOPPED: // Stopped logic break; case STATE_ERROR: // Error handling break; default: // Handle unexpected state break; } }
额外信息:有兴趣的可以了解 达夫设备:https://www.gnu.org/software/c-intro-and-ref/manual/html_node/Duffs-Device.html
switch已经满足绝大部分情况下的状态转移,如果追求极高的性能,可以直接避免编译器做任何的分支预测,直接使用函数指针代替分支预测形式的语法。例如
void initState() { // Initialization logic } void runningState() { // Running logic } void stoppedState() { // Stopped logic } void errorState() { // Error handling } void (*stateHandlers[])() = { initState, runningState, stoppedState, errorState }; void handleState(State state) { stateHandlers[state](); }
通过 handleState函数直接调用函数指针,代码在运行的时候,直接可以通过state的局部变量参数进行函数指针的偏移,从而直接定位到确定的函数调用上。
不过需要注意的是,绝大部分情况下,switch-case已经是最高效的状态机设计方案了。如果状态机状态集和过多,函数指针的优势才能体现。所以选择switch-case还是函数指针并没有什么约束,看个人爱好
状态机内部的状态转移经常容易出现重复状态的转移,这种很容易变成不轻易被发现的性能问题,当任务从STATE_RUNNING切换到STATE_RUNNING时,状态转移函数应该准确的识别,避免不必要的性能消耗,如下
void transitionTo(State newState) { if (currentState != newState) { currentState = newState; // Additional logic for state change } }
状态集和的状态,尽可能的时候位处理的方式实现,通过一个整数来表达多个状态,是内存友好型的行为。linux内核经常这么使用,如下
#define STATE_INIT (1 << 0) #define STATE_RUNNING (1 << 1) #define STATE_STOPPED (1 << 2) #define STATE_ERROR (1 << 3) int currentStateFlags = 0; void setState(int state) { currentStateFlags |= state; } void clearState(int state) { currentStateFlags &= ~state; } bool isStateActive(int state) { return (currentStateFlags & state) != 0; }
使用动态内存申请的方式获取数据结构会导致任务的高性能依赖操作系统的内存管理,如果需要开发极高性能的程序状态机,此时建议直接禁止动态内存申请。全部改用静态内存分配。
通过静态分配内存,可以有效的避免动态分配的开销和抖动。
但是另一方面又需要注意的是,一些非状态机外的场景,使用动态内存可以有效的节省内存使用。此情况额外分析。这里只谈涉及高性能状态机内的场景
typedef struct { State state; // Other state-related data } StateMachine; StateMachine sm = {STATE_INIT};
通过使用枚举、优化过渡逻辑、利用函数指针、最小化状态变化以及有效管理内存,可以显著提升状态机的性能。
如果自己在编写状态机时需要抄作业,下面提供了一个开源的高效状态机的仓库,可以直接搬运。