これを行う簡単な方法は、ピープホール オプティマイザーを有限状態マシンとして実装することです。
命令を生成するが命令を発行しない生のコードジェネレーターと、実際のコードをオブジェクト ストリームに送信する発行ルーチンがあると仮定します。
ステート マシンは、コード ジェネレーターが生成する命令をキャプチャし、状態間の遷移によって生成された 0 個以上の命令のシーケンスを記憶します。したがって、状態は、生成されたが発行されていない命令の (短い) シーケンスを暗黙的に記憶します。また、レジスタ名、定数値、アドレッシング モード、抽象的なターゲット メモリ位置など、キャプチャした命令の主要なパラメータを記憶する必要もあります。特別な開始状態は、命令の空の文字列を記憶します。いつでも、送信されていない命令 (「フラッシュ」) を送信できる必要があります。これを常に行うと、ピープホール ジェネレーターが次の命令をキャプチャしてから発行し、有用な作業が行われません。
有用な作業を行うには、マシンができるだけ長いシーケンスをキャプチャする必要があります。通常、マシン命令には多くの種類があるため、実際問題として、連続して覚えることができません。そうしないと、ステート マシンが巨大になります。ただし、最も一般的なマシン命令 (load、add、cmp、branch、store) については、最後の 2 つまたは 3 つを覚えておくと便利です。マシンのサイズは実際には、実行したい最長のピープホール最適化の長さによって決まりますが、その長さが 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 は、2 つの異なるスタイルの遷移を示しています。1 つは生成された命令を使用するもので、もう 1 つは使用しないものです。この例ではどちらでも機能します。これらの 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 パーサー ステート マシンの生成です。ここでは説明しません:-}