【问题标题】:Compiler written in Java: Peephole optimizer implementation用 Java 编写的编译器:Peephole 优化器实现
【发布时间】:2012-05-19 12:03:46
【问题描述】:

我正在为 Pascal 的一个子集编写一个编译器。编译器为制造的机器生成机器指令。我想为这种机器语言编写一个窥孔优化器,但是我在替换一些更复杂的模式时遇到了麻烦。

窥视孔优化器规范

我研究了几种不同的方法来编写窥视孔优化器,并确定了一种后端方法:

  • 每次生成机器指令时,编码器都会调用 emit() 函数。
  • emit(Instruction currentInstr) 检查窥视孔优化表:
    • 如果当前指令匹配模式的尾部:
      1. 检查之前发出的匹配指令
      2. 如果所有指令都匹配该模式,则应用优化,修改代码存储的尾部
    • 如果没有找到优化,照常发出指令

当前的设计方法

该方法很简单,但我在实现时遇到了麻烦。在我的编译器中,机器指令存储在 Instruction 类中。我写了一个InstructionMatch 类存储用于匹配机器指令的每个组件的正则表达式。如果模式匹配某些机器指令instr,则其equals(Instruction instr) 方法返回true

但是,我无法完全应用我拥有的规则。首先,我觉得鉴于我目前的方法,我最终会得到一堆不必要的东西。鉴于窥视孔优化数字的完整列表可以包含大约 400 个模式,这将很快失控。此外,我实际上无法使用这种方法获得更困难的替换(请参阅“我的问题”)。

替代方法

我读过的一篇论文将先前的指令折叠成一个长字符串,使用正则表达式进行匹配和替换,并将字符串转换回机器指令。这对我来说似乎是一个不好的方法,如果我错了,请纠正我。

示例模式、模式语法

x: JUMP x+1; x+1: JUMP y  -->  x: JUMP y
LOADL x; LOADL y; add     -->  LOADL x+y
LOADA d[r]; STOREI (n)    -->  STORE (n) d[r]

请注意,这些示例模式中的每一个都只是以下机器指令模板的人类可读表示:

op_code register n d

(n通常表示字数,d表示地址位移)。语法x: <instr> 表示指令存储在代码存储中的地址x

因此,当LOADL 操作码为5 时,指令LOADL 17 等价于完整的机器指令5 0 0 17(此指令中未使用nr

我的问题

因此,鉴于这种背景,我的问题是:当我需要在替换中包含先前指令的部分作为变量时,如何有效地匹配和替换模式?例如,我可以简单地将LOADL 1; add 的所有实例替换为增量机器指令——我不需要前面指令的任何部分来执行此操作。但是我不知道如何在替换模式中有效地使用我的第二个示例的“x”和“y”值。

编辑:我应该提到Instruction 类的每个字段只是一个整数(对于机器指令来说是正常的)。模式表中任何使用 'x' 或 'y' 的变量都是代表任何整数值的变量。

【问题讨论】:

    标签: java compiler-construction compiler-optimization


    【解决方案1】:

    一种简单的方法是将窥视孔优化器实现为有限状态机。

    我们假设您有一个生成指令但不发出指令的原始代码生成器,以及一个将实际代码发送到对象流。

    状态机捕获您的代码生成器生成的指令,并通过在状态之间转换来记住 0 条或更多条生成的指令序列。因此,一个状态隐含地记住了一个(短)生成但未发出的指令序列;它还必须记住它捕获的指令的关键参数,例如寄存器名称、常量值和/或寻址模式和抽象目标内存位置。一个特殊的开始状态会记住空的指令串。在任何时候,您都需要能够发出未发出的指令(“flush”);如果你一直这样做,你的窥视孔生成器会捕获下一条指令,然后发出它,不会做任何有用的工作。

    为了做有用的工作,我们希望机器捕获尽可能长的序列。由于机器指令通常有很多种,实际上你不能连续记住太多,否则状态机将变得巨大。但是对于最常见的机器指令(加载、添加、cmp、分支、存储),记住最后两个或三个是很实用的。机器的大小实际上将由我们愿意进行的最长窥视孔优化的长度决定,但如果该长度为 P,则整个机器不必是 P 个状态深度。

    每个状态都根据您的代码生成器生成的“下一个”指令转换到下一个状态。想象一个状态代表 N 条指令的捕获。 过渡选择是:

    • 刷新此状态表示的最左边的 0 条或更多(称为此 k)指令,并转换到下一个状态,表示 N-k+1 条指令,表示额外捕获机器指令 I。
    • 刷新该状态表示的最左边的k条指令,转换到该状态 表示剩余的 N-k 条指令,并重新处理指令 I。
    • 完全刷新状态,同时发出指令 I。 【其实你可以 在刚刚开始的状态下执行此操作]。

    在刷新 k 指令时,实际发出的是这些 k 的窥视孔优化版本。您可以在发出此类指令时计算任何您想要的东西。您还需要记住适当地“移位”其余指令的参数。

    这一切都可以通过窥视孔优化器状态变量以及代码生成器生成下一条指令的每个点的 case 语句轻松实现。 case 语句更新窥视孔优化器状态并实现转换操作。

    假设我们的机器是一个增强堆栈机器,有

     PUSHVAR x
     PUSHK i
     ADD
     POPVAR x
     MOVE x,k
    

    指令,但原始代码生成器仅生成纯堆栈机器指令,例如,它根本不发出 MOV 指令。我们希望窥视孔优化器能够做到这一点。

    我们关心的窥视孔案例有:

     PUSHK i, PUSHK j, ADD ==> PUSHK i+j
     PUSHK i, POPVAR x ==> MOVE x,i 
    

    我们的状态变量是:

     PEEPHOLESTATE (an enum symbol, initialized to EMPTY)
     FIRSTCONSTANT (an int)
     SECONDCONSTANT (an int)
    

    我们的案例陈述:

    GeneratePUSHK:
        switch (PEEPHOLESTATE) {
            EMPTY: PEEPHOLESTATE=PUSHK;
                   FIRSTCONSTANT=K;
                   break;
            PUSHK: PEEPHOLESTATE=PUSHKPUSHK;
                   SECONDCONSTANT=K;
                   break;
            PUSHKPUSHK:
            #IF consumeEmitLoadK // flush state, transition and consume generated instruction
                   emit(PUSHK,FIRSTCONSTANT);
                   FIRSTCONSTANT=SECONDCONSTANT;
                   SECONDCONSTANT=K;
                   PEEPHOLESTATE=PUSHKPUSHK;
                   break;
            #ELSE // flush state, transition, and reprocess generated instruction
                   emit(PUSHK,FIRSTCONSTANT);
                   FIRSTCONSTANT=SECONDCONSTANT;
                   PEEPHOLESTATE=PUSHK;
                   goto GeneratePUSHK;  // Java can't do this, but other langauges can.
            #ENDIF
         }
    
      GenerateADD:
        switch (PEEPHOLESTATE) {
            EMPTY: emit(ADD);
                   break;
            PUSHK: emit(PUSHK,FIRSTCONSTANT);
                   emit(ADD);
                   PEEPHOLESTATE=EMPTY;
                   break;
            PUSHKPUSHK:
                   PEEPHOLESTATE=PUSHK;
                   FIRSTCONSTANT+=SECONDCONSTANT;
                   break:
         }  
    
      GeneratePOPX:
        switch (PEEPHOLESTATE) {
            EMPTY: emit(POP,X);
                   break;
            PUSHK: emit(MOV,X,FIRSTCONSTANT);
                   PEEPHOLESTATE=EMPTY;
                   break;
            PUSHKPUSHK:
                   emit(MOV,X,SECONDCONSTANT);
                   PEEPHOLESTATE=PUSHK;
                   break:
         }
    
    GeneratePUSHVARX:
        switch (PEEPHOLESTATE) {
            EMPTY: emit(PUSHVAR,X);
                   break;
            PUSHK: emit(PUSHK,FIRSTCONSTANT);
                   PEEPHOLESTATE=EMPTY;
                   goto GeneratePUSHVARX;
            PUSHKPUSHK:
                   PEEPHOLESTATE=PUSHK;
                   emit(PUSHK,FIRSTCONSTANT);
                   FIRSTCONSTANT=SECONDCONSTANT;
                   goto GeneratePUSHVARX;
         }
    

    #IF 显示了两种不同风格的转换,一种使用生成的 指导,一个不指导;要么适用于这个例子。 当你最终得到几百个这样的案例陈述时, 你会发现这两种类型都很方便,“不消费”版本有帮助 你让你的代码更小。

    我们需要一个例程来冲洗窥视孔优化器:

      flush() {
        switch (PEEPHOLESTATE) {
            EMPTY: break;
            PUSHK: emit(PUSHK,FIRSTCONSTANT);
                   break;
            PUSHKPUSHK:
                   emit(PUSHK,FIRSTCONSTANT),
                   emit(PUSHK,SECONDCONSTANT),
                   break:
          }
          PEEPHOLESTATE=EMPTY;
          return; }
    

    考虑一下这个窥视孔优化器对以下生成的代码做了什么很有趣:

          PUSHK  1
          PUSHK  2
          ADD
          PUSHK  5
          POPVAR X
          POPVAR Y
    

    整个 FSA 方案所做的就是在状态转换中隐藏模式匹配,以及在案例中对匹配模式的响应。您可以手动编写代码,而且代码和调试速度快且相对容易。但是当案例数量变大时,您不想手动构建这样的状态机。你可以编写一个工具来为你生成这个状态机;很好的背景是 FLEX 或 LALR 解析器状态机生成。我不会在这里解释这个:-}

    【讨论】:

    • 哇,这是相当彻底的回应。谢谢你的建议,我一定会在我的设计中考虑这种方法。
    猜你喜欢
    • 1970-01-01
    • 2011-08-24
    • 1970-01-01
    • 1970-01-01
    • 2011-11-07
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多