web-dev-qa-db-ja.com

制御フローと副作用でスタック/レジスタの混合バイトコードを最適化する方法は?

私は次の仮想マシンのバイトコードを最適化する手法を理解しようとしています:

  • バイトコードは、最初の命令から実行が始まる、命令のフラットリストです。

  • スタックバイトコード:i ++、a + b、メソッド呼び出しf(a、b、c)などの命令は、オペランドスタックの上位1、2、Nの値で発生し、その結果をオペランドスタックの最上位に残します

  • スタックの最上位の値を破棄するためのバイトコード。副作用のメソッドを呼び出しても戻り値が必要ない場合

  • N個のローカル変数の配列、ローカル変数からオペランドスタックの最上部へ/から値をコピーするための命令

  • オペランドスタックの最上位にさまざまな述語を持つ無条件GOTO、条件付きJUMP

  • 副作用のある命令:オペランドスタックのトップ値のグローバル変数への読み取り/書き込み

入力は、この仮想マシンの有効なバイトコードになりますが、冗長性があります。一部の命令は到達不能であり、オペランドスタックで計算された変数はポップされ、ローカル変数に格納された一部の変数は上書きされずに上書きされます今まで読んでいるGOTOの中には、最終目的地に直接行くのではなく、最初に別のGOTOに行く人もいます。

まず、ジャンプに続く順方向トラバーサルによって、到達可能な命令がどこにあるかがわかり、到達できない命令を排除できます。しかし、他の方法はどうですか:未使用の値、または冗長なGOTOを計算するコードを最適化しますか?

副作用のないプログラムについては、戻り値から始めてデータフローグラフを計算し、逆に歩いてどの計算値が未使用であるかを確認できます。しかし、データフローグラフはそれらを無視するだけなので、順序を保持する必要のある副作用がある場合、それは機能しないようです。おそらく、データフローグラフに加えてコントロールフローグラフが必要になりますが、2つのグラフがどのように連携してバイトコードを最適化するのに役立つかはわかりません。

順序を保持する必要のある制御フローとグローバルな副作用(読み取りと書き込みの両方)が存在する場合に、それらを最適化するためにどのような種類のテクニックを使用できますか? CPS、SSA、またはその他の中間表現形式はこれに役立ちますか?

5
Li Haoyi

このトピックについては非常に多くの調査が行われているため、コンパイラの本を読むことをお勧めします。 Muchnikの本 は参考資料としてうまく機能します。私は本当に モダンコンパイラの実装 が好きですが、絶版だと思います。

あなたがコードを読むことでよりよく学ぶような人なら、Scalaコンパイラーは上記のすべてを行い、JVMはVMにリストしたすべての箇条書きに適合します。

もっと要点:

  • スタックを使用しない表現を強くお勧めします。コードを変更するときに一貫性を保つのは面倒です。あるブランチのスタックに値をプッシュする命令を削除した場合、他のブランチにもドロップを追加する必要があります。そうしないと、マージポイントでスタックの一貫性が失われます。 。すべての最適化パスが完了した後、レジスタベースのコードからスタックベースのコードを簡単に生成できます

  • 「デッドコード」を削除したい。表現がすでに単純な形式である場合(ANFはCPSよりもはるかに単純であり、私はそれをうまく使用していると思います-おおまかに言って、すべての式を単純な2オペランド式と一時式に分割します)、単に「ライブ」としてマークするだけで効果的です指示。次に、「ライブネス」情報が必要になります。これは、後で使用される割り当てを計算します(すべてのライブ命令から始めて逆戻りし、入力を「ライブ」としてマークするなど)。トリッキーなビットはCFGのマージポイントにあり、保守的に概算する必要があります(基本的に、どの分岐が行われたかの不確実性を説明するために、プログラムについての知識はますます少なくなります)。

  • 最も簡単な方法は、制御フローを主要なデータ構造として維持し、必要に応じてデータフロー情報(Livenessなど)を計算することだと思います

  • 選択肢がある場合は、CFGではなくツリーを使用してください。ツリーはプログラムの構造(while、ifs)を保持しますが、条件付きジャンプと無条件ジャンプはより強力であり、AST(forインスタンス、CFGで支配者を見つける)

  • 各最適化パスでより多くのデッドコードが生成される可能性があるため、パイプラインでデッドコードを数回実行する可能性があります。それは大丈夫です。通常、すべての最適化パスをよりスマートに、そして不必要に複雑にするよりも優れています。

7
Iulian Dragos

あなたが参照しているものはそれぞれ、独自の最適化アルゴリズムを必要としています/持っています。オプティマイザーは、シーケンスで実行される最適化アルゴリズムのコレクションであり、多くの場合、変更がなくなるまで繰り返されます。

まず、ジャンプに続く順方向トラバーサルによって、到達可能な命令がどこにあるかがわかり、到達できない命令を排除できます。しかし、他の方法はどうですか:未使用の値、または冗長なGOTOを計算するコードを最適化しますか?

未使用の式は、使用されていない最後の値を特定し、その値を生成する命令を排除することにより、一度に1つの命令を最適化(つまり、排除)します。一度に1つの命令で、未使用の式全体を削除できます。基本的なアルゴリズムは、値が使用されていない単一の命令を識別し、変更されなくなるまでアルゴリズムが繰り返されます。

冗長なブランチは、ブランチ間を探す独自の最適化で処理されます。条件付き分岐フォールスルーを改善するために、コードが再配置されることもあります。ループの最後にある無条件分岐を削除するためのループ指向の最適化もあります(条件付き分岐が優先されます)。

要約すると、各変換は個別の最適化であり、最適化コンパイラには何百ものこれらがあり、それらの一部は、探しているパターンに一致しなくなるまで繰り返されます。

しかし、データフローグラフはそれらを無視するだけなので、順序を保持する必要のある副作用がある場合、それは機能しないようです。おそらく、データフローグラフに加えてコントロールフローグラフが必要になりますが、2つのグラフがどのように連携してバイトコードを最適化するのに役立つかはわかりません。

データフロー分析 は必ず制御フローを考慮に入れます。

制御フロー分析には、 基本ブロック の識別が含まれます。ラベルで始まり、分岐命令または別のラベルで終わる。さらなる制御フロー分析は、基本ブロックをリンクし、ループ、if-then-elseコンストラクトなどを識別します。

制御フロー分析の出力は、ノードとエッジを意味するグラフ構造です。基本ブロックのノード、およびそれらを接続するエッジ。

データフロー分析は通常、2つの形式を取ります。基本ブロック内の分析と、制御フローに続く基本ブロックのエッジ間の分析です。 通常、これらは双方向で行われます。順方向:到達定義の追跡、逆方向:公開された使用の追跡です。したがって、変数と使用の定義に関する情報を取得します変数の。

データフローの出力は通常、ビットベクトルのバリエーションであり、各ビット位置はプログラム内の異なる変数を表します。ビットが設定されている場合は、変数の定義があります(定義の場合、または使用法の使用の場合。クリアの場合はありません)。 (制御フローグラフで動作し、 fixpoint アルゴリズム、つまり収束するデータフローアルゴリズム:変更がなくなるまで繰り返すことができます。)

プログラム内の各命令の前後に、その場所に到達する(先行する)定義と(後続の)使用が結果を消費することを表す2つの潜在的なビットベクトルがあると考えることができます。ただし、通常、この情報はすべての命令に保存されるわけではありませんが、各基本ブロックの開始前と終了後のみ(命令レベルの情報は、基本ブロックのトラバース中に再現可能です)。

最適化では、このデータフロー情報をさまざまな方法で使用します。たとえば、変数の定義に使用法がない場合、これを検出できます。これにより、未使用の計算が識別されます。別の例として、2つの変数がプログラム内の同じポイントに存在する(または存在しない)ため、異なるCPUレジスタが必要(または不要)であると判断できます。

SSAはデータフロー分析の一種であり、変数のバージョンを導入して追跡するため、情報はグローバルに正確(正確な位置情報なし)ですが、古い(おそらく単純な)データフローアルゴリズムは変数を直接追跡します。このような追跡については、場所のコンテキスト。

2
Erik Eidt

Forth というプログラミング言語について説明しました。オープンソースであり、比較的単純なものから非常に洗練されたものまで、多くの実装があります。

1つの手法は、マシン内のすべてのもの(スタック、ヒープ、ファイルシステム、画面など)を、関連付けられたアクションを持つものとして扱うことです各アクションが前のアクションによって生成された値を取り、更新された値を返すリスト。すべての命令は基本的に、これらのリストに追加される一連のアクションです。

アクションには依然として相対的な順序が必要ですが、各アクションが他のすべてのアクションと連続しているという意味で厳密である必要はありません。ただし、アクションは常に正しい値を読み取り、その値を変数の後の操作に提示できるように、十分に厳密である必要があります。これにより、これらのアクションはシステムの残りの状態を認識できなくなり、適用されているオブジェクトのみが考慮されます。彼らが使用する他の情報は、アクションがリストに追加されたときに渡されたか、以前の時間アクショングループ内の別のアクションが値を割り当てました。

これに続いて、2つ(またはそれ以上)の命令がこれらのリストに一連のアクションを生成します。これで、いくつかのアクションが互いにキャンセルされます。いう swap a,b; swap b,a;。それらが互いに隣接している場合、これらは結果に影響を与えません。残されたアクションは、そのコードが実行されるために必要な十分なアクションです。

このコードを最適化するには:

  1. これらのアクションを正確に定義した新しい命令を定義します。
  2. その定義と同じアクションを備えたより短い一連の指示を見つけてください。できれば、アクションをキャンセルしないか、キャンセルしないことをお勧めします。

また、一歩下がって、ソースが表すプログラム構造を確認します。これがマシン構造と同一の場合は無視してください。ただし、高次構造の場合は、このグラフレベルで最適化を適用してみてください。このレベルのセマンティックの詳細では、一連の小さなステップからそれらを認識する必要がないため、特定の最適化を適用する方がはるかに簡単です。ブランチや兄弟などに関するいくつかの一般的なクエリは簡単です。

1
Kain0_0