您的位置:首页 > 其它

指令选择器调查(2)

2016-03-11 13:16 253 查看
原作者:gabriel hjort blindell(Sweden)

3. 树覆盖

宏展开的一个主要局限是其活动范围局限于单个编程语言结构或IR节点。如果输入程序与机器指令都被转换为图形化的表示,一个更强有力的做法是可行的。因为机器指令将形成比输入程序更小的图,这样指令选择问题被简化为查找机器指令的一个排列,它们产生与该程序相等的图。

让我们假设输入程序可以被转换为一组树,其中节点代表操作,边代表操作间的数据依赖。边箭头的方向表示数据的流向。每棵树仅可以包含对最终结果(即根节点)产生影响的操作。这样的树被称为数据流树,通常也称为表达式树。互不相关的操作形成独立的树;这实际上破除了输入程序中文本排列与指令选择执行次序间的联系,结果也消除了这个联系对代码质量的影响。

让我们进一步假设目标机器上的每条指令可以被转换为能捕捉该指令行为的一棵树。这样的树被称为模式,每条机器指令可能存在多个模式。

那么,指令选择问题简化为查找并选择模式,使得每个表达式树中每个节点都为某个模式所覆盖。这个方法通常称为树覆盖。树模式匹配实例的一个例子展示在图3.1。正如已经提到的,这个任务是选择这些模式中的一个子集,使得树中每个节点被包含在某个模式里。如果一个节点被多个模式覆盖,那么这意味着该节点所代表的操作将由多条机器指令实现,因此比必须的执行次数要多。在某些情况下这可能是希望的——例如,重新计算一个值可能更高效,而不是保存它,然后稍后再提取它(这被称为重具现,rematerialisation)。 不过,每个操作仅由单条机器指令实现最有可能是最优的。因此感兴趣的解决方案通常限制在那些每个节点被正好一个模式覆盖的方案。找出可行模式的一个有效子集是非平凡的,因为选择一个模式可能摈弃了选择其他模式的可能性。如果模式应用了某些额外的限制,这个问题将进一步恶化,对于非常规的架构,比如嵌入式系统与DSP,这并非罕见。



图3.1:树覆盖的一个例子。模式实例由虚线表示,阴影背景包含由该模式匹配的节点

因此,树覆盖的指令选择实际上包含两个正交的子问题:

1. 识别哪些机器指令可能覆盖部分或全部表达式树。这个任务被称为模式匹配。

2. 选择机器指令,使得:

i. 每个节点仅覆盖一次,且

ii. 总体代价是最小的。

因此这个任务被称为模式选择。

正如我们将要看到的,某些做法统一了这两个子问题,而其他做法维持它们独立。

3.1. 第一批尝试[1]

Wasilew【232】与Weingart【233】,分别发表在1972年与1973年,第一次尝试设计使用树覆盖的代码生成器。在两者中,因为缺少前者的第二手资料,本报告将仅讨论后者。Weingart的做法[2]基于单棵模式树,称为判别网(discrimination
net),它由一个声明性的机器描述导出。Weingart提出,使用单棵树允许一个紧凑及高效的表示方法。那么,构建AST的解析过程被延伸为把解析符号压入一个栈。随着新符号的出现,通过将符号与当前树模式节点的子节点比较,渐进地遍历模式树。当命中一个叶子时发生一个匹配,然后流出对应该匹配模式的机器代码。

对比宏展开指令选择器,尽管Weingart的做法基于树覆盖,使更复杂的模式匹配成为可能,但在实践中该方法被证明难以应用。首先,因为代码生成器在语法树上工作,所有可能的代码序列必须被编码进模式树,因此存在某些序列不会被任何机器指令匹配的风险。虽然Weingart通过引入转换模式部分地解决了这个问题,例如将数据项移入机器指令支持的寄存器中。不过,没有方法能确定该组转换是否足够。其次,为配合单个目标——PDP-11,改写了机器描述的语义——因此可能会排斥其他机器模型。第三,该技术不能处理有多个匹配的情形。最后这项可能是最不利的,因为指令选择的核心功能是能够执行计算后选择,以用好可用的机器指令。

另一个做法由Newcomer【177】[3]提出,他使用方法-目的分析(means-end analysis)来驱动代码生成的过程。方法-目的分析是一个目标导向、递归的启发查找(参考Newell与Ernst【178】),其中的方法总是最小化当前状态与一个目标状态间的差异。不过,Newcomer的做法少有实用性,因为它仅处理算术表达式,并且只有有限的机器描述能力。另外,像Weingart,它工作在语法树上,有类似的缺点。最后,穷举查找策略在产品级编译器里极有可能代价过于高昂而不实用。

Fraser【100】[4]开发了一个基于知识的做法,输入程序首先被翻译到称为ISP’语言的形式,然后匹配也是以ISP’编写的、临时的机器特定规则。再次的,这个做法已知细节很少,我无法把握这篇论文——但在其他论文里讨论时,它被不予考虑,因为缺乏灵活性以及差劲的性能。

在Johnson【133】著名的可移植C编译器(pcc)的实现中,他使用了Snyder某些想法。代码生成器以一个树重写匹配器为中心,匹配器尝试将子树匹配到机器指令模板。它还包括了一个基于Sethi与Ullman想法的资源分配器。找不到匹配会导致使用机器相关重写规则的子树转换,这可能会导致无限循环。其他缺点包括与C编程语言的紧耦合,这使得它难以适应其他编程语言。另外,在代码生成器里,代码选择决策被分布到几个阶段,这样使重指向复杂化。

3.2. 线性化的、语法的解析

这些第一批尝试的一个共同的缺陷是,(i)它们仍然缺少一个正式的方法论,(ii)它们有相对较慢的倾向。比较起来,语法分析——依赖于字符串解析技术,比如LL,LR,SLR及LAL解析——是现代代码编译中被研究得最彻底的问题之一。另外,它们非常快,因为它们完全是表驱动的。幸好,就如我们将看到的,相同的想法也可以被应用来驱动指令选择。

在1978年Glanville与Graham【111】发表了一篇影响深远的论文,描述处理语法解析的语法技术如何可以适用于模式匹配及选择。(早以在1968年Feldman与Gries就暗示了这,尽管模糊【87,107页】)。Glanville与Graham认识到使用波兰前缀记法(即“1 + (2 + 3)”被表示为“+ 1 + 2 3”,使得括号被消除;还可参考图3.2)线性化树,机器指令可以被表达为一组语法产生规则。图3.3给出一组这样的规则)。使用一个修改的LR(1)解析器,这可以被用于驱动指令选择。我们把这个技术称为Glanville-Graham方法。

[1]本节主要基于Cattel【41】及Ganapathi等【108】的更早期的调查。

[2]基于来自【41,108】的第二手资料。

[3]基于来自【41,108】的第二手资料。

[4]基于来自【108】的第二手资料。



图3.2:线性化一棵树



图3.3:以Glanville-Graham记法表示的指令集语法(来自【111】)。▴符号表示内存提领(在他们的论文里,Glanville与Graham使用↑,但这会导致混淆,因为在后面的工作里,这个符号有另外的含义。

粗略地说,这个做法工作如下。程序字符串被逐步解析,并转换为压入一个栈的符号(token)。随着每个符号的压入,该算法决定是移动(继续)还是归纳(从栈弹出符号)。做出哪个决定是从一个由语法生成的、预先计算好的表推导来的。图3.4显示了由图3.3的语法计算得到的表。如果存在某个匹配栈顶部分的规则,就可以执行一个归纳。这对应于模式匹配任务。在归纳期间,栈上匹配的项将被弹出,为选中规则的左手侧符号所替代。因此这对应模式选择任务。移动和归纳的过程继续,直到整个程序被解析,希望由一个开始符号开始,以一个接受状态结束。这样执行的一个示范由图3.5给出。



图3.4:由图3.3语法产生的表(来自【111】)

常规的语法分析与Glanville和Graham的算法有几个关键的差别。首先,目标机器的指令集语法通常是高度二义性的。这导致了许多需要以某种方式解决的移动-归纳及归纳-归纳冲突。Glanville与Graham通过在这些情形里总是优先移动来解决移动-归纳冲突。效果是指令选择器将尝试选择尽可能大的模式,这通常是期望的结果。总是选择尽可能大的模式的思想俗称为maximum munching,或仅maximum munch,是Cattell【42】在他的博士论文里杜撰出来的。此外,归纳-归纳冲突使用选择最长右手侧产生式的简单启发式来解决。在相等长度的冲突中,该启发式选择这些规则中的第一个语法定义。

其次,归纳步骤要复杂得多,因为它需要考虑语法与语义信息。例如,当在一个输入符号r上执行一个移动时,r代表的寄存器的信息也要压入栈。为了流出机器指令,这是必须的。另外,Glanville与Graham选择把寄存器分配合并入这一步。

正如已经看到的,把机器描述表达为产生式规则语法使得指令选择器完全由表驱动。这是有利的,因为它允许在生成编译器时重新计算许多优化决策。此外,它把指令选择化简为简单的表查找[1]。表驱动代码生成的想法本身并不新奇——在1969年,Lowry和Medlock【166】展示了一个工作在比特字符串上的方法,而在1973年,Tirrell【221】与Donegan【68】都提出了使用各种表驱动代码生成的做法——更早的尝试都失败在不能提出产生这些表的一个自动化方法。因此这个能力是Glanville-Graham方法的核心贡献之一。

[1]Pennello【184】开发了一个技术把表直接转换为汇编代码,甚至取消了表查找的需要。据报告, LR解析速度提升6到10倍。



图3.5:在输入字符串“= + ka r7 + ▴ + kb ▴ r7 ▴ kc”上执行Glanville与Graham方法的一个示范(来自【111】)。该执行基于图3.4中预先计算的表。归纳步骤的进行可能需要一些解释。归纳可以引发两个操作:归纳操作,后跟一个可能是移动或另一个归纳的额外操作。让我们看第11步。首先,使用规则15从符号栈弹出▴ r7,发生了一个归纳。接着把该规则的结果r1压栈。与此同时,从状态栈弹出9和14,在栈顶留下状态34。现在使用这两个栈的栈顶元素查表,可以推断出下一个额外的动作。在这里,状态34下的输入符号r1产生了一个到状态38的移动。

因此,Glanville与Graham的论文被赞誉为该领域最重要的突破之一,许多现代的指令选择技术以这种那种的方式利用了这些思想:在1978年它发表后不久,实验和评估【9,108,116,117】显示,对比那个时代的做法,该方法被证明更简单、更通用。例如,高质量的代码可以非常快速地生成(尽管早期的实现与已有的其他实现处于相同的水平)。此外,由于它是建立在形式化方法之上,机器描述可以被表达为一个允许自动校验的形式。

例如,Emmelmann【76】展示了证明一个指令集语法完整性的首批做法之一。让我们把一个机器描述语法表示为G,把描述了合法输入树的语法表示为T。另外,L(T)表示T接受的所有树的集合,L(G)表示G接受的所有的树。如果表示被G拒绝的树集合,那么通过检查L(T) ∩(这等同于L(T) \ L(G))是否产生一个非空集合,我们可以检测这个指令集是否能处理所有可能的输入树。Emmelmann认识到,通过创建一个本质上实现了接受这个树反例集合的乘积自动机,就可以计算这个交集。近期,通过分割终结符以暴露隐藏的特性,Brandner【30】扩展了这个方法来处理包含动态检查(即谓词;更多参考3.2.1节)的机器说明。

不过,Glanville-Graham方法有几个缺点。首先,当尝试使用最长的模式时,LR解析器可能终止在一个状态,其中没有输入程序任何合法的“解析”,而如果应用较短的模式,则存在合法“解析”。这样的情形被称为语法阻塞,需要额外的语法来避免它们。这样的规则不能自动产生,需要由语法编写者添加。另一种阻碍是语义阻塞,在解析器因为语义不匹配不能往下进行时发生,即栈上的语义信息不能匹配任何规则。

其次,指令集语法常常膨胀得非常大。比如,VAX机器的语法——1980年代的CISC架构的CPU【45】——要求超过八百万条产生式【117】。这由于几个原因:不同的寄存器类别以及取址模式要求具有不同非终结符的独立产生式;因为规则是排序的,满足交换律的操作要求复制受影响的规则。这个问题可以通过仔细地重整语法而缓解。在VAX机器的情形中,这将语法的规模降低至大约1000条规则。不过,重整很容易导致代码质量下降。

第三, 随着解析器不带回溯地从左到右遍历,必须执行一个操作数的决策,而不能考虑其他操作数。这会可能做出导致低效代码的不正确的决定,要产生正确的代码,在后面必须撤销这个决定。最后,生成代码的质量很大程度上依赖于语法如何表述,这样要求实现者详尽地知道指令选择器内部如何工作。

3.2.1. 使用属性语法增强语法解析

Ganapathi与Fischer【104,105,106,107】通过使用属性语法【148】替换常规的上下文无关语法,扩展了Glanville与Graham的工作。这样语法的主要好处是允许在解析时解决归纳-归纳冲突,而不是在生成表的时候。这会导致更高效的代码,因为在归纳决策中可以考虑当前上下文。

属性语法[1]允许语义信息被表达为合成或继承属性。它们分别表示为↑与↓,因为前者沿着树向上传递,而后者向下传递。属性可以捆绑进谓词(predicate)及动作(action)符号。谓词用于检查产生式的适用性,而动作用于产生新的合成属性。那么,在应用多个规则的归纳情形里,谓词被依次检查,直到找到一个所有谓词都成立的规则。

图3.6是在一台有递增、两地址、三地址形式ADD指令的机器上处理加法的规则的一个例子。尽管某些指令比其他指令短,它们仅能用于某些情形。例如,规则1与2(两者在交换律下是等价的)仅当其中一个操作数是1且另一个操作数的值在别处不使用时才适用。最后的条件也适用于规则3与4。最后的规则是其他都失败时选中的缺省产生式。因此谓词确保规则仅被用在它们适用的上下文里,这样使得归纳-归纳冲突随着代码的产生得到解决,而不是在表生成时。这也导致了语法的简化;对于VAX机器的1000条规则,数量降至大约200条【7】。

[1]后来Ganapathi与Fischer更倾向于词缀语法(affix grammars)这一术语。



图3.6:Ganapathi-Fischer记法表示的指令集语法
属性语法一个优雅的特性是它们方便了机器描述的递增开发。这意味着可以先实现最通用的规则,来获得一个产生正确但低效代码的机器描述。然后,通过持续为更复杂的机器指令添加规则来提升代码质量。语法可以递增式地改进。因此,在实现的代价与代码的质量间取得平衡是可能的。这是重要的,因为不总是可以有大量的时间用于重指向一个编译器,能让它工作就可以了。另外,其他优化,比如常量折叠,也可以被表示为语法。

不过,为了允许单遍、从左到右的解析,属性语法的特性必须受到限制。首先,动作符号只可能出现在产生式的最右端。其次,继承属性不能出现在产生式的左手侧。这是因为使用了自底向上的解析,从表达式树的叶子开始产生代码。因为只有子树被归纳后,继承属性才可用,显然它们不能用于引导这些归纳。此外,在兄弟树间交流信息是不可能的,因为只有两者都被创建后,它们的兄弟关系才成立。结果,信息仅能从左到右及向上流动。这意味着尽管对于操作,机器指令选择不偏向任何操作数,取址模式选择仍然唯有从左到右执行。

总之,Ganapathi与Fischer通过延伸线性化句法分析的语法,扩展了Glanville与Graham的思想,并提高了代码质量。不过,仍然可能生成次优的代码,并且生成的代码质量仍然严重依赖语法如何形成,以至于在设计属性及谓词时需要小心。此外,把机器特定的限制表达为谓词,比如取址模式上的寄存器限制,是一项单调而困难的任务。

3.2.2. 优化的努力

这些局限的主要原因是模式的匹配与模式的选择实际上合并在单个步骤中(这将在3.4节进一步讨论)。尽管这是在表达式树上以单遍生成代码愿望的一个结果【205】,这个痴迷早已过时了。

Christopher等【49】尝试通过实现一个使用Glanville-Graham方法的思想,提取所有可能的解析(即所有匹配与选择的组合),然后使用动态规划选择最优的指令选择器来解决这个顾虑。出于效率的原因,他们用Earley算法【70】的一个实现替代了LR(1)解析器。尽管这个方法设法产生了优化的代码,它要求枚举所有可能解析,在实践上代价太高。

在2000年Madhavan等【167】扩展Glanville-Graham方法来实现据称在线性时间内生成优化代码。通过纳入由Shankar等【209】开发的一个LR解析的新变形,之前在匹配中执行的归纳,现在被允许推迟任意步。因此指令选择器实质上记录了多个并发的解析,这允许它在承诺一个可能次优的决定前,收集可用模式的足够信息。虽然看上去有指望,该技术的影响似乎极其有限,因为该论文的引用数很低。因此,它可能需要进一步调查。

近期,Yang【241】提出了一个类似的技术,涉及使用parser cactuses【sic】,它减少了记录不同解析树所要求的的空间。不过,该论文没有传达这个改进的重要性,因为它缺少试验数据及数学形式描述。

毫无疑问,未来还会改进,但这些仅是算法的微调——语法解析的基本方法保持不变。

3.3. 自顶向下,穷举搜索

尽管大多数指令选择器在遍历表达式树时以自底向上的方式工作,也有自顶向下生成代码的尝试。两者的区别是细微的且与上下文处置相关。在这个情形下,上下文必须包括产生机器代码所需的语义信息,比如一个特定值分配在哪个寄存器。在自底向上方法中,上下文从叶子向根节点慢慢移动。这通常通过在遍历过程中部分重写树来实现(即匹配的子树被一个节点替代,该节点表示选中机器指令输出所在的寄存器类别),或者,就像在语法方式中,通过弹出并替换栈上的项。相比之下,自顶向下的做法把上下文向下传递,因此是目标驱动的,因为一个选中的模式强制要求该树较低部分后续的模式选择必须满足约束条件。

这样的结果是,最优的自底向上代码生成器要求问题表现出最优子结构的特性——即最优代码可以通过最优地解决每个子问题来生成。正如我们在后面章节将看到的,这是很少出现的。另一方面,最优的自顶向下的做法依赖于回溯,这允许修订后来被证明是次优的不成熟的决定。不过,这样的算法通常必须包括穷举搜索,这是令人生畏的代价,要求聪明的做法来裁剪查找空间。

这样一个目标导向做法由Cattell等【40,43,162】在他们的PQCC(产品级编译器的编译器,Production Quality Compiler-Compiler)实现中开发出来,其中包括Wulf等【239】开发的Bliss-11编译器的衍生物。机器指令被表示为使用递归构造构成的模板,因此类似于语法规则。不过,这些模板不是手写的,而是从机器描述自动地推导,这是Glanville与Graham做法的一个改进。从输入树的根开始,一个算法递归地遍历这棵树并尝试把模板匹配到尽可能大的子树。根据一个匹配,产生选中模板的结果序列。当仅有一个轻微不匹配时(比如
一个操作数没有在要求的位置),算法尝试转换这棵树,使它匹配一个模板。如果有多个匹配,应用一个启发式选择有最少装载与存储的模板。

Cattell等做法的主要缺点是,为了追求最好的解决方案,它枚举了所有可能的解决方案,这过于昂贵。此外, 推断模板的技术是基于弱AI技术的启发式,这可能产生低收益的模板。

另一个穷举、表驱动的做法由Krumme与Ackley【154】提出,他们应用了一个分支界限算法[1]来尝试控制组合复杂度。分支界限是一个优化技术,其中当前找到的最好方案被记下,并裁剪所有被证明会产生最差方案的搜索树。该方法实现的一个DEC-10
C编译器的整体代码生成器还允许把代码大小——许多编译器中不常见的特性,作一个因素记入优化目标。不过,不像Glanville与Graham及Cattell等,Krumme与Ackley不提供一个方式来自动生成这些表。另外,该算法不需要重新访问搜索树的许多部分。

最近,Nymeyer等【180,181】开发了一个方法,其中自底向上重写系统(BURS)原理与A*搜索【198】结合,这是控制搜索空间的另一个方法。(我们将在3.6节详细讨论BURS原理)。BURS语法具有超过其他许多机器描述的优点,在于它们允许转换规则(比如把“X + Y”重写为“Y + X”)成为语法的部分。这导致更小、更简单的语法,因为前面的问题,像交换性可以更容易地处理。不过,作者没有发表任何试验结果来保证其在实践中的适用性。

[1]实际上,在他们的论文里,Krumme与Ackley宣称使用剪枝算法(α-β裁剪pruning)【198】,但这样一个搜索对代码生成没有意义。此外,他们的算法描述听起来更像分支界限算法。
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: