如何保证最佳的 Qt 状态机性能
如果您使用 Qt 进行应用程序开发并且使用状态机,那么您很可能正在使用 Qt 状态机框架。因此,您将使用普通 C++ 或 SCXML 定义状态机。另一种方法是从状态机图中生成 C++ 代码。本文比较了这些方法,同时考虑了功能、适用性和性能。
我敢打赌,作为一名软件开发人员,您已经实现了大量或多或少复杂的 switch-case 语句。这至少对我来说是正确的,而且大部分这种切换案例编码基本上只是实现不同的状态机。如果您手头只有所选的编程语言,那么这是开始编程状态机的最简单方法。虽然开始很容易,但随着状态机复杂性的增加,此类代码的可维护性越来越低。最后,您将确信您不想继续以这种方式手动实现状态机。 (顺便说一下——我假设你知道什么是状态机。)
实现状态机
有不同的替代方案来实现状态机。更好的方法之一——尤其是当你使用像 C++ 这样的面向对象的编程语言时——是应用状态模式。这种方法使用状态类,通常也使用转换类。然后通过创建状态类的实例并使用转换类的实例连接它们来定义状态机。在这种情况下,框架有助于减少代码大小和实现工作量。
Qt 状态机框架就是一个很好的例子。此 API 允许您使用紧凑代码“配置”状态机。您不必关心状态机执行语义的细节,因为框架已经实现了这些细节。您仍然需要编写代码,并且随着您的状态机变得更加复杂并且包含数十个甚至数百个状态,获得概览将变得非常困难。一张图片胜过千言万语,而众所周知的状态图概念有助于克服这种限制。 Qt 本身提供对状态图表 XML (SCXML) 的支持,这是 W3C 标准。由于手工编写 XML 并不有趣,Qt Creator 还包括一个简单的图形状态图编辑器。
不管具体的实现方法是什么,使用图形语法是编辑和理解状态机的最佳选择。这样的图形模型不仅可以由 SCXML 等语言以文本形式表示,还可以用于生成任何类型的编程语言源代码,例如普通 C++ 中的基于 switch-case 的状态机 - 或设置 QStateMachine。 使用为您进行此类转换的工具,您可以避免手写状态机代码的痛苦。它将所有三种实现方法提升到相同的可用性级别。尽管如此,实现仍然有根本的不同。这篇文章是关于比较它们的运行时行为,尤其是它们的性能。
Unsplash Photo by Austris Augusts on Unsplash
竞争对手
那么性能呢?可用的方法在所需的 CPU 周期方面有何不同?为了获得一些具体的数字,我设置了一个性能测试套件。第一部分比较了不同的实施策略。这些是竞争对手:
- SCXML 解释器——测试状态机使用 SCXML 定义并由 Qt 的 QSCXMLStateMachine 执行 类。
- 状态模式——使用QStateMachine实现测试状态机 类。
- 普通 C++ 代码 – 测试状态机由 C++ 类实现,该类应用了基于 switch-case 的基本方法。
注意:可以在此处找到这些示例的代码。
前两个变体意味着使用 Qt 概念(如信号和槽)以及 Qt 事件队列,而普通的 C++ 实现不需要这种基础结构。为了使这些方法更具可比性,测试套件包含另外两个测试场景:
- 带有信号和槽的普通 C++ 代码 - 测试状态机具有与上述相同的实现,但使用信号和槽将其集成到应用程序中。
- 带有 QEvent 的普通 C++ 代码 – 使用普通的 C++ 代码方法,但使用 Qt 事件队列处理in 然后出 事件。
这使得一方面可以比较使用信号和槽的影响以及使用 QEvents 另一方面,与普通的 C++ 实现相比,因为状态机执行代码在所有情况下都是相同的,只是包装方式不同。
测试状态机
为了测试所有五个竞争对手,我定义了图 1 所示的状态机。 1 为基本测试场景。
图 1:使用 YAKINDU 状态图工具创建的测试状态机。 (来源:作者)
测试状态机是一个简单的平面状态机。它定义了六个状态 A F 并在各州之间循环。两个输入事件 e1 和 e2 定义,交替触发状态转换。当发生状态转换时,还会执行一个简单的动作。每个转换操作只是将 10 添加到名为 x 的状态图变量 .从状态 F 的转换 A 另外提高(或发出)out 事件o .
图 2:作为 Qt Creator 中 SCXML 模型的测试状态机。 (来源:作者)
这个状态机是使用 YAKINDU Statechart Tools 定义的,它支持 SCXML 的生成。这个 SCXML 可以添加到 Qt 项目中,并且可以在 Qt Creator 中进行编辑。如图所示。 2、状态机的结构与图 2 中的状态机结构相同。 1,但一些细节,如过渡动作,在 Qt Creator 中是不可见的。 YAKINDU 状态图工具提供了更多优势,但我不会在这里讨论。
更重要的是,YAKINDU Statechart Tools 还可以生成普通的基于 switch-case 的 C++ 状态机类。它还提供了一个选项来生成带有信号和槽的启用 Qt 的类,所以这很方便。使用这个工具,我只需要使用 QStateMachine 来实现基于状态模式的状态机 用手。没有可用于该变体的代码生成器。尽管如此,我还是能够节省大量的实现工作,同时只需使用单个状态图定义即可为性能测试获得语义等效的状态机。
所有测试用例都遵循相同的方案。因为我想测量处理单个事件所需的平均时间,所以每个测试都捕获了单个状态循环的一百万次迭代。每个状态循环执行访问所有状态和处理所有转换和转换动作所需的所有事件。因此,状态循环以状态 A 开始和结束 活跃。这意味着对于每个测试用例 600 万in 事件和转换操作以及 100 万个输出 事件及其关联的转换动作被处理。测试作为命令行应用程序执行,并将迭代时间记录为单个批次。每个事件的时间消耗可以简单地通过将测量的时间除以in的数量之和来确定 事件和退出 事件。进行多次测试,选择值最低的测量结果。
这些测试是在我的旧款(2014 年中)MacBook Pro 上使用优化的代码执行的,没有调试信息,配备 Core i7 四核 CPU 2.4GHz。当然,具体的数字在不同的机器和操作系统上会有所不同。但是,这无关紧要,因为我想比较不同的实现方法。这些相对差异在不同的硬件和操作系统平台上具有可比性。
让我们来看看性能数据
是的 - 我认为几乎每个人都期望一个简单的 C++ 实现比其他替代方案更快,但差异的幅度确实令人震惊。
图 3:单事件处理时间比较。 (来源:作者)
使用普通 C++ 处理单个事件平均需要 7 纳秒。使用 SCXML 需要 33,850 纳秒——这是大约 4800 的一个因数,而且是一个巨大的差异!相比之下,光传播超过 10 公里,而 SCXML 状态机只处理一次转换,而普通 C++ 状态机中的相同转换只留下了光传播超过 2 米的时间。这意味着 CPU 周期和能耗的数量级非常不同。
当然,具体数字取决于机器和所使用的测试程序。我稍后会讨论这个话题。但让我们先讨论其他数字。前三个测试场景都包含相同的状态转换逻辑,该逻辑由 YAKINDU Statechart Tools 生成,但每个场景以不同的方式包装。
使用直接连接时,使用信号和槽处理事件平均需要 72 纳秒。因此,与实际的状态机逻辑相比,这种机制施加的最小开销约为 90%。在这一点上,我不想争论使用信号和插槽会使应用程序变慢。相反,我宁愿声称状态机的纯代码实现非常快 .
将此与第三种情况进行比较,可以很好地了解使用事件队列造成的性能开销。在这种情况下,所有状态图事件都通过事件队列路由。每个事件需要 731 纳秒,与信号和槽相比需要约 10 倍,与普通 C++ 相比需要约 100 倍。
我们可以假设类似的开销也适用于其他两个场景“plain QStateMachine ”和“SCXML 状态机”——它们都需要一个活动的事件队列。因此,当从每个事件的 5200ns 中减去假设的事件队列开销时,我们得到QStateMachine 的粗略时间消耗 每个事件 4500ns 的框架。与纯代码方法相比,基于 QStateMachine 的状态机实现速度较慢。 与普通的 C++ 代码实现相比,这是一个大约 635 的因数。
最后,让我们看看 SCXML 解释器。它涉及解释 JavaScript 代码并添加另一个约 7 的因素。与纯代码方法相比,基于 SCXML 的状态机实现速度非常慢。
分层和正交状态机怎么样?
到目前为止,我只是描述了一个简单的平面状态机。但是状态图提供了更多的特征,两个最重要的结构特征是层次结构和正交性。那么,这些特性的使用对状态机运行时有什么影响?
首先,为了衡量层次结构的影响,我定义了要分析的状态机的层次结构变体,如图 2 所示。 4.
图 4:分层测试状态图。 (来源:作者)
它提供与平面状态机完全相同的行为,但增加了一些复合状态。保持功能相同,但只是改变结构,可以找出结构变体意味着多少开销(如果有的话)。
其次,为了测量正交性的影响,我以四个正交区域的形式复制了平面状态机。它们都具有完全相同的功能。因此,生成的状态机(见图 5)将完成四倍于简单状态机的工作。
图 5:正交测试状态图。 (来源:作者)
对于分析,我选择了普通的 C++ 和 SCXML 实现,因为它们是最快和最慢的变体。图中的图。图6显示结果。非常令人鼓舞的是,在状态图中使用层次结构在两种实现变体中都没有任何可衡量的性能影响。
图 6:层次结构和正交性的性能影响。 (来源:作者)
另一个积极的结果是使用正交性也没有任何负面影响。相反,虽然人们可能预计至少需要四倍的处理时间才能完成四倍的工作,但运行时间的有效增加(因子 ~2.4 和 ~3.1)明显小于 4。
为什么会这样?这样做的原因是状态机处理有一个通用部分,它独立于单个状态和事件的处理。对于普通 C++ 状态机,这部分需要 52%(或每个事件 3.5ns),而 SCXML 为 28%(或每个事件 9300ns)。最后,与 SCXML 相比,使用生成的 C++ 代码时正交状态的影响较小。
结论
到目前为止,普通 C++ 比所有替代方案都更有效率。使用信号和槽或 Qt 事件队列是框架机制,可以简化实现和维护复杂状态机应用程序。 Qt 状态机框架需要这两种机制。使用生成的 C++ 代码,您可以选择。
在很多场景下,尤其是交互场景下,即使是 SCXML 状态机也足够快,它们可以通过在运行时切换状态图定义来使行为可配置,从而提供更大的灵活性。
嵌入式