コンパイラ演習 第8回 (2011/12/01) 中村 晃一 野瀬 貴史 前田 俊行 秋山 茂樹 池尻 拓朗 鈴木 友博 渡邊 裕貴 潮田 資秀 小酒井 隆広 山下 諒蔵 佐藤 春旗 大山 恵弘 佐藤 秀明 住井 英二郎 今回の内容 • データフロー解析とそれに基づく最適化手法 マシン依存性による最適化の分類 • マシン非依存 – 静的に評価出来る部分をコンパイル時に計算 – 無駄な評価・計算を減らす • マシン依存 – パイプライン効率を上げる – キャッシュ効率を上げる • 両者に関係 – 並列度を上げる – オーバヘッド除去 局所性による最適化の分類 • 局所的最適化 – Peephole 最適化 – 基本ブロック内最適化 • 大域的最適化 – 基本ブロック間最適化 – 関数間最適化 – プログラム間最適化 今回のターゲット – マシン非依存 – 基本ブロック間 • の – データフロー解析法 – 最適化手法 • を扱います。 今回説明する基本ブロック間最適化 1. 2. 3. 4. 5. • コピー伝播 定数畳みこみ 共通部分式除去 値番号付け 部分冗長性除去 今回は到達定義解析・定義使用解析 に基づくアルゴリズムを紹介します。 基本ブロック間最適化の手順 1. 制御フローグラフを構築 2. データフロー解析をする 3. 最適化を行う • 以上を個々の関数毎に行う 基本ブロックとは • 入ってから出るまで一直線な プログラム片 1.途中での合流が無く、 2.途中からの脱出が無い • 途中に関数呼び出しを含む ブロックを許すか否かは 最適化の種類による 基本ブロック let x = f() in if x > 0 then let y = - x in … else let y = x in … 準備:ループの検出 • MinCaml には明示的なループが存在しない → そのままでは関数間解析をしないと 高度な最適化が難しい。 • 単純な方法 – 自己末尾再帰呼び出しを while 型ループへ n=0 let rec f n = if n < 10 then (…; f (n+1)) else () in f 0 if n < 10 … n=n+1 準備:代入構文の用意 • ループ作成によって必要になる 変数の更新を表現 let rec f n = if n < 10 then (…; f (n+1)) else () in f 0 if n < 10 … nn+1 変数の 更新 • 左の様な let 文も右のように変換 let x = if ... then e1 else e2 n=0 if ... x e1 x e2 制御フローグラフ • 関数 body を基本ブロックを頂点、 制御移行を辺として entry グラフを作る • 空の entry,exit ブロックを用意 – 入口・出口を用意しておくと あとあと楽 exit データフロー解析 (前方解析) • プログラムポイントの集合を P、データの集合を D として以下を定義 – – – – • In[p] : D Out[p]: D pred: P2P f p: DD (ポイント p の入口でのデータ) (ポイント p の出口でのデータ) (ポイント p の一つ前のポイントの集合を得る関数) (ポイント p における伝達関数) すべてのポイント p について In[p], Out[p] を求める p1 p2 Out[p1] Out[p2] In[p3] p3 データフロー解析 (後方解析) • プログラムポイントの集合を P、データの集合を D として以下を定義 – – – – • In[p] : D Out[p]: D succ: P2P f p: DD (ポイント p の入口でのデータ) (ポイント p の出口でのデータ) (ポイント p の一つ後のポイントの集合を得る関数) (ポイント p における伝達関数) すべてのポイント p について In[p], Out[p] を求める p1 Out[p1] In[p2] p2 In[p3] p3 基本となるデータフロー解析 • 到達定義解析 – ある場所で使用される物(変数、式など)が どこで定義されたか? ⇒ 前方解析 • 定義使用解析 – ある場所で定義された変数が どこで使用されるか? ⇒ 後方解析 一方から容易に他方を求める事が出来る。 今回は到達定義解析の結果から定義使用解析を行う 到達定義解析 • データフロー解析の一種 • 使用される変数や式が どこで定義されているかを調べる • 流れ – まずプログラムポイントを基本ブロックとして解析 – 続いて基本ブロック毎に、 プログラムポイントを その中のステートメントとして解析 ブロック単位の到達定義解析: reach 集合 • reach[B]: 基本ブロック B の入り口に到達する 定義文の集合 reach[B1] = Φ reach[B2] = {s1, s2, s5, s6, s7} reach[B3] = {s1, s2, s5, s6, s7} B1 s1: i = 0 s2: s = 0 B2 s3: if i < 100 – これを計算する事が 解析の目標 s4: t = i B3 s5: t = 2*t s6: i = i + 1 s7: s = s + t ブロック単位の到達定義解析: def 集合 • def[B]: 基本ブロック B の中の定義文で B の出口で有効なものの集合 def[B1] = {s1, s2} def[B2] = Φ def[B3] = {s5, s6, s7} B1 s1: i = 0 s2: s = 0 B2 s3: if i < 100 s4: t = i B3 s5: t = 2*t s6: i = i + 1 s7: s = s + t ブロック単位の到達定義解析: kill 集合 • kill[B]: B の出口で無効化される定義文の集合 – 制御フローは考慮しなくてよい B 内の文によって無効化され得る全ての定義文を集める。 kill[B1] = {s6, s7} kill[B2] = Φ kill[B3] = {s1, s2} B1 s1: i = 0 s2: s = 0 B2 s3: if i < 100 s4: t = i B3 s5: t = 2*t s6: i = i + 1 s7: s = s + t 到達定義解析のデータフロー方程式 X B fB(X) • とすれば以下を得る。 到達定義解析のアルゴリズム 全ての基本ブロック B について def[B] と kill[B] を計算し、 reach[B] := Φとする reach[entry] := {arguments, global variables, ・・・} do { old_reach := reach # 集合としてコピー 全ての基本ブロック B について { 全ての基本ブロック b ∈ pred(B) について { reach[B] := reach[B] ∪ def[b] ∪ (reach[b] \ kill[b]) } } } while (old_reach != reach) # 集合として比較 到達定義解析の具体例 reach[B1] = Φ reach[B2] = Φ reach[B3] = Φ B1 s1: i = 0 s2: s = 0 B2 s3: if i < 100 def[B1] = {s1, s2} def[B2] = Φ def[B3] = {s5, s6, s7} kill[B1] = {s6, s7} kill[B2] = Φ kill[B3] = {s1, s2} s4: t = i B3 s5: t = 2*t s6: i = i + 1 s7: s = s + t ※ここでは entry, exit を省略している 到達定義解析の具体例 reach[B1] = Φ reach[B2] = {s1,s2} reach[B3] = Φ B1∈pred(B2) の def B1 s1: i = 0 s2: s = 0 B2 s3: if i < 100 def[B1] = {s1, s2} def[B2] = Φ def[B3] = {s5, s6, s7} kill[B1] = {s6, s7} kill[B2] = Φ kill[B3] = {s1, s2} s4: t = i B3 s5: t = 2*t s6: i = i + 1 s7: s = s + t ※ここでは entry, exit を省略している 到達定義解析の具体例 B3∈pred(B2) の def reach[B1] = Φ reach[B2] = {s1,s2,s5,s6,s7} reach[B3] = Φ B1 s1: i = 0 s2: s = 0 B2 s3: if i < 100 def[B1] = {s1, s2} def[B2] = Φ def[B3] = {s5, s6, s7} kill[B1] = {s6, s7} kill[B2] = Φ kill[B3] = {s1, s2} s4: t = i B3 s5: t = 2*t s6: i = i + 1 s7: s = s + t ※ここでは entry, exit を省略している 到達定義解析の具体例 reach[B1] = Φ reach[B2] = {s1,s2,s5,s6,s7} reach[B3] = {s1,s2,s5,s6,s7} B2∈pred(B3) の reach\kill def[B1] = {s1, s2} def[B2] = Φ def[B3] = {s5, s6, s7} kill[B1] = {s6, s7} kill[B2] = Φ kill[B3] = {s1, s2} B1 s1: i = 0 s2: s = 0 B2 s3: if i < 100 s4: t = i B3 s5: t = 2*t s6: i = i + 1 s7: s = s + t ※ここでは entry, exit を省略している 到達定義解析の具体例 reach[B1] = Φ reach[B2] = {s1,s2,s5,s6,s7} reach[B3] = {s1,s2,s5,s6,s7} 変化なし 変化なし B1 s1: i = 0 s2: s = 0 B2 s3: if i < 100 def[B1] = {s1, s2} def[B2] = Φ def[B3] = {s5, s6, s7} kill[B1] = {s6, s7} kill[B2] = Φ kill[B3] = {s1, s2} s4: t = i B3 s5: t = 2*t s6: i = i + 1 s7: s = s + t ※ここでは entry, exit を省略している ステートメント単位の到達定義解析 • 変数の到達定義解析の場合以下を計算 – reach[s, x] : ステートメント s で使用されている変数 x を定義している文集合 • s をブロック B の文とすると、reach[s,x] は – B 内の s の先行命令が x を定義していれば それのみ – そうでなければ、reach[B] に含まれる x を定義している文集合 • として計算できる ステートメント単位の到達定義解析の例 • 例えば reach[s6,i] についてみてみる – B3 の先行命令(S4,s5)は I を定義しないので reach[B] を見る。 s1: i = 0 – reach[B] 内 で i を定義している文は B1 s2: s = 0 s1 と s6 reach[B3] = {s1,s2,s5,s6,s7} B2 s3: if i < 100 – 従って reach[s6,i] = {s1, s6} B3 s4: t = i s5: t = 2*t s6: i = i + 1 s7: s = s + t ステートメント単位の到達定義解析の例 • 最終結果 reach[s3, i] = {s1, s6} reach[s4, i] = {s1, s6} reach[s5,t] = {s4} reach[s6,i] = {s1, s6} reach[s7,s] = {s2, s7} reach[s7,t] = {s5} s1: i = 0 B1 s2: s = 0 B2 s3: if i < 100 B3 s4: t = i s5: t = 2*t s6: i = i + 1 s7: s = s + t 定義使用解析 • 以下を計算する – use[s] : ステートメント s で定義されている変数 x を使用している文集合 • 計算方法 – use[s] = { t | s ∈ reach[t, x], x は s が定義する変数} 定義使用解析の例 use[s1] = {s3, s4, s6} use[s2] = {s7} use[s4] = {s5} use[s5] = {s7} use[s6] = {s3, s4, s6} use[s7] = {s7} reach[s3, i] = {s1, s6} reach[s4, i] = {s1, s6} reach[s5,t] = {s4} reach[s6,i] = {s1, s6} reach[s7,s] = {s2, s7} reach[s7,t] = {s5} s1: i = 0 s2: s = 0 s3: if i < 100 s4: t = i s5: t = 2*t s6: i = i + 1 s7: s = s + t 実際の最適化の例: コピー伝播 • 変数の到達定義解析・定義使用解析を利用 copyprop(prog) { worklist := (let x = v(変数)の形の文) while (worklist != Φ) { s := worklist.pop foreach(t in use[s]) { x := s が定義する変数 if (|reach[t,x]| == 1) { # t に到達する変数が 1 つしかない t 内の x の使用を v に置換 worklist.add(t) if t が let x’ = v’ の形で worklist に存在しない } } } } 実際の最適化の例: 定数畳みこみ • 変数の到達定義解析・定義使用解析を利用 constfold(prog) { worklist := 定数定義文 while (worklist != Φ) { s := worklist.pop foreach(t in use[s]) { x := s が定義する変数 x を定数値に置換 if reach[t,x] の全てが同じ値を定義 静的に t を評価 if t の全引数が定数 worklist.add(t) if t の計算結果が定数 } } } 実際の最適化の例: 共通部分式除去 • 「利用可能な式」の到達定義解析を行う • ここでは基本ブロック単位の解析を説明する – Eavail[B]: – Ekill[B]: ブロック B の入口で利用可能な式集合 ブロック B 内でオペランドが 上書きされる式集合 – Edef[B]: ブロックB内にある式で 出口で利用可能な式集合 • として以下を解く (∪でなく∩となる点に注意) • その後ステートメント単位での解析をする 共通部分式除去の例 x=i*4 y=a+x reach = {i * 4, a + x} p = load y x=i*4 y=b+x q = load y reach = {i * 4} p=i*4 z=a+x i * 4が共通部分式になっている ※右のパスで x が上書きされるので a + x はここに到達しない。 共通部分式除去の例 x=i*4 t=x y=a+x i * 4 の為の一時変数を作って… reach = {i * 4, a + x} p = load y x=i*4 y=b+x q = load y reach = {i * 4} p=i*4 z=a+x 共通部分式除去の例 x=i*4 t=x y=a+x reach = {i * 4, a + x} p = load y x=t y=b+x q = load y reach = {i * 4} p=t z=a+x 共通部分式を除去 実際の最適化の例: 値番号付け • 先ほどのアルゴリズムでは 例えば下の様なものが解決出来ない a=1 b=1 c=a+2 d=b+2 a=b+c d=b e=d+c 同じ値を持つが共通部分式じゃない 同じ値を持つが共通部分式じゃない 実際の最適化の例: 値番号付け • 式の「値」を「番号」で識別して 共通な値を見つける a=1 b=1 c=a+2 d=b+2 a=① b=① c=② d=② (②=①+2) a=b+c d=b e=d+c b=① c=② a=③ d=① e=③ (③=①+②) 実際の最適化の例: 部分冗長性除去 • 値番号付けでも 部分冗長なケースは 解決できない x=a+b このパスでだけ a + b が冗長 y=a+b 部分冗長性除去: 簡単なアイデア • 式を挿入して全冗長にする z=a+b x=a+b z=a+b y=a+b 部分冗長性除去 • 手順 – – – – – 共通部分式の除去 危険辺の除去 入口計算・出口計算・挿入点を検査 上安全性・下安全性の検査 挿入点を求める • 今回はせわしいコード移動 (busy-code-motion) という方法を紹介 – 式をできるだけ前方へ挿入する • 以下 a + b という形の一つの式に注目する 危険辺の除去 • 2つ以上の後続ブロックを持つブロックから、 2つ以上の先行ブロックを持つブロックへの辺を 新たにブロックを挿入して除去 y=a+b y=a+b ここに a + b を挿入すると 後続のパスに影響 ここなら a + b を挿入しても 大丈夫 入口計算・出口計算 • ブロック B 内に a, b への代入文がある場合、 その最後の代入文の直後で分割 – 入口計算: 入口部分にあり、かつ a, b の代入より前にある a + b – 出口計算: 出口部分にある a + b x =a+b a=2 b=x+b y=a+b • ブロックの集合をBとして – NCompa+b: BBoolean – XCompa+b: BBoolean – Transpa+b: BBoolean x =a+b a=2 b=x+b y=a+b 入口計算 出口計算 (ブロック B が入口計算を持つ) (ブロック B が出口計算を持つ ) (ブロック B 内に a, b への代入文がない) 挿入点 • 以下の 2 つのプログラムポイントを「挿入点」と呼ぶ – 入口挿入点 • 入口計算の直前 (入口計算がなければ基本ブロックの先頭) – 出口挿入点 • 出口計算の直前 (出口計算がなければ基本ブロックの最後) x =a+b a=2 b=x+b y=a+b x =a+b a=2 b=x+b y=a+b 入口挿入点 入口計算 出口挿入点 出口計算 上安全性・下安全性 • ある挿入点に a + b を代入しても安全かどうか • 上安全性 – entryからその点までの どのパスにも a + b があり全部同じ値 • 下安全性 – その点から exit まで どのパスにも a + b があり全部同じ値 • • • • NuSafea+b[B] : Boolean XuSafea+b[B] : Boolean NdSafea+b[B] : Boolean XdSafea+b[B] : Boolean (B の入口挿入点で上安全) (B の出口挿入点で上安全) (B の入口挿入点で下安全) (B の出口挿入点で下安全) 上安全性・下安全性の解析 • NdSafe, XdSafe は以下の方程式で計算できる • NuSafe, XuSafe も同様 せわしいコード移動 • 下安全で出来るだけ前方の挿入点に t = a + b を挿入 • NEarlist[B] = trueである B の入口挿入点, XEarliest[B] = trueである B の出口挿入点に t = a + b を挿入 • すべての入口計算・出口計算を t に置換 その他最適化のヒント: Do-All 型ループの検出 • 後の回やる並列化・キャッシュ最適化等に 欠かせない作業 • Do-All 型ループとは – i = a,a+d,a+2d,…,a+(k-1)d と順番に回すループ for (i = a; i < n; i += d) { … } • while ループには既に直してあるのだから インデックス変数 i を見つけてやればよい Do-All 型ループの検出: 帰納的変数検出 • Do-All 型ループのインデックスは 帰納的変数になっている – i0 = a – in+1 = in + d – ループ脱出条件により i の上限 (下限) が決定出来る • 到達定義解析の結果から 右のようなパターンを 見つければよい i=a if ループ脱出条件 i=i+d 帰納的変数の検出(例) s1: i = 0 s2: s = 0 s3: if i < 100 s4: t = i s5: t = 2*t s6: i = i + 1 s7: s = s + t s=0 for (i=0;i < 100; i++) t=i t = 2*t s=s+t 共通課題 • 二つの共通課題のうち 一つ以上を解いてください 課題1 • MinCamlに到達定義解析を実装し それを用いた最適化 (最低1つ) を実装せよ。 – 第2回で扱った到達定義解析に寄らない最適化 と比較し評価せよ。 課題2 • 今回紹介した単純な部分冗長性除去では 上手く冗長性が除去出来ないプログラムや 遅くなってしまうプログラムがある • その例を挙げ、改善する方法を考えよ – 参考文献 • Knoop, J., Ruthing, O., and Steffen, B. Lazy Code Motion. ACM SIGPLAN Notices Vol. 27, Num. 7, Jul. 1992, '92 Conference on PLDI. • VanDrunen, T., and Hosking, A.L. Value-Based Partial Redundancy Elimination, Lecture Notes in Computer Science Vol. 2985/2004, pp. 167 - 184, 2004. • Kennedy, R., Chan, S., Liu, S.M., Lo, R., Peng, T., and Chow, F. Partial Redundancy Elimination in SSA Form. ACM Transactions on Programming Languages Vol. 21, Num. 3, pp. 627-676, 1999. コンパイラ係用課題 • 関数間解析・エイリアス解析など さらに高度な解析と それを用いた最適化を実装せよ 課題の提出先と締め切り • 提出先: [email protected] • 締め切り: 2 週間後 (12/15) の午後 1 時 – コンパイラ係用課題の締め切り: 2012 年 2月 27日 • Subject: Report 8 <学籍番号: 5 桁> 半角スペース 1 個ずつ – 例: Report 8 11099 • 本文にも氏名と学籍番号を明記のこと 質問は [email protected] まで
© Copyright 2024 ExpyDoc