一个简单的方法是将窥视孔优化器实现为有限状态机。
我们假设您有一个生成指令但不发出指令的原始代码生成器,以及一个将实际代码发送到对象流的发出例程。
状态机捕获您的代码生成器生成的指令,并通过在状态之间转换来记住 0 个或多个生成的指令序列。因此,一个状态隐含地记住了一个(短)生成但未发出的指令序列;它还必须记住它捕获的指令的关键参数,例如寄存器名称、常量值和/或寻址模式和抽象目标内存位置。一个特殊的开始状态会记住空的指令串。在任何时候,您都需要能够发出未发出的指令(“flush”);如果你一直这样做,你的窥视孔生成器会捕获下一条指令,然后发出它,不会做任何有用的工作。
为了做有用的工作,我们希望机器捕获尽可能长的序列。由于机器指令通常有很多种,实际上您不能连续记住太多,否则状态机将变得庞大。但是对于最常见的机器指令(加载、添加、cmp、分支、存储),记住最后两个或三个是很实用的。机器的大小实际上将由我们关心的最长窥视孔优化的长度决定,但如果该长度为 P,则整个机器不需要 P 个状态深度。
每个状态都根据我由您的代码生成器生成的“下一个”指令转换到下一个状态。想象一个状态代表 N 条指令的捕获。过渡选择是:
- 刷新此状态表示的最左边的 0 条或更多(称为此 k)指令,并转换到下一个状态,表示 N-k+1,表示额外捕获机器指令 I 的指令。
- 刷新此状态表示的最左边的 k 条指令,转换到表示剩余 Nk 条指令的状态,并重新处理指令 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 显示了两种不同风格的转换,一种使用生成的指令,另一种不使用;要么适用于这个例子。当你最终得到几百个这样的 case 语句时,你会发现这两种类型都很方便,“不使用”版本可以帮助你保持代码更小。
我们需要一个例程来刷新窥视孔优化器:
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 解析器状态机生成。我不会在这里解释这个:-}