LET'S BUILD A COMPILER!(3)

来源:互联网 发布:java 释放mysql连接池 编辑:程序博客网 时间:2024/04/30 10:23

                       LET'S BUILD A COMPILER!(3)---第二部分:表达式分析续

括号

我们可以为分析器添加进处理括号的部分。括号是一种强制改变运算优先次序的机制。比如,表达式
               2*(3+4) ,

括号强制使加法在乘法之前运算。更重要的是,括号为我们提供了一种定义任意复杂度的表达式的机制。比如表达式
               (1+2)/((3+4)+(5-6))

将对括号的处理加到我们的分析器中,其关键在于认识到一个被括号括起来的表达式无论它有多复杂,相对于括号外的其他部分来说它看起来只是相当于一个简单的因子而已。所以term的一种表示形式就是:
          <factor> ::= (<expression>)

这已经涉及到递归的概念。一个表达式可以包含term,而这个term也可以包含另一个表达式,这个表达式又可以包含term…….以至于无穷。

不管它是否看起来很复杂,我们只要加进少数几条语句到Factor过程中就可以解决这个问题。

{---------------------------------------------------------------}

{ 分析并翻译因子 }

procedure Expression; Forward;

procedure Factor;

begin

   if Look = '(' then begin

      Match('(');

      Expression;

      Match(')');

      end

   else

      EmitLn('MOVE #' + GetNum + ',D0');

end;

{--------------------------------------------------------------}

请再次留意,我们对这个分析器的扩展是很容易的,并且这些代码与BNF产生式匹配得也很好。

像以前一样,编译运行这个新版本的分析器,确保它能正确地分析合法的语句,并对非法语句给出错误信息。

 

一元减法

到现在为止,我们似乎已经能让这个分析器处理任何形式的表达式了,是这样吗?OK,输入这个语句试试:
                         -1

噢!它不能正常工作,是吗?Expression过程希望每个表达式都以一个整数开始,所以它不能识别开头的减号。你会发现分析器不能识别+3,也不能识别
                    -(3-2) .
这样的表达式。

有两种方法解决这个问题。最简单的方法(尽管不是最好的方法)是假想成表达式开头有一个0,所以-3就变成了0-3。我们可以很容易的用这种方法修正Expression现存的版本。

{---------------------------------------------------------------}

{分析并翻译表达式}

procedure Expression;

begin

   if IsAddop(Look) then

      EmitLn('CLR D0')

   else

      Term;

   while IsAddop(Look) do begin

      EmitLn('MOVE D0,-(SP)');

      case Look of

       '+': Add;

       '-': Subtract;

      else Expected('Addop');

      end;

   end;

end;

{--------------------------------------------------------------}

我在前面说过对源程序做修改是很容易的!这次修改我们只增加了3行新代码。注意一下新引用的函数IsAddop。因为对加法操作符的检验出现了两次,于是我为其定义了一个新的函数。函数IsAddop应该和函数IsAlpha一样直观易懂。它的代码如下:

{--------------------------------------------------------------}

{ 识别加法运算符 }

function IsAddop(c: char): boolean;

begin

   IsAddop := c in ['+', '-'];

end;

{--------------------------------------------------------------}

OK,对我们的程序做如上更改并重新编译。你应该把函数IsAddop也包含到你的程序中。后面还需要用到它。现在试着再次输入-1。呜!产生的代码的效率真是低得可怜……6行代码都只是为了装载一个简单的常数……但至少它是正确的。记住,我们这里的目的不是为了取代Turbo Pascal。

到现在为止我们的工作都只是完成了我们的表达式分析器的结构。目前的版本应该可以正确地分析和编译你刻意输入的任何形式的表达式。但它仍然局限于处理term仅为单个十进制数字的情况。但是我希望现在你已意识到我们能对分析器进行更大的扩展,而只需要做一些小的修改就可以了。也许听到一个变量甚至一个函数调用都只不过是另一种形式的factor,你都不会感到惊讶。

下一节里,我会向你展示如何将我们的分析器扩展到能处理上述问题,以及如何扩展将其扩展到能处理多个字符组成的数字和变量名。你会看到我们离一个真正实用的分析器已经不远了。

 

关于代码优化

本节的开头部分,我说过会给出一些关于如何提高目标代码质量的提示。虽然我也说过本系列教程的主要目的不是要制造紧凑的代码。但是你至少应该明白我们在这里不是在浪费时间……因为我们的确可以对此分析器做进一步修改以使它生成更好的代码,而不是把我们迄今为止所作的所有工作通通扔掉、另起炉灶。和以前碰到的情况一样,完成某些优化措施也不是那么困难的……只需要在分析器中添加一些额外的代码。

我们有两种基本优化方法:

  o目标代码生成后对其改进

这就是“窥视孔”(peephole)优化法的原理。通常的想法是我们知道编译器将会生成什么样的指令组合,我们也知道哪些指令组合是效率低下的(比如上面为-1生成的代码)。所以我们要做的就是扫描已生成的代码,找到那些没有效率的指令组合,并用更好的高效的指令组合取代它们。这种方法在一定程度上会使代码剧烈膨胀,但却是一种相当直接的模式匹配的方法。唯一复杂的地方就是也许会有许多这样的组合需要查找。而这种方法被称为窥视孔优化法的原因就在于它一次只在一小组指令中查找。窥视孔优化法对目标代码的优化效果很明显,几乎不用对编译器自身的结构做任何改动。尽管如此,在编译器的速度,体积和复杂性上还是要付出一定的代价。查找所有的那些指令组合要使用很多的IF测试,每个IF都可能是产生错误的根源,它当然也要耗费不少时间。

在经典的窥视孔优化器的实现中,它是作为编译器的第二遍进行的。第一遍输出的代码被写到磁盘上,然后优化器读取并处理目标代码文件。事实上,你会发现优化器甚至可以成为一个与整个编译器相对独立的程序。因为优化器只是从一个小“窗口”中检视指令(这就是此方法名称的由来),一个更好的实现方法是将若干行的输出代码放到缓冲区中,然后在每条EmitLn语句后扫描缓冲区。

  o一开始就生成质量更好的代码

这种方法要求我们在调用Emit前就查找源程序中某些特定的情况。作为一个不太典型的例子,在做加0的加法时,我们应该定义常数0,并调用Emit过程生成一条CLR语句代替load指令,或者干脆什么都不做。举个更接近的例子,假如我们要在Factor过程中识别一元减,而不是在Expression过程中识别,我们可以将-1当作普通的常数看待,而不是从它们的绝对值来生成负数。这些工作都不难完成……只需要再加入一些额外的代码即可。对这种方法我的看法是,一旦我们有了一个能运行起来的编译器,它可以生成有用的能执行的代码,我们就可以经常回头完善它,使它生成的代码更加紧凑。这就是世界上为什么会有2.0版本的原因。

还有其他的优化方法值得一提,它们毫无疑问也能生成漂亮的紧凑的代码。我觉得下面要介绍的方法是我的“发明”,尽管我不能证明它是我的原创,但我没有在任何已出版的刊物上看到过。

实际上,这种方法也就是更多地使用CPU的寄存器,从而避免过多地使用栈。回想一下我们只做加法和减法时,我们使用的是寄存器D0和D1,而不是用的栈,记起来了吗?这种方法工作良好,因为对那两种运算,“栈”所用的操作数不会超过两个。

68000处理器有8个寄存器。为什么不用它们来构造一个我们自己管理的栈呢?关键在于认识到,在处理过程中的任何时刻,分析器都知道栈中到底有多少个操作数,所以分析器一定能够正确地管理这个栈。我们可以设置一个自定义的“栈指针”用来指示栈顶,并且用来定位相应的寄存器。举例来说,Factor过程将不会再直接将数据放到寄存器D0中,而是放到碰巧当前处于“栈顶”位置的寄存器中。

我们要做的事就是要有效地用寄存器组成的、由我们自己管理的栈来取代CPU管理的内存栈。对多数的表达式来说,栈的深度不会超过8,所以我们能够得到相当漂亮的目标代码。当然,我们也不得不处理那些不常见的栈深超过8的表达式,但那也不是问题。我们只要简单地让我们的栈溢出到CPU管理的栈中即可。对栈深超过8的表达式,目标代码质量不会比我们先前生成的代码差,而对那些栈深小于8的表达式,目标代码质量就要好的多。

为了在教程中写下上述观点,我已经事先实现了此方法,以确保它是可行的。真正实践的时候,你会发现你并不能真正把8个寄存器都用来构造栈……你至少需要闲置一个寄存器用来交换除法运算中操作数的次序(真希望68000有XTHL这样的指令,就像8080处理器一样!)。对包含函数调用的表达式,我门还需要为函数调用保留一个寄存器。即使这样,对大多数表达式来说,目标代码的质量仍然有很大的提高。

现在你已经看到了,要生成质量更好的代码并不是那么困难,但也的确会增加编译器的复杂性……因此,我强烈建议我们在本教程的后继部分继续忽略效率问题,希望你记住的是我们的确可以提高代码质量,而不需要另起炉灶将我们已经完成的工作扔掉。

 

下一课,我会讲解如何处理变量形式的factor和函数调用,也会继续演示处理多字符的token和空白字符是件多么容易的事。