最近准备开一个新坑,记录一下读过的一些论文,主要聚焦笔者在阅读过程中的感悟,一些重点算法的理解,以及笔者觉得可以改进的地方。本文为系列的第一篇,试试水先。
本文选择的论文是Furthering Datalog in the pursuit of program analysis。 是一篇剑桥大学的博士论文,发现此文的契机是在对value numbering技术进行跟踪时发现了一篇2004年的A Polynomial-Time Algorithm for Global Value Numbering。最新的引用中找到了这篇文章。因为笔者水平有限,难免有遗憾和缺漏,仅供参考,欢迎评论区补充。
全文共分为7章,分别是Introduction, Background, Related Work, Dynamic stratification in Souffle, Declarative global value numbering, Semantic set annotation analysis, Conclusion, 还有一些References.
此部分主要是讲解了对本文主要研究的介绍,其中介绍了Datalog,这门语言对于很多读者是陌生的,但是在程序分析领域应用非常普遍,该语言是函数式的,可以针对性的学习以下,网络上有相关的资料。
作者在该部分末尾介绍了本论文的三个主要贡献,分别是扩展了Souffle,使其能够在编译器优化时展示更优异的表现;第二是基于上述2004年的论文改造了该GVN算法,使其比LLVM效果更好,这也是笔者关心的一个点;最后就是提出了一个新的程序分析方法,能够更好的发现程序之间的可交换性。
该部分主要是介绍该研究的相关背景,对读者来说最难的部分是对Declarative languages部分的介绍,该部分使用的Datalog和Prolog笔者并未用过,因此在CSDN中搜索了相关的内容,主要是Prolog相关的,这篇资料介绍较为完善。还需注意一个问题,文中作者描述规则时使用的变量存在用小写表示的情况,但这在SWI-prolog中不支持使用小写的变量,需要用大写编写。且使用注释时不能用//的样式,而应该使用%。此外,笔者在利用SWI-Prolog复现论文第33页提到的调换edge和path之后的结果,与文中作者提到的一丢失,两循环相同,但具体的值不同,想来是实现模式不同,并未深究。
在介绍Datalog evaluation部分时,作者在2.2.1.3中介绍了Bottom-up Datalog evaluation,然后在2.2.1.4部分介绍了一种改进的evaluation算法,但是在2.2.1.3中并未给出具体的例子,反而是在2.2.1.4中介绍了相关的例子,这种写法笔者认为对初次接触该领域的读者不太友好,也不利于读者理解Algorithm1。
现将Algorithm1和使用到的公式进行补充解读。
本部分主要是为了对一个Datalog程序进行推理,推理的过程是按照迭代的思路进行分析,每一次推理的结果得到新的结果,但这些新的结果不会立即加入到推理的前提条件中。读者可能会有一个疑惑——为什么推理的结果不能直接用到推理过程中呢?其实两种方式进行的操作是一样的,新的推理结果的加入不会造成原有推理过程的省略,该走的路还是要走的。
而上述公式本质上是根据一个前提集合I,根据这个集合推理出的新的fact用上述公式中后半部分表示,而该公式需要表示的是所有的inferrable fact,前提条件以及已证明的事实自然符合inferrable fact的范畴,所以要对二者取并集。
基于上述认知的前提下再来看Algorithm 1:
根据上述公式可以看到,该算法本质上就是不断进行推理,如果本轮推理得出了新的结论,则进行新一轮的推理,如果本轮推理没有得到结论则说明所有的结论都已推出,则推理完毕。
在此基础上再来看上述推理,无非是保证了本轮推理过程中使用到的前提必须包含一个上一轮推理得到的新结论,这也是显然而然的结论。
precedence graph 前趋图,好像是跟数据库联系比较紧密的一个概念,看论文的图结合示例程序很好理解。
第一部分介绍Souffle语言的相关工作,这部分不熟悉,略过。
第二部分介绍GVN,这部分介绍了LLVM两个版本的GVN之间的区别,我对NewGVN是了解过的,但是还并不知道原来NewGVN的new是新在这里,原来另一个作者在2016年提出了一个更加balance的方法,本文提到的两篇论文有必要研读一下。
将引用列在下面:
Karthik Gargi. “A sparse algorithm for predicated global value numbering”. In:
Proceedings of the Conference on Programming Language Design and Implementation
(PLDI) (2002).
Rekha R. Pai. “Detection of redundant expressions: A precise, efficient, and pragmatic
algorithm in SSA”. In: Computer Languages, Systems and Structures 46 (Nov. 2016).
介绍NewGVN算法时,给出了一个例子,可以看Figure3.1,但这个例子中有一些错误,1是 P E 3 P_{E_3} PE3画在了基本块的方框中且未给出其元素;2是 E 3 E_3 E3中的 z 3 = x + z 2 z_3 = x + z_2 z3=x+z2应该改为 z 3 = c + z 2 z_3 = c + z_2 z3=c+z2,因为后续给出了该表达式的值编号,其中 x x x被替换为了 v 4 v_4 v4。
这段表述是为了通过该算法找到 z 1 z_1 z1和 z 3 z_3 z3的相等性。
将本段笔者理解的部分翻译如下:
Pai [38] presents value phi functions (VPFs), a data structure, to express unexplored-equivalence-partition intersections created when partitions are operands in ϕ instructions.
上述论文引入了一种新的数据结构用来表示程序合并点那些尚未确定的 ϕ \phi ϕ结点的等价划分。
They are used lazily to search for partition equivalence. This technique can increase the precision of the algorithm without computing the intersection of partitions that are not later used by successive instructions—partitions are instead computed on demand.
过去的方法并未考察此处的相似性,本算法能够增加程序的精确度而不须计算后续指令用不到的合并点的划分——划分在用到的时候才计算。
This technique can be combined with the approaches of the previous works, gaining the benefits of both, and which has been implemented by the LLVM compiler in the pass NewGVN (further referred to as LGVN). The usage of VPFs can be seen in the example in figure 3.1. The set P E 1 P_{E_1} PE1contains all the values from an (omitted) previous basic block which are propagated at a fork point. The partitions with value numbers v 5 v_5 v5and v 6 v_6 v6 are derived using value number substitution. P E 2 P_{E_2} PE2 is derived similarly to P E 1 P_{E_1} PE1. P E 3 P_{E_3} PE3 has underlined partitions computed from the intersection of P E 2 P_{E_2} PE2 and P E 3 P_{E_3} PE3.
该算法可以和以前的算法结合使用,兼取其长,已经被LLVM实现为NewGVN pass。图3.1举了一个VPFS的例子, P E 1 P_{E_1} PE1包含所有来自之前的基本块的值编号(图中省略)且这些值编号传播到了分支点(该分支点指的是途中两个基本块 E 1 E_1 E1和 E 2 E_2 E2), v 5 v_5 v5和 v 6 v_6 v6的划分是用值编号实现的。 P E 2 P_{E_2} PE2类似。 P E 3 P_{E_3} PE3中的下划线编号是来自二者的交集**(原文错误,应该是 E 1 E_1 E1和 E 2 E_2 E2的交集)**。
Then the IR ϕ \phi ϕ-nodes causes z 2 z_2 z2 to be added to the partition with value number v 8 v_8 v8 and value-expression (vpf) ϕ ( v 1 , v 2 ) \phi(v1, v2) ϕ(v1,v2) representing the predecessor dependent choice of the values 1 or 2. The addition of the vpf allows later steps in the algorithm to search in predecessor basic blocks.
IR中的 ϕ \phi ϕ结点导致了 z 2 z_2 z2被添加到一个新的划分,编号是 v 8 v_8 v8,(此处忽略了 z 1 z_1 z1是因为取交集后该phi结点依赖的两个值都是未定义的)该划分代表前驱结点依赖选择1或者2.vpf的引入允许算法后续步骤搜索前去基本块。
The previously used alternatives were to ignore equivalence hidden by a join point or to create the set of all of them and propagate this throughout the program, if this was done there would be a partition which represents the predecessor dependent values of c+3:E1 or c+2: E2, this partition would be created and would cause later partitions to be created, all of which might never be used—this causes a large amount wasted work.
以前使用的算法忽略了join结点的相等性,或是直接创建一个新的分类,传播到整个程序,如果这样做,会有一个分类代表E1中的c+2和E2中的c+2。这重分类得以创建并导致后续分类创建,而这些都是之后用不到的——造成了大量时间的浪费。
The variable z 1 z_1 z1 and vpf ϕ \phi ϕ(v5, v7) are added to the partition with value number v 9 v_9 v9. Then the expression assigned to z 3 z_3 z3 is considered, this is seen as v 4 v_4 v4 + v 8 v_8 v8, then the vpf search is conducted for v 4 + ϕ ( v 1 , v 2 ) v_4 + \phi(v1, v2) v4+ϕ(v1,v2). This expression is equal to the previous expression, but with value numbers being replaced with their vpf expression if one exists. Then the value expressions v4 + v1 : E1 and v4 + v2 : E2 are searched for in the respective partition. Since both values exist there is an equivalence found between v4 + v8 and ϕ(y1, y2), they are
therefore added to the same partition. The work for checking previous partitions is only done in a demand driven fashion—leading to a large performance increase.
z 1 z_1 z1和 ϕ \phi ϕ(v5, v7)的值添加到一个新的分类编号为 v 9 v_9 v9。然后考虑赋值给 z 3 z_3 z3的表达式,被看作 v 4 v_4 v4 + v 8 v_8 v8,然后vpf搜索创建了 v 4 + ϕ ( v 1 , v 2 ) v_4 + \phi(v1, v2) v4+ϕ(v1,v2)。该表达式和以往的表达式相等,通过值编号被另一个vpf表达式代替。然后v4 + v1 : E1 和 v4 + v2 : E2 在相关的分类中搜索。因为发现了v4 + v8和 ϕ(y1, y2)是相等的,因此可以划分到一个分类。检查过去分类的工作只有在需要的划分中才会执行——因此提升了性能。
第三部分介绍并行编程的一些方法,主要目的是后面作者会针对这种场景下的GVN给出一些优化策略。
略
本部分主要介绍笔者感兴趣的GVN部分,本文作者设计的DGVN能够更好的检测到两个不同的 ϕ \phi ϕ结点之间的联系,作者在该部分举了例子并介绍了自己的算法。其中展示了大量的代码,笔者准备研读之后开一篇用C++ 进行复现。本文的例子也有疏漏,笔者将在那一篇文章中指出。在Evaluation部分作者也并不拖泥带水,直接给出了能够检测到的等值关系的数量。
略
重点关注GVN部分,作者提出了DGVN的实现可以应用到symbolic reduction和memory equivalence中。另一个课题是可以改变Souffle使用的semi-naive evaluation的算法,以减少不必要的负载。希望有新的研究成果不断出现。
本文是一篇博士论文,洋洋洒洒,内容丰富。虽然有些图例和程序上的小瑕疵,但本文不乏一篇好的文章。敢于使用LLVM编译器进行对比就是一个很贴近工业界的实验方法。笔者对Datalog和程序分析的相关技术理解尚浅,还需要更进一步的学习和成长!
另外笔者主要关注GVN相关的技术,因此并未展开另外两部分,对相关技术感兴趣的读者也可以自行在网络中搜索原文来看。