如何保证Qt状态机的最佳性能

如何保证Qt状态机的最佳性能

How to ensure the best Qt state machine performance

如果您使用Qt进行应用程序开发,并且使用状态机,那么很可能您正在使用Qt状态机框架。因此,您将使用普通C++或SCXML定义状态机。另一种方法是从状态机图生成C++代码。本文比较了这些方法,并将功能性、适用性和性能考虑在内。             

我敢打赌,作为一个软件开发人员,您已经实现了大量或多或少复杂的switch case语句。这至少对我来说是正确的,而且这种交换情况编码基本上只是实现不同的状态机。如果手头除了您选择的编程语言之外没有其他任何东西,那么这是启动状态机编程的最简单方法。虽然开始很容易,但是随着状态机复杂性的增加,这样的代码变得越来越不可维护。最后,您将确信您不希望继续以这种方式手动实现状态机。(顺便说一句,我假设您知道什么是状态机。)             

实现状态机             

有不同的方法来实现状态机。其中一个更好的方法——尤其是当你使用像C++这样的面向对象编程语言——正在应用状态模式。这种方法使用状态类,通常也使用转换类。然后,通过创建状态类的实例并使用转换类的实例连接它们来定义状态机。在这种情况下,框架有助于减少代码大小和实现工作量。             

Qt状态机框架就是一个很好的例子。这个API允许您使用紧凑代码“配置”状态机。您不必关心状态机执行语义的细节,因为框架已经实现了这些细节。您仍然需要编写代码,并且随着状态机变得越来越复杂,并且包含几十个甚至数百个状态,因此很难获得概述。一幅图胜过千言万语,众所周知的状态图概念有助于克服这一限制。Qt本身提供了对状态图XML(SCXML)的支持,SCXML是W3C标准。由于手工编写XML并不有趣,Qt-Creator还包含一个简单的图形状态图编辑器。             

不管具体的实现方法如何,使用图形语法是编辑和理解状态机的最佳选择。这样的图形模型不仅可以用像SCXML这样的语言来表示,还可以用来生成任何类型的编程语言源代码,比如基于C++的基于实例的状态机,或者C++代码,它建立QSTATEMACHEN的实例。使用一个可以为您进行这种转换的工具,您可以避免手工编写状态机代码的痛苦。它将所有三种实现方法提升到相同的可用性级别。尽管如此,实现仍然是根本不同的。本文将比较它们的运行时行为,特别是它们的性能。

竞争对手             

那么性能呢?关于所需的CPU周期,可用的方法有什么不同?为了得到一些具体的数字,我建立了一个性能测试套件。第一部分比较了不同的实施策略。以下是竞争对手:             

SCXML解释器–测试状态机是使用SCXML定义的,并由Qt的QSCXMLStateMachine类执行。             

状态模式–测试状态机是使用QStateMachine类实现的。             

普通C++代码——测试状态机是由C++类实现的,该类应用了一个基本的基于交换机实例的方法。             

注意:这些例子的代码可以在这里找到。             

前两个变体意味着使用QT概念,如信号和时隙以及QT事件队列的使用,而普通C++实现不需要这种基础结构。为了使这些方法更具可比性,测试套件还包括两个测试场景:             

带有信号和槽的普通C++代码——测试状态机具有如上所述的相同实现,但使用信号和时隙将其集成到应用程序中。             

带有QQuebug的普通C++代码使用普通C++代码方法,但使用QT事件队列来处理输入和输出事件。             

这使得有可能比较信号和时隙的使用一方面和使用QQueS的影响,另一方面与普通C++实现相比,因为状态机执行代码在所有情况下都是相同的,只是只是不同地包装。             

测试状态机             

为了测试所有五个竞争对手,我为基本测试场景定义了图1所示的状态机。

Figure 1: The test state machine, as created with YAKINDU Statechart Tools.

测试状态机是一个简单的平面状态机。它定义了六个状态A到F,并循环这些状态。定义了两个输入事件e1和e2,它们交替触发状态转换。当状态转换发生时,也会执行一个简单的操作。每个转换操作只需向名为x的statechart变量添加10。从状态F到a的转换额外引发(或发出)out事件o。

 

 Figure 2: The test state machine as an SCXML model in Qt Creator. 

这个状态机是使用支持SCXML生成的yakindustatechart工具定义的。这个SCXML可以添加到Qt项目中,并且可以在Qt Creator中进行编辑。如图2所示,状态机的结构与图1中的结构相同,但有些细节,如转换操作,在Qt Creator中不可见。YAKINDU状态图工具提供了更多的优势,但我在这里不讨论它们。             

更重要的是,YakDu StuteCARS工具也可以生成基于C++的状态机类的简单的基于交换的实例。它还提供了一个选项,用信号和插槽生成支持Qt的类,因此这很方便。使用这个工具,我只需要手工使用QStateMachine实现基于状态模式的状态机。没有可用于该变体的代码生成器。尽管如此,我还是能够节省大量的实现工作,同时只需使用一个状态图定义就可以为性能测试获得语义上等价的状态机。             

所有测试用例都遵循相同的方案。当我想测量处理单个事件所花费的平均时间时,每个测试捕获了单个状态循环的一百万次迭代。每个状态循环执行访问所有状态和处理所有转换和转换操作所需的所有事件。所以,一个状态循环开始和结束,状态a处于活动状态。这意味着,对于每个测试用例,将处理600万个in事件和transition操作,以及一百万个out事件及其关联的转换操作。测试作为命令行应用程序执行,并将迭代的时间记录为单个批处理。每个事件的时间消耗可以简单地通过将测量的时间除以in事件和out事件的数量之和来确定。进行多次试验,并选择最低值的测量结果。             

测试是在我的旧版(2014年年中)MacBookPro上使用优化的代码执行的,没有调试信息,CoreI7四核CPU为2.4GHz。当然,具体数字在不同的机器和操作系统上会有所不同。但是,这并不相关,因为我想比较一下不同的实现方法。这些相对差异在不同的硬件和操作系统平台上是可以比较的。             

让我们看看性能数据             

是的——我想几乎所有人都会期望一个简单的C++实现比其他的选择要快,但是差异的大小确实令人震惊。

Figure 3: Single event processing time compared.

使用普通C++处理单个事件平均花费7纳秒。使用SCXML需要33850纳秒——这是一个大约4800纳秒的系数,这是一个巨大的差异!为了比较,光传播更多的10公里,而SCXML状态机只处理一个过渡,而在平原C++状态机中的相同的转换只留下了太多的时间光可以超过2米。这意味着CPU周期和能量消耗的数量级非常不同。             

当然,具体数字取决于机器和使用的测试程序。我稍后再讨论这个话题。但让我们先讨论其他数字。前三个测试场景都包含一个完全相同的状态转换逻辑,它是由YAKINDU Statechart工具生成的,但是每一个都以不同的方式包装起来。             

在使用直接连接时,使用信号和插槽来处理事件平均需要72ns。因此,与实际的状态机逻辑相比,这种机制的开销最小为90%。在这一点上,我不想争论使用信号和插槽会使应用程序变慢。相反,我宁愿声明状态机的纯代码实现非常快。             

将其与第三个场景进行比较,可以很好地了解使用事件队列所造成的性能开销。在这个场景中,所有状态图事件都通过事件队列进行路由。与73NS每个事件,它需要一个因素10,对应信号和槽,100对应普通的C++。             

我们可以假设类似的开销也适用于其他两个场景“普通QStateMachine”和“SCXML state machine”—它们都需要活动事件队列。因此,当假设的事件队列开销从每个事件的5200ns中减去后,我们得到QStateMachine框架的粗略时间消耗,即每个事件4500ns。与普通代码方法相比,基于QStateMachine的状态机实现是慢点。这个与普通C++代码实现相比,是一个大约635的因素。             

最后,让我们看看SCXML解释器。它涉及到解释JavaScript代码,并添加了另一个因子~7。与纯代码方法相比,基于SCXML的状态机实现非常慢。              

分层和正交状态机呢?             

到目前为止,我只分析了一个简单的平面状态机。但是状态图提供了更多的特性,两个最重要的结构特征是层次性和正交性。那么,这些特性的使用对状态机运行时有什么影响呢?             

首先,为了度量层次结构的影响,我定义了一个要分析的状态机的层次变体,如图4所示。

Figure 4: Hierarchical test statechart. 

它提供了与平面状态机完全相同的行为,但是添加了一些复合状态。保持功能相同,但只需改变结构,就可以知道结构变量意味着多少开销(如果有的话)。             

其次,为了测量正交性的影响,我以四个正交区域的形式复制了平面状态机。它们都有完全相同的功能。因此,得到的状态机(见图5)所做的工作将是简单状态机的四倍。

Figure 5: Orthogonal test statechart. 

对于概要文件,我选择了普通C++和SCXML实现,因为它们是最快和最慢的变体。图6中的图表显示了结果。非常令人鼓舞的是,在状态图中使用层次结构并不会对两种实现变体产生任何可测量的性能影响。

Figure 6: Performance impact of hierarchies and orthogonality. 

另一个积极的结果是使用正交性也没有任何负面影响。相反,虽然人们可能期望至少四倍的处理时间来完成四倍的工作,但在运行时的有效增加系数~2.4和~3.1显著小于4。              为什么会这样?原因是状态机处理有一个一般的部分,它独立于单个状态和事件的处理。这部分使用52%(或3.5Ns每个事件)的平原C++状态机,相比之下28%(或9300 NS每个事件)SCXML。最后,当使用生成的C++代码时,正交状态与SCXML相比影响较小。             

结论              

普通C++比所有的替代方案都要有效得多。使用信号和插槽或Qt事件队列是一种框架机制,可以简化复杂状态机应用程序的实现和维护。Qt状态机框架需要这两种机制。使用生成的C++代码,你可以选择。             

在许多场景中,特别是交互场景中,即使是SCXML状态机也足够快,它们可以通过在运行时切换状态图定义使行为可配置,从而提供更大的灵活性。

posted @ 2020-07-06 19:22  吴建明wujianming  阅读(725)  评论(0编辑  收藏  举报