摘要
动态偏序约简(DPOR)通过探索并发程序的所有交错直至某种等价关系(例如 Mazurkiewicz 迹等价)来验证并发程序。这样做涉及空间和时间之间的复杂权衡。现有的 DPOR 算法要么是探索最优的(即仅精确探索每个等价类的交错),但可能使用程序大小的指数内存,要么保持多项式内存消耗,但可能会探索指数级的许多冗余交错。
在本文中,我们表明可以两全其美:在线性内存消耗的情况下探索每个等价类的确切一个交错。我们的算法 TruSt 在 Coq 中形式化,不仅适用于顺序一致性,还适用于满足一些基本假设的任何弱内存模型,包括 TSO、PSO 和 RC11。此外,TruSt 的可并行性令人尴尬:它的不同探索选项没有共享状态,因此可以完全并行探索。因此,TruSt 在内存和/或时间方面优于最先进的技术。
作者:
- Michalis Kokologiannakis, MPI-SWS, Germany
- Iason Marmanis, MPI-SWS, Germany
- Vladimir Gladstein, MPI-SWS, Germany and St Petersburg University/JetBrains Research, Russia
- Viktor Vafeiadis, MPI-SWS, Germany
相关文章:https://zhuanlan.zhihu.com/p/446927002
INTRODUCTION
无状态模型检查(SMC)[Godefroid 1997; Musuvathi et al. 2008] 是一种用于验证有限尺寸的并发程序的有效验证技术。它是作为显式状态模型检查的替代方案而引入的,可以避免过多的内存消耗,因此有可能扩展到更大的程序。SMC 的工作原理是系统地探索给定并发程序的所有执行,而不存储它已经访问过的程序状态集。
虽然 SMC 允许根据多项式内存要求来验证程序,但它有一个明显的缺点,即要探索的执行次数通常与程序大小成指数关系。因此,SMC 几乎总是与巧妙的动态偏序约简(DPOR)算法一起使用 [Abdulla et al. 2014; Flanagan et al. 2005; Kokologiannakis et al. 2019] 减少了需要探索的执行次数,以覆盖所有可能的程序行为。DPOR 将程序的执行跟踪根据某些关系分为等效类,例如 Mazurkiewicz 跟踪等价 [Mazurkiewicz 1987],所有等效路径都表现出相同的可观察结果。然后,为了验证程序,只需从每个等价类中探索一条路径就足够了。
然而,探索每个等价类的一个代表性执行轨迹(即最优)并不容易。 DPOR 算法(例如,[Abdulla et al 2015; 2014; 2016; Albert et al 2017; 2018; Chalupa et al 2017; Chatterjee et al 2019; Flanagan et al 2005; Kokologiannakis et al 2019; Zhang et al 2015])通常开始通过探索一个程序路径,每当他们检测到一对竞争的的访问时,他们就会以相反的顺序探索包含竞争的访问的其他跟踪,同时还保持某种状态以避免重新探索等效的执行跟踪。然而,现有算法要么不是最优的,这意味着即使对于具有
在本文中,我们表明这种妥协是不必要的。我们开发了一种具有线性内存需求的探索最优 DPOR 算法。除了在时间和内存方面为 DPOR 提供可扩展的解决方案外,我们的算法,TruSt(Truly Stateless Model-checker)还具有另外两个大优势:
- 这两项内容是参数化的:(1) 内存模型的选择,不仅支持顺序一致性 (SC) [Lamport 1979],还支持各种弱内存模型,例如 TSO [Sewell et al 2010]、PSO [SPARC International Inc. 1994] 和 RC11 [Lahav et al 2017],以及 (2) DPOR 等价关系的选择,支持 Shasha-Snir 等价 [Shasha et al 1988](Mazurkiewicz 等价于弱内存模型)和读取等效性 [Chalupa et al 2017]。
- 它对不同执行跟踪的探索绝对不共享任何状态,这意味着 TruSt 的并行性令人尴尬。这与现有的 DPOR 解决方案形成鲜明对比,后者的并行化需要共享以避免重复(例如,[Lång et al 2020])。
为了实现这一目标,我们以之前的工作为基础——尤其是 GenMC [Kokologiannakis et al 2019]——并将程序执行表示为执行图 [Alglave et al 2014]。然而,我们从根本上改变了 DPOR 逆转恶意访问的条件。具体来说,我们限制只有当要删除的事件形成剩余执行的最大扩展时才发生反转——这是我们在本文中引入的一个新颖概念。我们证明最大扩展总是存在并且是唯一的,因此人们可以在不记录任何附加状态的情况下实现正确性和最优性。线性空间复杂度源于 TruSt 以递归深度优先的方式探索执行:递归深度受程序大小限制,每次递归调用都需要常数空间来表示其与当前执行图的差异,即大小也是线性的。
总之,我们做出以下贡献:
通过一系列示例,我们描述了现有 DPOR 解决方案的工作原理以及为什么保持最优性和多项式空间复杂性会带来相当大的挑战。 我们提供了一个直观的说明,说明 TruSt 如何通过将程序执行表示为图形并使用极大值概念限制替代探索来克服这一挑战。我们的算法适用于任何受一些基本假设限制的内存模型。 我们详细描述我们的算法,并证明(在 Coq 中)它是合理的、完整的和最优的。 我们将 TruSt 的并行版本实现为验证 C/C++ 程序的工具。 我们证明 TruSt 在内存和/或验证时间方面优于最先进的技术,并且它在多核机器上的扩展性非常好。
SMC & DPOR: A TROUBLED MARRIAGE
首先,为什么 SMC/DPOR 会遭受这种探索/内存权衡的困扰?为了回答这个问题,让我们首先回顾一下 DPOR 的基本原理。为了简化演示,在本节的其余部分中,我们假设顺序一致性(SC)的设置,并且使用的等价概念是 Mazurkiewicz 等价概念。在
DPOR in a Nutshell
正如简要提到的,DPOR 首先探索一个完整的程序交织,然后进入竞争检测阶段,当它识别出需要探索的进一步交织时。具体来说,它检测竞争的转换,如果以相反的顺序执行,将导致属于不同等价类的交错。然后,DPOR 以深度优先的方式一一探索这些替代方案。(在探索每个交错之后,运行竞争检测阶段,该阶段可以递归地生成要探索的进一步交错等等)
我们用下面的
该程序有 12 个交错,分为 4 个 Mazurkiewicz 等价类。
TODO:上图中,红色的集合是睡眠集,蓝色的集合是每个转换的回溯集。
基本思想:交换竞争的转换(操作),以寻找新的路径
现在让我们看看 DPOR 如何生成该程序的所有四个 Mazurkiewicz 迹线。从空跟踪开始,DPOR 添加与程序转换相对应的事件,直到到达完整跟踪 ①,如图 1 所示。在此过程中,它会跟踪回溯集中每个步骤执行的转换,稍后它将记录任何替代探索选项。
一旦第一个跟踪被完全探索,DPOR 就会启动竞争检测(
回到
现在竞争检测阶段已经完成,DPOR 以深度优先的方式考虑替代探索选项。它通过将跟踪限制为仅包含在该转换之前触发的事件,然后触发尚未探索的转换,来定位其回溯集中存在未探索的转换的最新转换,并回溯(
此时,构建了第二个完整迹线(迹线 ②),并且 DPOR 再次启动竞争检测阶段。在此阶段中,检测到两个竞争:一个在转换 (1) 和 (3) 之间,一个在 (2) 和 (4) 之间。然而,这两个竞争都不会对后道设置产生任何影响。就第一个竞争而言,这几乎是预料之中的,因为 (3) 已经处于初始过渡的回溯组中。然而,也许令人惊讶的是,DPOR 也不会填充第三次转换的回溯集,尽管事实上 (2) 不属于那里的回溯集。原因是 (2) 处于该转换的睡眠集中。这表明这将“重新反转”正在反转的竞争(导致相当于迹线 ① 的跟踪),因此 DPOR 避免尝试一起反转第二个竞争。
随后,DPOR 进一步回溯(参见图 2)并探索第一次过渡的替代方案。由于已经探索了针对此转换触发 (1),因此将 (1) 插入睡眠集,并触发 (3)。此时,(1) 立即从睡眠集中移除,因为它正在与 (3) 进行竞赛。然后,DPOR 可以继续使用 (1)、(2) 和 (4) 中的任何一个,但我们假设它首先选择 (4),然后继续使用 (1) 和 (2),最终导致迹线 ③。
继续进行现在应该熟悉的竞争检测阶段,DPOR 注意到 (2) 与 (4) 处于数据竞争状态,因此将 (2) 插入第二个转换的回溯集中。
最后,算法回溯到第二个转换并触发 (2) 而不是 (4)。然后加上 (1) 和 (4) ,得到迹线 ④。与之前一样,④ 的竞争检测阶段不会向回溯集添加任何新的转换,从而结束了对该程序的所有四个不同 Mazurkiewicz 等价类的代表性交错的探索。
笔者将整个 DPOR 的搜索的过程用下图和伪代码表示。
1
2
3
4
5
6
7
8
9
10
sleep_set;
traceback_set[];
procedure DPOR_DFS(u) {
find_valid_path();
race_detection();
}
procedure race_detection {
// add racy transitions into each traceback_set above
}
The Race-Reversal Problem
到目前为止,DPOR 有一个很大的缺点:竞争逆转的方式不是预先确定的。当 DPOR 检测到两个事件 a 和 b 之间存在竞争且 a 在跟踪中出现在 b 之前时,它需要填充在 a 位置设置的回溯集。然而,如果 b 在 a 的位置不可触发(例如,因为它因果上取决于 a 之后添加的一些其他事件,这里所说的因果取决于具体使用的 DPOR),则会添加一个不同的事件。事实上,我们已经看到了这个问题的一个例子:在
一般来说,DPOR 使用出现在跟踪中 a 和 b 之间的事件 c 填充在 a 处设置的回溯,并且不因果依赖于任何其他事件。当然,虽然将 b 本身(或来自同一线程的 b 的前驱)添加到 a 的回溯集确实有意义,但这并不总是可能的,因为 b 可能依赖于跨越许多不同线程的多个其他事件。
竞争逆转机制中的这种非确定性有时会导致 DPOR 遇到执行阻塞。要查看这样的示例,请再次考虑
TODO:在上一段的示例中,(2) 和 (1) 并不竞争,为什么要把 (2) 添加到第一个转换的回溯集中?
反过来,虽然对迹线 ②、③ 和 ④ 的探索与
更一般地说,即使在这个示例中,基本 DPOR 算法仅探索一个阻塞执行,但也存在具有线性数量的 Mazurkiewicz 等价类的参数程序,其中 DPOR 探索指数级的许多阻塞执行 [Abdulla et al 2017; Nguyen et al. 2018]。以最佳方式解决上述竞争逆转问题(即,仅探索每个 Mazurkiewicz 等价类的一条迹线)是非常困难的。而且,即使有 DPOR 算法能够实现这种最优性 [Abdulla et al 2017; 2019; 2018; Aronis et al. 2018; Kokologiannakis et al. 2019; 2020],正如我们很快就会看到的,所有此类解决方案都会遭受指数级的内存消耗。
Exponential-Cost Solutions to the Race-Reversal Problem
但现有的 DPOR 解决方案如何真正实现最优呢?这些技术背后的关键思想是不仅保存回溯集中的转换,而且保存转换序列。因此,每当 DPOR 试图逆转两个事件 a 和 b 之间的竞争(a 在跟踪中较早)时,它不必猜测应该将哪个转换添加到在 a 位置设置的回溯中;相反,它可以简单地添加一个包含 b 及其所有因果前身的序列,以便 a 和 b 之间的竞争精确逆转。通过这样做,它还可以摆脱睡眠集,该睡眠集仅用于不过早地触发转换(即在竞争逆转之前)。
作为一个具体的例子,让我们简要讨论 Abdulla 等人 [2014] 的最优 DPOR 算法的探索过程在验证
虽然保存转换序列足以保证最优性,但它可能导致指数内存消耗 [Abdulla et al 2014; Nguyen et al. 2018]。要查看这方面的示例,请考虑下面的 exp-mem 程序。
该程序的问题在于,指数数量的转换序列将存储在第一个 x 访问的回溯集中,并且只有在 y 访问上的所有竞争都反转后才会探索这些序列。具体地,假设最优 DPOR 算法通过按从左到右的顺序执行程序线程来获得程序的第一条踪迹。在该跟踪的竞争检测阶段,由于两个 x 访问之间的竞争,第一个 x 访问的回溯集将填充转换序列 s。由于第二次 x 访问因果关系取决于之前对 y 的所有访问,因此 s 将包含第一次访问 x 后触发的所有转换。
当然,s 的大小不是问题;然而,问题是 DPOR 还必须逆转 y 上的所有竞争。而且,更糟糕的是,对于每个反转,另一个(新的)转换序列将被插入到第一个 x 访问的回溯集中,因为它将与所有现有的 y 访问顺序不同。鉴于有
TruSt: RECONCILING SMC & DPOR
在本节中,我们将描述 TruSt,这是我们的 DPOR 框架,它结合了三个关键功能:(1) 内存模型参数性、(2) 最优性和 (3) 多项式内存要求。在以下小节中,我们将逐步描述 TruSt 如何实现这些功能。为了避免混淆,我们对 TruSt 的每个“中间”版本使用不同的索引(即 TruSt~0~、TruSt~1~),直到我们在
TruSt~0~: Representing Executions as Graphs
使用执行图(execution graphs)而不是交错(interleavings)来表示执行。执行图本身是一个 DAG,包括
分别对应数据库执行历史中的 WW、WR 和 Commit Order 关系。这一算法的优点是不需要为无竞争关系的事件确定相对顺序。
为了支持弱内存模型,我们不能简单地将程序执行表示为交错,因为我们必须考虑此类模型允许的可能的重新排序。因此,我们将并发程序的执行表示为执行图 [Alglave et al 2014],其中包含一组事件以及它们之间的一些关系。
我们对执行图的定义与文献中的大多数演示有两个不同之处。首先,它包含一个附加组件:总顺序
我们将因果依赖关系
然后,程序 P 在内存模型 m 下的语义由满足 m 的一致性谓词的程序对应的执行图集合给出。一致性通常确保读取返回相对较新的值,并且写入的一致性顺序与程序顺序不矛盾。
作为示例,SC 下
在显示图表时,我们遵循弱内存文献中的标准约定,将
请注意,执行图包含 DPOR 使用的“等价类”概念。事实上,如图 4 所示,
TruSt 可以为任何满足三个基本假设的内存模型
- 格式良好(Well-formedness):一致性不取决于事件添加到图中的顺序(即,如果
一致,则任何图 也一致),并且,一致的图, 应该是无环的(即事件不能循环依赖于自身)。 - 前缀封闭性(Well-formedness):将一致图限制为其事件的任何
-prefix-close 子集会产生得到的图也是一致的(即,若 G 是一致的,则 G 的任何前缀也是一致的)。前缀封闭性使 TruSt 能够增量地构建一致的图。 - 最大可扩展性(Maximal-extensibility:):如果以最大方式添加
-maximal 事件到一致图,则可以保持一致性:对于写入来说是 -maximally,对于读取来说是从 -maximally 写入中读取(关于 maximal 的定义,参考 )。直观上,如果线程有更多语句要执行,则执行程序永远不会被卡住:其余语句始终可以使用 SC 语义执行(特别是,每次读取都可以返回由最近的同一位置写入写入的值)。
TruSt~0~: Partially Alleviating the Race-Reversal Problem
执行图使 TruSt~0~ 能够执行两项关键优化,这两项优化共同使其能够以最佳方式扭转超过 50% 的竞争。
第一个优化源于观察到 DPOR 的竞争检测阶段不需要在执行结束时发生,而是可以在每次将新事件添加到图中时增量执行。因此,每当将事件 b 添加到图中时,只需检查它是否与图中 G 中已有的某个事件 a 冲突即可。给定图形表示,这样的检查是非常自然的,因为如果 b 是写入或读取事件,TruSt~0~ 无论如何都必须确定 G 中的最后一个事件相同位置写入,以便分别更新
第二个优化在逆转竞争时利用写入的语义:即,写入仅影响可以从内存读取的值,而不影响任何程序线程的本地状态。因此,如果我们在写入事件 a 和稍后的事件 b 之间存在竞争,我们不需要从执行图中删除任何事件。如果 b 是读事件,则改变
下图中,紫色的集合是重访集。
是 forward revisit 的缩写。相比于基于交织的 DPOR,这一回溯的过程是:
- 不会删除两个活跃事件之间添加的任何事件;
- 从当前 R 事件的重访集修改对应的
关系,使之从其他 W 那里读取。
我们在
由于第一张图(图中 ①)已完成,TruSt~0~ 以深度优先的方式回溯并探索任何记录的替代选项。然而,TruSt~0~ 的回溯过程与基于交织的 DPOR 不同。当回溯以便
其余的探索以类似的方式进行。执行 ② 完成后,TruSt~0~ 重新访问
TruSt~0~ 能够以这种优化方式回溯的原因在于它检查一致性的方式。事实上,正是因为一致性不依赖于跟踪中不同事件的顺序,TruSt~0~ 能够将冲突的读取和写入保留在图中,并且仅通过更改读取的
TruSt~0~: The Backward Revisit Problem
下图中
是 backward revisits 的缩写。有些较早的读取(如 )和较晚的写入(如 )之间的事件需要删除。这一回溯过程是:
- 限制图表仅包含
之前添加的事件,以及 的 前缀,即 本身; - 添加其他事件,完成执行。
我们刚刚看到了 TruSt~0~ 如何通过更新执行图而不删除任何中间事件来解决
我们再次使用
完成完整执行后,TruSt~0~ 以深度优先的方式探索记录的重访。因此,它从
然后它添加
接下来,算法继续
直观地,我们可以想象由从写入 w
虽然保存前缀避免了 TruSt~0~ 的睡眠集等构造,但仅靠它还不足以消除重新访问集的需要。要了解这一点,请考虑
评论。事实上,在从图中删除相应的读取之前,重访集的任何项目都不能从集合中删除,如图 7 中的示例所示,为了简洁起见,我们不记录两次写入 x 之间的
TruSt~1~: Achieving Optimality via Maximal Extensions
TruSt 通过确保特定的
最大扩展背后的关键思想很简单。考虑由于写事件 w
现在让我们将这种直觉形式化。我们说写事件
定义 3.3(最大扩展)。执行图 G 是从
到 的潜在 的最大扩展,如果每个 使得 和 被最大添加在 w 之前。
上述定义与上面的直观描述密切相关,因此让我们在牢记上述解释的同时详细了解它。首先,请注意事件 e 的
现在让我们看一个示例,说明 TruSt~1~ 如何避免两次考虑相同的
理解为什么 TruSt~1~ 不应该在执行 2 中重新访问
正如我们很快就会看到的 (
TruSt: From Exponential to Linear Memory Requirements
尽管最大路径渲染重访集已过时(从某种意义上说,TruSt~1~ 不需要记住已
现在让我们看看 TruSt 的最终版本如何扩展 TruSt~1~ 来解决上述问题。TruSt 的解决方案是热切地探索所有重访。也就是说,每当添加事件 a 时,TruSt 都会执行本地“竞争检测”阶段(即,如果 a 是读取,则查找
由于执行的
通过巧妙的数据结构和更仔细的计算,我们可以将 TruSt 的内存需求降低到
现在让我们将所有内容放在一起,看看 TruSt 如何验证不同的程序,即下面的
该程序在 SC 下有 6 个执行,可以在图 8 的探索过程的叶节点处看到(假设 TruSt 从左到右探索)。
由于 TruSt 急切地执行重访,因此当 TruSt 第一次遇到
TruSt: Features and State-of-the-Art
我们通过关于 TruSt 和当前最先进技术的两个观察来结束本节。
首先,如
对于
程序: 以下两个执行历史
等价,尽管 和 的相对顺序和关系不同: 因为这两个执行历史中每个
读到的值等价,即两个历史对应的树状历史等价,而树状历史(如图 6)不关心 边。
将 TruSt 扩展到
其次,TruSt 的结构本质上是可并行的。尽管最佳 DPOR 算法已经并行化(例如,[Lång et al 2020]),但这种并行化涉及这些算法的实现,而不是算法本身,因为需要数据共享。TruSt 是第一个最佳的、与内存模型无关的 DPOR,它完全不需要在不同线程之间共享,因为如
ALGORITHM
在本节中,我们将介绍模型检查算法 TruSt 的完整版本。首先,在
Algorithm Overview
TruSt 的算法可以在算法 1 中看到。给定一个输入程序 P,Verify 通过使用仅包含初始化事件的执行图
现在让我们仔细看看 Visit 函数,它是验证过程的核心。在每一步中,只要当前执行图 G 根据底层内存模型(第 4 行)保持一致,Visit 就会通过调用 nextP (G) 来扩展当前图 G。
函数 nextP (G) 找到一个既没有阻塞也没有完成的线程,将相应的事件添加到 G.E 和
Visit 采取的下一步行动取决于 a 本身。
- 如果 a 为 ⊥ 或错误,则 Visit 分别返回(第 6 行)或引发错误(第 8 行)。
- 如果 a 是读取,Visit 需要计算新添加事件的所有可能的 rf 选项。为此,对于每次将 w 写入与 a 相同的位置(第 11 行),如果 G rf(r) 映射到 w,它会在结果图上递归调用 Visit。任何不一致的选择随后将通过相应递归调用第 4 行的一致性检查来消除。
- 如果 a 是写入,如
中所述,则访问需要检查 a 不重新访问任何图形读取的情况以及 a 重新访问 G 中的某些读取的情况。
为了处理第一个案例,Visit 致电 VisitCO(第 14 行)。对于 G 中 a 的每个可能的共同前任 w𝑝,VisitCOs 将通过 SetCO(G w𝑝 a) 在 co 中的 w𝑝 之后立即插入 a,然后在结果图上调用 Visit(第 22 行)。形式上,
处理 a
因此,IsMaximallyAdded(G e w) 紧密遵循事件 e 在 G 中的 w 之前最大添加的定义(参见
最后,回到 Visit 的 switch 语句,对于所有其他事件情况(例如,内存栅栏),Visit 只是启动一个递归调用(第 19 行),无需特别注意。
至此,我们对 TruSt 的介绍就完成了。然而,关于算法 1,有两点值得一提。
第一个是,当
值得一提的第二点是,算法 1 可以多次
TruSt: Adaptation for a Reads-From Equivalence Partitioning
虽然算法 1 枚举了给定程序的所有一致执行图,但它同时还跟踪相同位置写入之间的完全一致性 (co)。事实上,正如第 3.4 节和第 4.1 节中所解释的,在检查最大扩展时,
然而,许多最近的 DPOR 方法避免跟踪完全的一致性(例如,[Abdulla et al 2019; 2018; Chalupa et al 2017; Kokologiannakis et al 2019; 2020])。如果不这样做,他们会获得指数级粗略的等价分区,通常称为读取等价分区。虽然尚不清楚这种分区是否会对现实工作负载的验证时间产生实际影响 [Kokologiannakis et al 2019],但它可能会导致在具有无序、并发、同一位置写入的程序中探索指数级减少的执行次数。
作为一个例子,再次考虑
一般来说,DPOR 避免对未观察到的并发写入进行排序的方法是用仅部分对相同位置写入进行部分排序的不同一致性关系替换 co。对于 GenMC(以及扩展后的 TruSt),这种关系称为 writes-before (wb) [Kokologiannakis et al 2019;拉哈夫等人 2015]。为了了解 wb 的工作原理,请考虑图 11 中的示例。在所描绘的执行图中,第一个线程的两次写入按 wb 排序,因为
此时,必须提出一个问题:我们能否将 TruSt 的最大扩展思想(严重依赖 co)与 wb 之类的关系(通常避免排序并发写入)结合起来?
幸运的是,答案是肯定的,但实现起来却很棘手。显然,在最大添加事件的定义中,不能简单地将 co 替换为 wb,因为在一致的执行图中可能存在多个 wb 最大写入。为了保持最优性,我们需要在这些 wb-maximal 事件之间建立一个平局。一个简单的解决方案是选择一个任意的仲裁者(例如,最后插入到图表中的写入,即 wb-maximal 事件中的 <G -maximal 事件)。该解决方案有效,但它需要底层内存模型具有更强的可扩展性:使用从任何 wb-maximal 事件读取的读取事件扩展一致的执行图应该会产生一致的图。虽然此属性适用于某些简单模型,例如释放-获取一致性,但它不适用于其他模型,包括 SC。问题在于,虽然一致图的 co 必须是扩展 wb 的某个位置总阶,但并非所有扩展 wb 的位置总阶都满足模型的一致性谓词。
在算法 2 中,我们提出了一个更好的解决方案,它仅假设有一种由函数 GetConsCO 给出的方法来计算图一致的相关关系。该函数的简单实现是以系统方式枚举扩展 wb 的所有位置总订单,并返回满足模型一致性谓词的第一个订单。更好的实现可以通过更有效的方法来检查不包含协同组件的执行图的一致性(例如,[Abdulla et al 2019; Biswas et al 2019; Bui et al 2021])。
我们的改编算法(以下称为 TruSt/wb)通过在图的部分上调用函数 GetConsCO 来获得合适的相关性,该部分将在
唯一需要的其他更改是 VisitCO 的定义:由于 TruSt/wb 不跟踪完全一致性,VisitCO 简单地归结为 Visit 调用,从而展示了 TruSt/wb 潜在提供的指数级改进。
TruSt: Soundness, Completeness & Optimality
假设输入程序 P 的执行次数只有有限大小,我们表明 TruSt 算法(算法 1)总是终止,并且是健全的、完整的和最优的。健全性确保如果 Verify(P) 生成 G,那么 G 是一致的完整程序执行。完整性确保如果 G 是 P 的一致完整执行,则 Verify(P) 将生成 G。最优性确保 TruSt 准确地生成每个执行一次,并且从不进行浪费的探索。 Kokologiannakis 等人 [2022] 中完整给出了这些结果的证明;我们继续进行概述。
算法终止。我们首先表明,一旦任何 writew
算法 1 的终止源自以下假设:P 的所有执行都是有界大小的。由于除了
健全性。 TruSt 非常合理,因为事件按照程序语义添加到图中,而不一致的执行一旦到达就会被删除。
完整性。完整性表明,Verify(P) 访问 P 的每个一致的完整图。
定理 4.1(完备性)。令 G𝑓 成为 P 的一致完整执行图。然后,Verify(P) 为某个图 G' 𝑓 ≈ G𝑓 调用 Visit(P G' 𝑓 )。
证明背后的关键思想是,给定算法达到的执行 G,我们可以推断出在导致 G 的(唯一)生产序列中紧邻它之前的执行。这一观察使我们能够定义一个过程 Prev(算法 3),将每个非空一致执行映射到其“上一个”执行。 Prev 让我们退后一步;从 G 到唯一执行 G𝑝,这样 Visit(P G𝑝 ) 立即导致 Visit(P G) 调用。
我们表明,从 G𝑓 重复执行上一步最终将得到初始图:在每个点 G𝑓 的最大事件被删除或从较早的事件中读取。接下来,我们表明,只要图 G 通过一致的完整执行是 Prev 可到达的,并且 G𝑝 是可到达的算法配置,使得 Prev(G) = ⟨G' 𝑝 _⟩ 且 G' 𝑝 ≈ G𝑝 ,则 Visit(P G𝑝 ) 调用 Visit(P G' ) 对于一些 G' ≈ G。然后,定理 4.1 对来自 G𝑓 的 Prev-step 序列进行归纳。
最优性。最优性包括显示两个属性:(1)不存在重复的探索,(2)不存在注定会被阻止且永远无法完全执行的无结果的探索。
为了建立前者,我们首先证明对于每个可到达的算法配置 G,如果 G 执行算法步骤 𝑡 并达到配置 G',则 Prev(G') = ⟨G𝑡⟩。这是因为 𝑡 要么将最大事件添加到 G(非重访情况),要么将其读取的写入(
然后我们可以轻松证明不存在重复探索,因为每个配置 G 最多达到一次。 (通过矛盾假设有两个生产序列达到相同的配置。但是,我们刚刚证明它们必须具有完全相同的最后一步,现在我们有两个较短的生产序列达到相同的配置,通过归纳,它们也应该一致)
定理 4.2(无重复探索)。给定图 G,Verify(P) 在针对某些 G' ≈ G 调用 Visit(P G' ) 之前最多会经历一系列嵌套的 Visit(P _) 调用。
为了建立后一个属性,我们需要对涉及 RMW 事件处理的记忆模型进行额外的假设。我们说内存模型是独占写入可扩展的,如果对于所有具有 po 最大独占读 r ε G R ex 的一致图 G ,使得不存在具有立即 po- 的独占读 r ' ε G R ex 。后继(匹配)独占写入和 G rf(r ' ) = G rf(r),添加其相应的独占写入事件 w 产生一致的图(对于 co 的某些选择)。
然后,我们可以证明,如果可达算法配置无效,那么它会立即被阻止。事实上,可能出现无效配置的唯一方法是添加从另一个 RMW 已读取的写入中读取的 RMW 事件的读独占部分。下一步将添加其写入独占部分,从而使图形不一致。
TruSt: Iterative Version with Linear Memory Consumption
最后,我们提出了具有线性内存消耗的 TruSt 算法的迭代版本。为简单起见,在算法 4 中,我们展示了不记录 co 且适用于 rfe 等价的版本。该算法显然具有线性空间复杂度,因为它仅保留执行图 G 的一份副本以及用于跟踪
算法 4 以迭代方式运行,采用以下两种模式之一:(1) 前向模式,在可能的情况下不断向图中添加事件; (2) 回溯模式,当替代探索选项可能时,该模式改变图的 rf 边缘,删除事件(例如,执行
前向模式对应于算法 4 的外部 while 循环,非常简单。只要图形一致,图形就会随着下一个可用事件 a 进行扩展(第 3 行)。如果该事件表示错误,则验证失败并显示错误消息(第 4 行)。如果 a 是读,则其 rf 根据插入顺序设置为同一位置的最大写。如果 a 是写入,我们初始化它在 B 数组中的索引。否则,如果执行完成(或不一致),我们将进入回溯模式(第 8 行)。
回溯模式对应于内部 while 循环,并且更微妙一些。首先从 G 中选择最大事件 a(第 9 行)。如果不存在此类事件(即图中仅包含初始化事件),则回溯完成,验证完成(第 10 行)。现在,如果 a 存在并且是一个读取事件,我们必须检查 a 是否还有任何未考虑的剩余
类似地,如果 a 是一个写入事件,我们必须检查是否有任何(进一步的)读取需要由 a
最后,如果 a 没有任何剩余的重访选项,我们调用 Prev(G)(算法 3),返回上一个执行步骤(第 22 行)。如果这不是
为此,对于 a 之前的事件,我们遵循 G 中的事件顺序,对于已删除的事件,则遵循插入顺序。每当删除的事件 𝑑 从稍后(按插入顺序)的 G 事件读取时,这意味着 𝑑 已被
IMPLEMENTATION
在第 1 节和第 3 节中,我们强调内存模型参数化是 TruSt 的关键特征之一。为了在内存模型的选择上实现参数化,我们在 GenMC 之上实现了 TruSt [Kokologiannakis et al 2019]。 GenMC 的实现是开源的 (https://github.com/mpi-sws/genmc),并且内置了对 RC11 等弱内存模型的支持 [Lahav et al 2017]。
也就是说,尽管 GenMC 的架构通常是模块化的并且允许扩展,但我们必须大幅修改 GenMC 的现有实现,以集成 TruSt 所需的更改并适应 TruSt 的并行化。后一部分可以说是技术方面最具挑战性的部分。
就 TruSt 的顺序实现而言,我们的实现主要遵循 TruSt 算法的递归版本,但在原地执行
就 TruSt 的并行实施而言,我们面临两大挑战。第一个挑战是 GenMC 使用多个非线程安全的 LLVM 内在函数。因此,为了排除由于 GenMC 所依赖的内部 LLVM 库导致的并发错误,我们重新设计了 GenMC 基础设施的很大一部分,以减少其对 LLVM 内在函数的依赖,以便不同的线程可以以线程安全的方式进行通信。
我们必须处理的另一个问题是不同线程之间通信的设计。为此,我们选择了以下简单的解决方案:由于 TruSt 每当启动递归
将所有内容放在一起,我们获得了 TruSt 的实现,它具有多项式内存要求,并充分利用底层机器的并行性 (
EVALUATION
我们的评估围绕验证
(2) TruSt 本质上是可并行的:它可以扩展到大量核心,因为不同的探索不共享状态。
为了证明第一点(
为了演示第二点 (
实验装置。我们在 Dell PowerEdge M620 刀片系统上进行了所有实验,该系统运行基于 Debian 的自定义发行版,配备两个 Intel Xeon E5-2667 v2 CPU(8 核 @ 3.3 GHz)和 256GB RAM。我们对 Nidhugg、GenMC 和 TruSt 使用 LLVM 7.0.1。除非另有明确说明,所有报告的时间均以秒为单位。我们设置了 24 小时的超时限制和 500MB 的内存限制。
TODO
RELATED WORK
在 Verisoft 等开创性作品之后 [Godefroid 1997; 2005] 和 Chess [Musuvathi et al 2008] 为无状态模型检查铺平了道路,关于 SMC 和 DPOR [Flanagan et al 2005] 已经有大量工作。 Abdulla 等人 [2014] 在这方面取得了重大突破,他们为 Mazurkiewicz 迹等价提供了第一个最优 DPOR 算法,以实现顺序一致性。如
首先,许多技术旨在通过引入更粗糙的等价划分来解决状态空间爆炸问题[Abdulla et al 2019;阿加瓦尔等人 2021;艾伯特等人 2017; 2018;阿罗尼斯等人,2018;查卢帕等人 2017; Chatterjee 等人 2019]。其中,只有 Nidhugg [Abdulla et al 2019; Aronis et al 2018] 是最优的。尽管如 6.1 中所示,其等价分区可能会遭受指数内存消耗的影响。
其次,其他技术侧重于通过针对特定的弱内存模型将 DPOR 扩展到弱内存模型 [Abdulla et al 2015; 2016 年; 2018; Norris et al 2013] 或通过相对于公理定义的内存模型进行参数化 [Kokologiannakis et al 2019; 2020]。此外,其中许多工作可以在更粗略的读取等价分区下运行,包括 Abdulla 等人 [2018] 和 Kokologiannakis 等人 [2019, 2020],它们在保持最优性的同时做到了这一点。 TruSt 建立在 GenMC [Kokologiannakis et al 2019] 奠定的基础上,以实现内存模型参数化;但是,它将底层内存模型的“可扩展性”要求调整为更精确的“最大可扩展性”要求(参见
然而,与 TruSt 相比,上述技术都没有能够将 (a) 参数化与 w.r.t 结合起来。内存模型,(b) 在读取等效性下运行,(c) 最优,以及 (d) 保持多项式内存消耗。在某种程度上,Nguyen 等人 [2018] 已经利用准最优 DPOR 探索了 (c) 和 (d) 之间的组合。准最优 DPOR 能够使用用户提供的常数 𝑘 来近似最优 DPOR。随着 𝑘 值的增加,内存消耗也呈指数增长。尽管准最优 DPOR 理论上仅在 𝑘 = ∞ 时才达到最优,但事实证明,对于较小的 𝑘 值,它实际上是最优的。
最后,值得思考的是,是否可以使用最大扩展的思想来实现像 RCMC [Kokologiannakis et al 2017] 这样的算法的最优性,该算法并不是严格意义上的 DPOR 算法。不幸的是,事实证明最大扩展是不够的。要看到这一点,请考虑下面的程序:
与 TruSt 不同,在
结论和未来的工作
我们提出了 TruSt,一种优化的内存模型参数无状态模型检查算法,可协调 SMC 与 DPOR。 TruSt 提供了第一个保持多项式内存消耗的最佳 DPOR 框架。与之前的工作相比,TruSt 仅在要从当前执行图中删除的事件形成最大扩展(即它们以共最大方式添加)时才逆转两次内存访问之间的竞争。
未来,我们计划追求几个不同的研究方向。首先,我们计划放宽 TruSt 的波尔夫无环性假设,以便它也可以解释 POWER [Alglave et al 2014] 和 ARMv8/RISC-V [Pulte et al 2018] 等硬件内存模型。其次,我们计划通过设计更精细的消息传递方案来微调和优化 TruSt 的并行实现,该方案将允许在更大的基准测试中实现更大的可扩展性。最后,我们计划利用 TruSt 的低内存要求,并用它来验证大型软件系统的关键组件(例如 Linux 内核的数据结构)。