550 likes | 645 Views
コンパイラ演習 第 8 回 (2011/12/01). 中村 晃一 野瀬 貴史 前田 俊行 秋山 茂樹 池尻 拓朗 鈴木 友博 渡邊 裕貴 潮田 資秀 小酒井 隆広 山下 諒蔵 佐藤 春旗 大山 恵弘 佐藤 秀明 住井 英二郎. 今回の内容. データフロー解析とそれに基づく最適化手法. マシン依存性による最適化の分類. マシン 非依存 静的に評価出来る部分をコンパイル時に計算 無駄な評価・計算を減らす マシン依存 パイプライン効率を上げる キャッシュ効率を上げる 両者に関係 並列度を上げる オーバヘッド除去. 局所性による最適化の分類.
E N D
コンパイラ演習第 8 回(2011/12/01) 中村晃一 野瀬貴史 前田俊行 秋山 茂樹 池尻 拓朗 鈴木 友博 渡邊 裕貴 潮田 資秀 小酒井 隆広 山下 諒蔵 佐藤 春旗 大山 恵弘 佐藤 秀明 住井 英二郎
今回の内容 • データフロー解析とそれに基づく最適化手法
マシン依存性による最適化の分類 • マシン非依存 • 静的に評価出来る部分をコンパイル時に計算 • 無駄な評価・計算を減らす • マシン依存 • パイプライン効率を上げる • キャッシュ効率を上げる • 両者に関係 • 並列度を上げる • オーバヘッド除去
局所性による最適化の分類 • 局所的最適化 • Peephole 最適化 • 基本ブロック内最適化 • 大域的最適化 • 基本ブロック間最適化 • 関数間最適化 • プログラム間最適化
今回のターゲット • マシン非依存 • 基本ブロック間 • の • データフロー解析法 • 最適化手法 • を扱います。
今回説明する基本ブロック間最適化 • コピー伝播 • 定数畳みこみ • 共通部分式除去 • 値番号付け • 部分冗長性除去 • 今回は到達定義解析・定義使用解析に基づくアルゴリズムを紹介します。
基本ブロック間最適化の手順 • 制御フローグラフを構築 • データフロー解析をする • 最適化を行う • 以上を個々の関数毎に行う
基本ブロックとは • 入ってから出るまで一直線なプログラム片 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 文も右のように変換 n = 0 let rec f n = if n < 10 then (…; f (n+1)) else () in f 0 if n < 10 … n n + 1 変数の更新 変数の更新 if ... let x = if ... then e1 else e2 x e1 x e2
制御フローグラフ • 関数 body を基本ブロックを頂点、制御移行を辺としてグラフを作る • 空のentry,exitブロックを用意 • 入口・出口を用意しておくとあとあと楽 entry exit
データフロー解析 (前方解析) • プログラムポイントの集合を P、データの集合を D として以下を定義 • In[p] : D (ポイント p の入口でのデータ) • Out[p]: D (ポイント p の出口でのデータ) • pred: P2P (ポイント p の一つ前のポイントの集合を得る関数) • fp: DD (ポイント p における伝達関数) • すべてのポイント p について In[p], Out[p] を求める p1 p2 Out[p1] Out[p2] In[p3] p3
データフロー解析 (後方解析) • プログラムポイントの集合を P、データの集合を D として以下を定義 • In[p] : D (ポイント p の入口でのデータ) • Out[p]: D (ポイント p の出口でのデータ) • succ: P2P (ポイント p の一つ後のポイントの集合を得る関数) • fp: DD (ポイント p における伝達関数) • すべてのポイント p について In[p], Out[p] を求める p1 Out[p1] In[p2] In[p3] p2 p3
基本となるデータフロー解析 • 到達定義解析 • ある場所で使用される物(変数、式など)がどこで定義されたか?⇒ 前方解析 • 定義使用解析 • ある場所で定義された変数がどこで使用されるか?⇒ 後方解析 一方から容易に他方を求める事が出来る。 今回は到達定義解析の結果から定義使用解析を行う
到達定義解析 • データフロー解析の一種 • 使用される変数や式がどこで定義されているかを調べる • 流れ • まずプログラムポイントを基本ブロックとして解析 • 続いて基本ブロック毎に、プログラムポイントをその中のステートメントとして解析
ブロック単位の到達定義解析:reach 集合 • reach[B]: 基本ブロック B の入り口に到達する定義文の集合 • これを計算する事が解析の目標 s1: i = 0 s2: s = 0 reach[B1] = Φ reach[B2] = {s1, s2, s5, s6, s7} reach[B3] = {s1, s2, s5, s6, s7} B1 s3: if i < 100 B2 s4: t = i s5: t = 2*t s6: i = i + 1 s7: s = s + t B3
ブロック単位の到達定義解析:def集合 • def[B]: 基本ブロック B の中の定義文でB の出口で有効なものの集合 s1: i = 0 s2: s = 0 def[B1] = {s1, s2} def[B2] = Φ def[B3] = {s5, s6, s7} B1 s3: if i < 100 B2 s4: t = i s5: t = 2*t s6: i = i + 1 s7: s = s + t B3
ブロック単位の到達定義解析:kill 集合 • kill[B]: B の出口で無効化される定義文の集合 • 制御フローは考慮しなくてよいB 内の文によって無効化され得る全ての定義文を集める。 s1: i = 0 s2: s = 0 kill[B1] = {s6, s7} kill[B2] = Φ kill[B3] = {s1, s2} B1 s3: if i < 100 B2 s4: t = i s5: t = 2*t s6: i = i + 1 s7: s = s + t B3
到達定義解析のデータフロー方程式 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] = Φ s1: i = 0 s2: s = 0 B1 s3: if i < 100 B2 def[B1] = {s1, s2} def[B2] = Φ def[B3] = {s5, s6, s7} s4: t = i s5: t = 2*t s6: i = i + 1 s7: s = s + t B3 kill[B1] = {s6, s7} kill[B2] = Φ kill[B3] = {s1, s2} ※ここでは entry, exit を省略している
到達定義解析の具体例 B1∈pred(B2) のdef reach[B1] = Φ reach[B2] = {s1,s2} reach[B3] = Φ s1: i = 0 s2: s = 0 B1 s3: if i < 100 B2 def[B1] = {s1, s2} def[B2] = Φ def[B3] = {s5, s6, s7} s4: t = i s5: t = 2*t s6: i = i + 1 s7: s = s + t B3 kill[B1] = {s6, s7} kill[B2] = Φ kill[B3] = {s1, s2} ※ここでは entry, exit を省略している
到達定義解析の具体例 B3∈pred(B2) のdef reach[B1] = Φ reach[B2] = {s1,s2,s5,s6,s7} reach[B3] = Φ s1: i = 0 s2: s = 0 B1 s3: if i < 100 B2 def[B1] = {s1, s2} def[B2] = Φ def[B3] = {s5, s6, s7} s4: t = i s5: t = 2*t s6: i = i + 1 s7: s = s + t B3 kill[B1] = {s6, s7} kill[B2] = Φ kill[B3] = {s1, s2} ※ここでは entry, exit を省略している
到達定義解析の具体例 reach[B1] = Φ reach[B2] = {s1,s2,s5,s6,s7} reach[B3] = {s1,s2,s5,s6,s7} s1: i = 0 s2: s = 0 B1 s3: if i < 100 B2 B2∈pred(B3) のreach\kill def[B1] = {s1, s2} def[B2] = Φ def[B3] = {s5, s6, s7} s4: t = i s5: t = 2*t s6: i = i + 1 s7: s = s + t B3 kill[B1] = {s6, s7} kill[B2] = Φ kill[B3] = {s1, s2} ※ここでは entry, exit を省略している
到達定義解析の具体例 reach[B1] = Φ reach[B2] = {s1,s2,s5,s6,s7} reach[B3] = {s1,s2,s5,s6,s7} s1: i = 0 s2: s = 0 B1 変化なし 変化なし s3: if i < 100 B2 def[B1] = {s1, s2} def[B2] = Φ def[B3] = {s5, s6, s7} s4: t = i s5: t = 2*t s6: i = i + 1 s7: s = s + t B3 kill[B1] = {s6, s7} kill[B2] = Φ kill[B3] = {s1, s2} ※ここでは 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] を見る。 • reach[B] 内でiを定義している文はs1 と s6 • 従って s1: i = 0 s2: s = 0 B1 reach[B3] = {s1,s2,s5,s6,s7} s3: if i < 100 B2 reach[s6,i] = {s1, s6} s4: t = i s5: t = 2*t s6: i = i + 1 s7: s = s + t B3
ステートメント単位の到達定義解析の例 • 最終結果 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 B1 s3: if i < 100 B2 s4: t = i s5: t = 2*t s6: i = i + 1 s7: s = s + t B3
定義使用解析 • 以下を計算する • 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} s1: i = 0 s2: s = 0 s3: if i < 100 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} 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]: ブロック B の入口で利用可能な式集合 • Ekill[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 同じ値を持つが共通部分式じゃない
実際の最適化の例: 値番号付け • 式の「値」を「番号」で識別して共通な値を見つける b = ① c = ② a = ③(③=①+②) d = ① e = ③ a = b + c d = b e = d + c a = 1 b = 1 c = a + 2 d = b + 2 a =① b = ① c = ② (②=①+2) d = ②
実際の最適化の例: 部分冗長性除去 • 値番号付けでも部分冗長なケースは解決できない 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 • ブロックの集合をBとして • NCompa+b: BBoolean (ブロック B が入口計算を持つ) • XCompa+b: BBoolean (ブロック B が出口計算を持つ ) • Transpa+b: BBoolean (ブロック B 内に a, b への代入文がない) x = a + b a = 2 b = x + b y = a + b x = a + b a = 2 b = x + b y = 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 (B の入口挿入点で上安全) • XuSafea+b[B] : Boolean (B の出口挿入点で上安全) • NdSafea+b[B] : Boolean (B の入口挿入点で下安全) • XdSafea+b[B] : Boolean (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 と順番に回すループ • while ループには既に直してあるのだからインデックス変数iを見つけてやればよい for (i = a; i < n; i += d) { … }
Do-All 型ループの検出:帰納的変数検出 • Do-All 型ループのインデックスは帰納的変数になっている • i0 = a • in+1 = in + d • ループ脱出条件によりiの上限 (下限) が決定出来る • 到達定義解析の結果から右のようなパターンを見つければよい i = a if ループ脱出条件 i = i + d
帰納的変数の検出(例) s1: i = 0 s2: s = 0 s = 0 s3: if i < 100 for (i=0;i < 100; i++) t = i t = 2*t s = s + t s4: t = i s5: t = 2*t s6: i = i + 1 s7: s = s + t