33

ローカル変数をレジスタに割り当てる方法を探しています。それを行うためのいくつかの深刻な方法(つまり、ウィキペディアで言及されているもの)を知っていますが、「こぼれ」がどのように達成されるかについては固執しています。また、関連する文献は非常に威圧的です。私の優先事項を満たすもっと簡単なものがあることを願っています:

  1. 正しさ -- ローカル変数の数に関係なく、正しいコードを生成するアルゴリズム。
  2. シンプルさ -- あまり文献を読まなくても理解できるもの。
  3. 効率 -- 次のような現在の方法よりも優れている必要があります。

x = y # z操作を次のように変換します。

movl y, %eax
movl z, %ebx
op %ebx, %eax
movl %eax, x

Intel 386 をターゲットにしているため、関連する制約は次のとおりです。

  • 二項演算は 2 つの引数を取り、そのうちの 1 つはソースと宛先です。単項演算は単一の引数を取ります。
  • 操作は 1 つのメモリ ロケーションにのみアクセスできます。したがって、二項演算には、レジスタに少なくとも 1 つの引数が必要です。
  • 使用できるレジスタは最大 6 つです%eax %ebx %ecx %edx %esi %edi。(%ebp最後の手段として含めることもできます。)
  • 整数除算や戻りレジスタなどの特殊なケースもありますが、今のところ無視できます。

現時点で、コンパイラーが通過する 3 つのステップがあります。

  • i386ification: すべての演算はフォームa = a # b(またはa = #a単項演算) に変換されます。
  • ライブネス分析: 各操作の前後のライブ変数のセットが決定されます。
  • 登録割り当て: 干渉グラフが作成され、色付けされます。

そして、コンパイラはクレヨンを空中に投げ、次に何をすべきかわかりません。

public int mf(int cr, int ci) {
    int i = 0;
    int zr = 0;
    int zi = 0;

    while (i < 100 && zr*zr + zi*zi < 4) {
        int t = zr * zr - zi * zi + cr;
        zi = 2 * zr * zi + ci;
        zr = t;

        i = i + 1;
    }
    return i;
}

これは、関数のかなりきれいな干渉グラフと、活性情報を含む CFG です。残念ながら、CFG 画像には垂直方向のスクロールが必要です。

7色使用しました。それらの1つ(またはその色が割り当てられた変数のセット)をこぼしたいと思います。あまり重要ではない選択方法。こぼれた変数をどのように処理するかは、注意が必要です。

t変数のセット$t4である「ピンク」をこぼしたとしましょう$t7。これは、これらの変数の 1 つを参照する操作は、レジスターではなく、スタック フレーム上のその位置からアクセスすることを意味します。これは、この例では機能するはずです。

しかし、プログラムが次の場合はどうでしょう。

...
a = a + b
...

両方abもこぼれなければなりませんでしたか?addl b, a2 つのメモリ アドレスを持つ命令を発行できません。オペランドの 1 つを一時的に保持する別のスペア レジスタが必要です。これは、別の色をこぼすことを意味します。これは、次の一般的な方法を示唆しています。

  1. すべての変数に色を付けることができればr、すばらしいことです。
  2. それ以外の場合は、いくつかの色とそれに関連する変数をスピルします。
  3. スピルされた 2 つの変数にアクセスする操作が存在する場合は、別の色をスピルし、そのようなすべての操作の一時記憶域としてスペア レジスタを使用します。

この時点で、必要以上に多くのものがこぼれているのではないかと思います。変数自体ではなく、変数の有効期間の一部をこぼすなど、何か賢い方法でこぼす方法はないかと考えています。ここで使用できる簡単な(っぽい)テクニックはありますか?繰り返しますが、私は特に高いところを目指しているわけではありません。確かに、何かを深く読む必要があるほど高くはありません。;-)

特定の問題

具体的な主な問題は、変数がスピルされた場合、生成される命令にどのような影響があるかということです。その変数を使用するすべての命令は、(スタック位置から) メモリ内で直接アクセスする必要がありますか? 操作が 2 つのスピルされた変数を使用する場合、これはどのように機能しますか? (アーキテクチャでは、命令が 2 つの異なるメモリ位置にアクセスすることはできません。)

二次的な問題は次のとおりです。

  • 正確さ (そしてそれほど重要ではありませんが効率) のために、ロード/ストア命令を挿入する場所を決定するにはどうすればよいですか?
  • 変数がすぐに使用されない場合に、その有効期間のその部分だけをスピルし、後でアンスピルすることはできますか? すべての命令がスピルされていないレジスタで動作するようにします。変数は、異なる時点で異なるレジスターに存在する可能性があります。
  • 特殊なケースでもう少し効率的にできますか。例えば%eax戻り値には が使われているので、たまたま戻り値が出るまでにそのレジスタに戻り値の変数が割り当てられていればいいのですが。同様に、一部のレジスタは「callee-save」であるため、関数呼び出し時にアクティブな変数が少ない場合、それらを呼び出し先保存以外のレジスタに割り当てると、それらのレジスタの格納を回避できます。
  • SSAフォームは(もしあれば)大いに役立ちますか?一般的な部分式を削除して定数を評価できるようになれば、レジスタの負担が軽減 (?) される可能性がありますが、それ以外の場合は効果がありますか?

私が(今のところ)気にしていない側面は次​​のとおりです。

  • スタックの割り当てと最適化: すでに単純に実装されており、必要に応じて干渉グラフを使用して最適化できます。
  • 終了する限り、コンパイル時の効率。(NP 完全性は、特定のアルゴリズムを避ける必要があることを意味するものではありません。)

アップデート

ダウンタイムについて申し訳ありません-私は与えられた答えについて考えていて、いくつかのアイデアの実装を開始するための簡単なアプローチを見つけようとしています. 正直、先延ばしにしていました... :-\

とても素敵なプレゼンテーションを見つけました (PPT、悲しいことに):

http://www.cs.princeton.edu/courses/archive/spr05/cos320/notes/Register%20Allocation.ppt

これは、特定の操作のニーズに対処する方法に関する質問に答えます (ソースと宛先に同じレジスタを使用する、または一部の操作に特定のレジスタを必要とするなど)。私が確信していないのは、Liveness-Coloring-Allocation サイクルが終了するかどうかです。

すぐに実際の作業を行い、うまくいけば質問を閉じます。

4

2 に答える 2

12

JVM アロケーターで貪欲なアプローチを使用したことがありますが、これはかなりうまく機能しました。基本的に、すべての値がスタックに格納されている基本ブロックの先頭から開始します。次に、命令を順方向にスキャンし、値を含むレジスタのリストを維持し、値がダーティかどうか (書き戻す必要があるかどうか) を確認します。命令がレジスターにない (または正しいレジスターにない) 値を使用する場合は、ロード (または移動) を発行して、命令の前に空きレジスターに入れます。命令が値を書き込む場合は、それがレジスターにあることを確認し、命令の後にダーティ マークを付けます。

レジスターが必要になった場合は、使用済みのレジスターから値の割り当てを解除してスピルし、ダーティでライブの場合はスタックに書き込みます。基本ブロックの最後で、ダーティ レジスタとライブ レジスタを書き戻します。

このスキームにより、すべてのロード/ストアがどこに行くのかが正確に明確になり、行くにつれてそれらを生成します。これは、メモリ内の値を取る命令、またはメモリ内の 2 つの引数のいずれかを取ることができるが両方ではない命令に簡単に適応できます。

すべての基本ブロック境界ですべてのデータをスタックに配置しても問題ない場合、このスキームはうまく機能します。基本的に非常に似たようなことを行うため、基本ブロック内の線形スキャンと同様の結果が得られるはずです。

スピルする値と割り当てるレジスタを決定する方法について、任意に複雑になる可能性があります。たとえば、各値を特定のレジスタでマークして、基本ブロックのある時点に配置する必要がある場合 (たとえば、戻り値の場合は eax、シフト量の場合は ecx)、値が最初に割り当てられます(そして、他の割り当てのためにそのレジスタを回避します)。しかし、アルゴリズムの正確性を改善ヒューリスティックから切り離すのは簡単です。

このアロケーターは、SSA コンパイラー YMMV で使用しました。

于 2010-01-04T22:48:18.590 に答える
8

最初に: それを行うスマートな方法はありません。問題はNP完全です;-)

こぼれる方法:

レジスタ割り当てアルゴリズムを実行し、スピルする必要がある変数のリストを取得します。これで、関数の先頭でスタックにスペースを割り当てることができます。スピルされたすべての変数をスタック上の場所にリンクします。オーバーラップしないライブ範囲でメモリをスマートに合体させたい場合。レジスタをスピルする必要があるときはいつでも、それをメモリに保存し、再び必要になったときにロードします。

eaxの扱い方:

レジスターを使用済みとしてマークしますが、そこに変数を保管しません (事前割り当て)。これにより、コード ジェネレーターがそのレジスターをクリアします。有益な場合は、スマートに値を別のレジスタに格納します。

こぼれを処理する簡単で正しい方法:

すべてこぼすだけ。これは、すべての変数の有効範囲がプログラム全体であると想定しています。これは、LRU や使用カウントなどを使用して解放するレジスタを選択することで拡張できます。

次善の策は、おそらくリニア スキャン レジスタの割り当てです。事前割り当てを使用する場合でも、実装は非常に簡単です。リンク先の紙を調べることをお勧めします。

具体的な回答

  1. あなたにとって正しさとは何ですか?単純な割り当てアルゴリズムでも、プログラミング エラーがなければ正しいものです。(数学的) 正しさを証明することは、はるかに困難です。値/レジスタが再度必要になる前に、ロードとストアの両方を挿入する必要があります。値が保存/作成された後に両方を挿入する必要があります。

  2. はい。そのようにプログラムする場合。アルゴリズムがライブタイム中に複数のレジスタの値を処理できる場合は、それらの最適化を使用できます。

  3. 特定の改善を実装するのは、あなた次第です。1 つの可能性は、プログラム全体ではなく、必要なときにのみ eax をブロックすることです。

  4. 特定の条件下では、SSA が役に立ちます。SSA コードの推論グラフは常にchordalです。つまり、3 つを超えるノードを持つサイクルはありません。これはグラフの彩色の特殊なケースであり、多項式時間で最小の彩色を見つけることができます。SSA への変換は、必ずしもレジスタ プレッシャーの多かれ少なかれを意味するわけではありません。通常、SSA フォームにはより多くの変数がありますが、有効期間が短くなる傾向があります。

于 2009-12-25T22:09:48.947 に答える