データフロー解析は、コンピュータプログラムのさまざまな箇所で計算される可能性のある値の集合に関する情報を収集する手法です。これは、多種多様なコンパイラ最適化やプログラム検証手法の基礎となります。プログラムの制御フローグラフ(CFG)は、変数に割り当てられた特定の値が伝播する可能性のあるプログラムのどの部分かを判断するために使用されます。収集された情報は、コンパイラがプログラムを最適化する際によく利用されます。データフロー解析の典型的な例は、到達定義です。その他によく用いられるデータフロー解析には、ライブ変数解析、利用可能な式、定数伝播、非常にビジーな式などがあり、それぞれコンパイラ最適化パスにおいて異なる役割を果たします。
プログラムのデータフロー解析を行う簡単な方法は、制御フローグラフの各ノードに対してデータフロー方程式を設定し、システム全体が安定するまで、つまり固定点に達するまで、各ノードで入力から出力をローカルに繰り返し計算して方程式を解くことです。このプロセスの効率と精度は、解析の方向(順方向または逆方向)、値の範囲、複数の制御パスからの情報をマージするために使用される結合操作など、データフローフレームワークの設計によって大きく左右されます。この一般的なアプローチは、キルダルの方法としても知られており、ゲイリー・キルダルが海軍大学院で教鞭をとっていたときに開発されました。[ 1 ] [ 2 ] [ 3 ] [ 4 ] [ 5 ] [ 6 ] [ 7 ] [ 8 ]
データフロー解析とは、プログラム内で変数がどのように定義され、使用されているかに関する情報を収集するプロセスです。これは、プロシージャの各ポイントで特定の情報を取得しようとします。通常、基本ブロックの境界でこの情報を取得すれば十分です。なぜなら、そこから基本ブロック内のポイントでの情報を容易に計算できるからです。順方向フロー解析では、ブロックの終了状態はブロックの開始状態の関数です。この関数は、ブロック内のステートメントの効果の合成です。ブロックの開始状態は、その前のブロックの終了状態の関数です。これにより、次のデータフロー方程式が得られます。
各ブロックbについて:
この中で、ブロックの伝達関数エントリー状態に基づいて動作します出口状態が得られる結合操作先行する要素の終了状態を組み合わせるのエントリー状態は。
この一連の方程式を解いた後、ブロックの開始状態および/または終了状態を用いて、ブロック境界におけるプログラムの特性を導出できます。各ステートメントの伝達関数を個別に適用することで、基本ブロック内の特定の位置における情報を取得できます。
データフロー解析の種類ごとに、固有の転送関数と結合操作があります。データフロー問題の中には、逆フロー解析を必要とするものもあります。これは、転送関数を終了状態に適用して開始状態を生成し、結合操作を後続ノードの開始状態に適用して終了状態を生成するという点を除いて、同じ手順に従います。
(順方向フローにおける)エントリポイントは重要な役割を果たします。エントリポイントには先行する処理がないため、解析開始時にエントリ状態が明確に定義されます。例えば、既知の値を持つローカル変数のセットは空です。制御フローグラフにサイクルが含まれていない場合(手順に明示的または暗黙的なループがない場合)、方程式を解くのは簡単です。制御フローグラフはトポロジカルにソートできます。このソート順で実行することで、各ブロックの開始時にエントリ状態を計算できます。これは、そのブロックのすべての先行処理が既に完了しているため、それらの終了状態が利用可能だからです。制御フローグラフにサイクルが含まれている場合は、より高度なアルゴリズムが必要になります。
データフロー方程式を解く最も一般的な方法は、反復アルゴリズムを用いることです。まず、各ブロックの入力状態を近似します。次に、入力状態に伝達関数を適用して出力状態を計算します。そして、これらの出力状態から結合演算を適用して入力状態を更新します。この2つのステップは、いわゆる固定点、つまり入力状態(およびそれに伴う出力状態)が変化しない状態に到達するまで繰り返されます。
データフロー方程式を解くための基本的なアルゴリズムは、ラウンドロビン反復アルゴリズムです。
反復法が実用的であるためには、実際に不動点に到達する必要があります。これは、状態の値域、伝達関数、および結合演算の組み合わせに制約を課すことで保証できます。
値領域は有限の高さを持つ半順序である必要があります(つまり、無限に上昇する連鎖はありません)。<< ...)。伝達関数と結合操作の組み合わせは、この半順序に関して単調である必要があります。単調性により、各反復で値は同じか大きくなるかのどちらかになり、有限の高さにより無限に大きくなることはありません。したがって、最終的にはすべての x に対して T(x) = x となる状況に到達し、これが不動点となります。
上記のアルゴリズムは、先行するブロックの出力状態が変化しない限り、そのブロックの入力状態も変化しないことに着目することで容易に改善できます。そこで、処理待ちのブロックのリストであるワークリストを導入します。ブロックの出力状態が変化するたびに、そのブロックの後続ブロックをワークリストに追加します。各イテレーションでは、ワークリストからブロックが1つ削除され、その出力状態が計算されます。出力状態が変化した場合は、そのブロックの後続ブロックがワークリストに追加されます。効率化のため、ブロックはワークリストに複数回含まれないようにする必要があります。
このアルゴリズムは、情報生成ブロックをワークリストに追加することから開始され、ワークリストが空になった時点で終了します。
データフロー方程式を反復的に解く効率は、ローカルノードを訪問する順序によって左右されます。[ 9 ]さらに、データフロー方程式がCFG上の順方向データフロー解析に使用されるか逆方向データフロー解析に使用されるかによっても異なります。直感的には、順方向フロー問題では、ブロック自体の前にすべての先行ブロックが処理されている場合が最も高速です。なぜなら、その場合、反復処理は最新の情報を使用するからです。ループがない場合、各ブロックを一度だけ処理することで正しい出力状態が計算されるようにブロックを順序付けることができます。
以下では、データフロー方程式を解くためのいくつかの反復順序について説明します(CFGの反復順序に関連する概念として、ツリーの ツリー走査があります)。
初期状態は、正確で精度の高い結果を得るために重要です。コンパイラの最適化に結果を使用する場合、結果は保守的な情報を提供する必要があります。つまり、情報を適用してもプログラムのセマンティクスは変更されません。不動点アルゴリズムの反復では、値は最大要素の方向に取られます。したがって、すべてのブロックを最大要素で初期化しても意味がありません。少なくとも1つのブロックは、最大値より小さい値を持つ状態から始まります。詳細はデータフロー問題によって異なります。最小要素が完全に保守的な情報を表す場合、データフローの反復中でも結果を安全に使用できます。最小要素が最も正確な情報を表す場合は、結果を適用する前に不動点に到達する必要があります。
以下は、データフロー解析によって計算できるコンピュータプログラムの特性の例です。データフロー解析によって計算される特性は、通常、実際の特性の近似値にすぎないことに注意してください。これは、データフロー解析がプログラムの正確な制御フローをシミュレートすることなく、CFGの構文構造に基づいて動作するためです。しかし、実用上役立つように、データフロー解析アルゴリズムは通常、実際のプログラム特性の上限または下限の近似値を計算するように設計されています。
到達定義分析は、各プログラムポイントについて、そのプログラムポイントに到達する可能性のある定義のセットを計算します。
b == 4 の場合 a = 5; それ以外 a = 3; endif a < 4 の場合 ... 7行目における変数の到達定義は、 2行目と4行目におけるa代入の集合である。a = 5a = 3
ライブ変数分析は、各プログラムポイントについて、次の書き込み更新前に読み込まれる可能性のある変数を計算します。この結果は通常、 デッドコード削除において、その後使用されない値に代入するステートメントを削除するために使用されます。
ブロックのイン状態とは、ブロックの開始時点で有効な変数の集合です。転送関数が適用され、実際の変数値が計算される前に、ブロック内で有効な(含まれている)すべての変数が初期状態で含まれます。ステートメントの転送関数は、このブロック内で記述された変数を消去する(有効な変数の集合から削除する)ことによって適用されます。ブロックのアウト状態とは、ブロックの終了時点で有効な変数の集合であり、ブロックの後続のイン状態の和集合によって計算されます。
初期コード:
逆算分析:
b3 の入力状態には、 cが書き込まれているため、bとdのみが含まれます。b1 の出力状態は、b2 と b3 の入力状態の和集合です。c はステートメントの直後には生存していないため、 b2におけるcの定義は削除できます。
データフロー方程式の解法は、まずすべての入力状態と出力状態を空集合に初期化することから始まります。ワークリストは、出口点(b3)をワークリストに挿入することで初期化されます(これは逆フローの場合によく見られます)。計算された入力状態は前のものと異なるため、その前の入力状態b1とb2が挿入され、処理が続行されます。処理の進行状況は、以下の表にまとめられています。
b1がb2より先にリストに入力されたため、b1が2回処理されることになった(b1がb2の先行要素として再入力された)。b2をb1より前に挿入していれば、処理をより早く完了できたはずだ。
空集合で初期化することは楽観的初期化です。つまり、すべての変数は最初は「デッド」状態になります。出力状態は入力状態よりも小さくなる可能性はありますが、1回の反復で出力状態が縮小することはないことに注意してください。これは、最初の反復以降、出力状態は入力状態の変化によってのみ変化するという事実からわかります。入力状態は空集合から始まるため、それ以降の反復では大きくなるしかありません。
現代のコンパイラの多くは、変数依存性の解析方法として静的単一代入形式を使用している。 [ 10 ]
2002年、Markus Mohnenは、データフローグラフを明示的に構築する必要のない新しいデータフロー解析手法を提唱した[ 11 ]。この手法は、プログラムの抽象的解釈に依存し、プログラムカウンタのワーキングセットを保持する。各条件分岐では、両方のターゲットがワーキングセットに追加される。各パスは、可能な限り多くの命令(プログラムの終了まで、または変更なしでループするまで)にわたって追跡され、その後セットから削除され、次のプログラムカウンタが取得される。
制御フロー分析とデータフロー分析の組み合わせは、システムの機能(機能、要件、ユースケースなど)を実装するまとまりのあるソースコード領域を特定する上で有用かつ相補的であることが示されている。[ 12 ]
データフロー問題には、効率的な解法や一般的な解法が存在する様々な特殊なクラスが存在する。
上記の例は、データフローの値が集合である問題です。たとえば、到達定義の集合(プログラム内の定義位置を表すビットを使用)や、生存変数の集合などです。これらの集合は、各ビットが特定の要素の集合メンバーシップを表すビットベクトルとして効率的に表現できます。この表現を使用すると、結合関数と転送関数をビット単位の論理演算として実装できます。結合演算は通常、和集合または積集合であり、ビット単位の論理ORと論理ANDで実装されます。各ブロックの転送関数は、いわゆるgen集合とkill集合に分解できます。
例えば、ライブ変数解析では、結合演算はユニオンです。キルセットはブロック内で書き込まれる変数のセットであり、ゲンセットは最初に書き込まれることなく読み込まれる変数のセットです。データフロー方程式は次のようになります。
論理演算では、これは次のように読みます。
out( b ) = 0 for s in succ( b ) out( b ) = out( b )またはin( s ) in( b ) = (out( b ) and not kill( b )) or gen( b )
データフロー値の集合をビットベクトルとして表現できるデータフロー問題は、ビットベクトル問題、gen-kill問題、または局所的に分離可能な問題と呼ばれます。[ 13 ]このような問題には、一般的な多項式時間解があります。[ 14 ]
上記の到達定義と生存変数の問題に加えて、次の問題はビットベクトル問題の例です。[ 14 ]
手続き間有限部分集合問題またはIFDS問題は、一般的な多項式時間解を持つ別のクラスの問題です。[ 13 ] [ 15 ]これらの問題の解は、コンテキスト依存およびフロー依存のデータフロー分析を提供します。
IFDS ベースのデータフロー分析は、一般的なプログラミング言語向けにいくつか実装されており、例えばJava 分析用のSoot [ 16 ]や WALA [ 17 ]フレームワークなどがあります。
ビットベクトルに関する問題はすべてIFDS問題でもあるが、真に生存している変数や初期化されていない可能性のある変数など、ビットベクトル問題ではない重要なIFDS問題がいくつか存在する。
データフロー解析は通常、経路に依存しないが、経路に依存する解析結果をもたらすデータフロー方程式を定義することも可能である。
x>0スルーパスでは、解析は を仮定し、分岐のターゲットでは、 が実際に成り立つと仮定します。x<=0x>0[…]
Eubanks
: […]
Gary
[…] は発明家であり、独創的で、物事を成し遂げました。彼の博士論文は、グローバルフロー解析が収束することを証明しました。 […] これはコンピュータ科学の基本的な考え方です。 […] 以前、
ダムデレ
という人の夏期講座を受講したことがあるのですが、
彼らは最適化について1週間ほど講義した後、スライドを出して「キルダル法」、これが本当の話だと言いました。 […] それは誰も考えたこともないことです。 […]
(33ページ)