3.4. 分离模式匹配与选择
在前面所讨论的做法里,模式匹配及模式选择的任务组成了单个步骤。尽管这使得单遍代码生成成为可能,但它阻碍了指令选择器进行最优的模式选择,因为在进行决策前,它不能收集所有适用模式的信息。保持两者独立可以解决这个问题,代价是必须在表达式树上执行多个遍。
3.4.1. 通过树解析的模式匹配
多年来,已经找出许多算法来查找另一棵树中子树的匹配。应用在指令选择中大多数主要的模式匹配算法都受到在字符串中查找模式的技术的影响。在树及字符串上的模式匹配本质上是类似的,它们通常都依赖一个在部分匹配失败时有效地消除了回溯需要的内部状态机。这样的算法通常被称为树解析器,它们可以在线性时间内找出在一棵树里所有出现的模式。要理解树解析,让我们首先讨论字符串算法如何工作。
两个线性时间的字符串匹配算法由Aho与Corasick【5】及Knuth等【149】开发。它几乎同时被独立地发现——Aho与Corasick在1975年发表了他们的论文,而Knuth等在1977年——这两个算法以相同的方式工作,因此它们在的方法上是几乎相同的。直观地说,当一个具有重复模式的子字符串的一个部分匹配失败时,匹配器不需要回到匹配开始的地方。这显示在图3.7里,其中子字符串abcabd与输入字符串abcabcabd匹配。首先,该子字符串匹配输入字符串的开头,直到最后的字符(位置5)。现在,不是回到位置1并从头开始匹配,匹配器记住子字符串的头三个字符(abc)现在已经匹配了。因此匹配器在位置6继续,尝试匹配子字符串中的第四个字符。
图3.7:字符串匹配例子。箭头表示算法考量的当前字符
继续我们对Aho与Corasick做法的讨论,因为它能够匹配多个子字符串,而Knuth等的算法仅考虑单个子字符串(虽然它也很容易扩展来处理多个子字符串)。该算法依赖一个状态机,该状态机使用三个函数:goto,failure及output(参见图3.8)。这些函数的构造超出了本文的范畴,但有兴趣的读者可以参阅论文。函数goto把下一个字符作为输入,并指定匹配状态。如果没有这样的迁移,查询failure函数,它识别应该落到机器的哪个状态。例如,如果输入字符串是“shi”,机器将首先从0迁移到3,然后到4,在那里在输入字符“i”上失败。这时,机器迁移到1,然后到6。最后,output函数指出机器何时进入对应一个成功匹配的状态。
Karp等【138】首先意识到树模式匹配可以被简化为字符串匹配。在1982年Hoffmann与O’Donnell【130】通过将Aho与Corasick,及Knuth等的思想整合入一个自顶向下的模式匹配算法,改进并扩展了这些想法。Hoffmann与O’Donnell还提出了一个自底向上的算法,它以更长的预处理时间换取快速的线性时间的模式匹配[1]。两者中,自底向上的算法是最广为人知的。
自底向上的算法工作如下。从叶子开始,每个节点被一个标识符标记,该标识符表示匹配以该节点作为根节点的子树的模式的集合。我们称这组集合为匹配集(matchset)。标记从一组预先计算的,以标记为索引的表中提取。因此,使用当前节点n的子节点的标记进行一个表查找确定了节点n的标记。一旦所有的节点被标记,执行一个自顶向下的遍历来提取每个节点的匹配集。
在常见的情形里,匹配集的数量是指数级增长的,就像在生成表时。不过,Hoffmann与O’Donnell意识到对于某个类别的模式树林,匹配集的可能数量等于模式树林的大小。此外,每个这样的匹配集可以由单棵树来表示。基于这个洞察,导致了一个构造表的算法的发现,许多机器指令集被适配以产生符合这个类别的树模式。
图3.9展示了一个例子,模式I与II包含了带有符号a,b,c或v的节点,其中a有两个孩子,而b,c及v没有孩子。符号v是一个特殊的空白符号(nullary symbol),它是匹配任何子树的占位符。常用的术语是这些符号构成了字母表(alphabet),我们以Σ表示。字母表必须是有限且分级,意即每个符号具有一个表示孩子个数的分级函数(有时也称为引数数目(arity))。因此,在我们的例子里,rank(a) = 2,而rank(b) = rank(c) = rank(v) = 0。遵循该论文的术语,我们还引入记法Σ-term,并定义如下【130】:
1. 对于所有的i∈Σ,其中rank(i) = 0,i是一个Σ-term。
2. 如果i∈Σ,其中rank(i) = m > 0,如果每个ti是一个Σ-term,那么“i(t1, . . . , tm)”是一个Σ-term。
3. 其他都不是Σ-term。
因此一棵树是一个Σ-term,这允许我们把其中的模式I与II分别写作“a(a(v, v), b)”与“a(b, v)”,Σ-term是有序的,这意味着“a(b, v)”不同于“a(v, b)”。结果,交换律性质必须像在Glanville-Graham方案那样,通过复制模式来处理。
自模式I与II,为每个符号i ∈Σ生成一张表Ti。大致上,构建表的算法工作如下。首先所有来自模式树林的子模式被枚举,以非负数顺序编号。对于我们的运行示例,这生成了树“v (0)”,“b (1)”,“a(v, v) (2)”,“a(b, v) (3)”,及“a(a(v, v), b) (4)”;我们把这组模式记为S。下一步是使用S中的树构成立即包容图(immediate subsumption graph)。称模式t1包含另一个模式t2,写作“t1 ≥ t2”,如果t1的匹配总是意味着t2的匹配(即如果t1是一个匹配集,那么t2也在这个匹配集里)。按照定义,所有的模式包含自己。另外,如果不存在t3使得“t1 > t3 > t2”成立,称模式t1立即包含另一个模式t2,写作“t1 > t2”。通过为每个模式创建一个节点,然后如果由n1代表的树立即包含由n2代表的树,从n1节点画一条边直接到n2节点,构成了立即包含图。出于完整性,如果没有出现在彼此的匹配集中,称模式t1与t2为是不一致。而如果存在一棵t1匹配但t2不匹配的主题树(subject tree),及另一棵t2匹配但t1不匹配的主题树,称模式t1与t2为是无关的。没有包含无关模式的模式树林被称为简单模式树林,它正是自底向上Hoffmann-O’Donnell算法 所期望的。
S的立即包含图显示在图3.9的(c)。我们把这个图表示为GS。我们注意到,如果“a(b, v)”匹配,那么“a(v, v)”与“v” 也必须匹配。通常GS是一个仅有叶子v的DAG。对于我们正在考虑的模式树林的类别(即简单模式树林),GS总是具有一棵树的形状。模式树林S是简单的这个事实导致一个重要的定理:每个匹配集M实际上包含了GS中从节点ni到v路径上所有的树。另外,这棵由节点ni代表的树可以精确地表示整个匹配集。这是Hoffmann与O’Donnell的表生成算法的主要驱动力。
图3.9:使用HoUmann-O’Donnell(来自【130】)的树模式匹配。Nullary节点以虚线边框突出。子模式标记如下:v (0),b (1),a(v, v) (2),a(b, v) (3),及a(a(v, v), b) (4)。
构造了GS之后,在立即包含图GS上执行一个拓扑排序,以包含升序排列模式。一旦排好序,为每个符号i∈Σ构造一张具有rank(i)维的表,每维的大小是|S|。使用分配给树v的标记来初始化这张表。然后包含升序,对于每个S中的模式p,以遍历表索引的所有组合。以符号i作为根节点,由索引代表的子树(记住索引也是标记)作为根节点的孩子,从一个给定的组合构造出了一棵树。现在,如果这棵树包含了p,就把索引组合指出的表项设置为p的标记。注意我们正在谈论包含的弱形式,不是立即包含。在这个过程终止时,表中的每个项将包含可以匹配一棵给定树根节点的最大模式的标记。因为模式树林是简单的,最大的匹配就给出了整个匹配集。
构造了这些表之后,查找一棵主题树的所有匹配集只需表查找的一个后续遍历深度优先搜索。例如,要标记一个c0与c1分别作为左右孩子的节点,该算法只要执行一个表查找Ta[label(c0), label(c1)]。一棵完整的被标记主题树在图3.9(g)给出。
Chase【46】通过开发一个压缩生成表的算法进一步推进了这个技术。事实上,在某些情形下这些表可以大到无法首先创建出来。幸好,该方法可以被扩展为在表生成时压缩。Cai等【36】后来改进了Chase算法的渐进界。我们继续描述主要的想法,不进入如何进行压缩的细节。
关键点是,生成的表通常包含重复的信息,因为许多行与列是重复的。例如,在图3.9的Ta中可以清楚地看到这一点,图3.10(a)也能看到。通过对每个查找表引入一组索引图(index maps),把索引中相同的行或列映射到查找表中同一行或列,可以消除重复。那么查找表可以减小为仅包含最少的信息。我们把压缩的操作表表示为τi,把压缩图表示为μij,其中j是该表的维度。因此,之前通过Ti[l0, . . . , lm]执行的查找,现在被扩展为Ti[μi0[l0],. . . , μim[lm]]。Ta的压缩版本在图3.10(b)中显示作τa。
图3.10:操作表的压缩(来自【46】)
因为这个做法要求模式必须排序,交换性必须通过模式复制来处理。不过,这产生了彼此无关的模式,因此模式树林不再是简单的。Hoffmann-O’Donnell算法仍然能够处理这样的模式树林,但生成表将包含只能强制选择一个模式或另一个模式,而不能是两者(即使它们都匹配)的项。基于这个做法的指令选择器将不能获得最优的代码质量,因为模式匹配将不是完整的。尽管如此,我们将讨论几个仍然设法将Hoffmann-O’Donnell算法高效用于指令选择的做法。
在本报告的调查过程中发现,但没有充分研究的其他三个模式匹配算法,包括:Purdom与Brown【190】,Weisgerber与Wilhelm【234】,Ramesh与Ramakrishnan【191】,Dubiner等【69】,Chen等【48】,Cole与Hariharan【52】,Shamir与Tsur【208】,及Wuu等【240】。