Skip to content
Published on

[コンパイラ] 15. ループ最適化とコード移動

Authors

概要

プログラムの実行時間の大部分はループで消費されます。したがってループを最適化することが全体的なプログラム性能向上に最も効果的です。この記事では、ループを識別し最適化する技法を体系的に見ていきます。


1. 支配者(Dominator)

1.1 定義

フローグラフでノードdがノードnを**支配(dominate)**する = 入口点からnへのすべての経路がdを経由する。

記号で d dom n と表記します。

性質:

  • すべてのノードは自分自身を支配する(反射的)
  • d dom nかつn dom dならばd = n(反対称的)
  • d dom nかつn dom pならばd dom p(推移的)

1.2 支配者木(Dominator Tree)

支配関係をツリーで表現したものです。各ノードの親はそのノードの**直接支配者(immediate dominator)**です。

// フローグラフ:
//   1 -> 2 -> 3 -> 4
//        |    ^    |
//        v    |    v
//        5 ---+    6
//                  |
//                  v
//                  7

// 支配者木:
//        1
//        |
//        2
//      / | \
//     3  5  6
//     |     |
//     4     7
//
// 解釈:1 dom すべてのノード
//       2 dom 3,4,5,6,7
//       3 dom 4
//       6 dom 7

1.3 支配者計算アルゴリズム

反復アルゴリズムで支配者を計算します:

アルゴリズム:支配者計算

初期化:
  DOM[entry] = {entry}
  DOM[n] = すべてのノードの集合(n != entry)

反復(収束するまで):
  for(すべてのノード n != entry):
    DOM[n] = {n} U ( n DOM[p] )  // pはnのすべての先行者
                                  // 先行者のDOMの積集合にnを追加

1.4 支配境界(Dominance Frontier)

ノードnの支配境界 DF(n)はnが支配する領域のすぐ外側の境界です。

DF(n) = ノードyの集合 | nがyの先行者を支配するが、y自体は(厳密には)支配しない

支配境界はSSA構築でphi関数の挿入位置を決定するのに核心的に使用されます。


2. 自然ループ(Natural Loop)

2.1 後退辺(Back Edge)

フローグラフで辺 n -> d においてdがnを支配するなら、この辺を後退辺と呼びます。

// フローグラフで:
//   1 -> 2 -> 3 -> 4
//        ^         |
//        +---------+   // 4 -> 2は後退辺(2 dom 4)

2.2 自然ループの定義

後退辺 n -> d に対する自然ループ

  • ヘッダ:ノードd(ループの唯一の入口点)
  • 本体:dを経由せずにnに到達できるすべてのノードの集合とd
アルゴリズム:自然ループ構成(後退辺 n -> d)

1. loop = {d}
2. stack = {n}(nがdと異なる場合)
3. while stackが空でない:
     m = stackからpop
     if mがloopにない:
         loopにmを追加
         mのすべての先行者をstackにpush
4. 結果:loop

2.3 例

// フローグラフ:
//   B1 -> B2 -> B3 -> B4
//          ^          |
//          +----------+  (後退辺:B4 -> B2)
//          |
//          +-- B5(B2 -> B5、B5 -> B2も後退辺)

// 後退辺 B4 -> B2 の自然ループ:
//   {B2, B3, B4}(B2を経由せずB4に到達可能:B3、B4)

// 後退辺 B5 -> B2 の自然ループ:
//   {B2, B5}

2.4 ループのネスト

2つのループが同じヘッダを共有する場合、合わせて一つのループとして扱います。そうでなければ一方が他方に完全に含まれる(ネスト)か、互いに素です。

// 外側ループ:{B1, B2, B3, B4, B5}
// 内側ループ:{B3, B4}
//
// 内側ループを先に最適化した後、外側ループを最適化
// (内側から外側へ)

3. ループ不変コードの検出と移動

3.1 ループ不変コードの検出

命令 s: x = y op z がループ不変である条件:

  1. yとzのすべての定義がループ外にあるか
  2. y(またはz)の定義が正確に一つで、その定義が既にループ不変と判定されている場合
  3. y(またはz)が定数
アルゴリズム:ループ不変コード検出

1. 定数であるかループ外でのみ定義された被演算子を持つ命令を
   「ループ不変」としてマーク
2. 反復:
   - すべての被演算子が定数であるか、ループ外定義であるか、
     既にループ不変とマークされた定義一つだけを持つ命令を
     新たに「ループ不変」としてマーク
3. 新たなマークがなければ終了

3.2 例

// ループ:
//   B2: i = i + 1
//       t1 = n * 4       // nはループ外で定義 -> ループ不変
//       t2 = a[t1]       // t1がループ不変でaがループ内で不変 -> ループ不変
//       t3 = t2 + i
//       if i < 100 goto B2

// 1回目:t1 = n * 4 マーク(nがループ外定義)
// 2回目:t2 = a[t1] マーク(t1がループ不変)
// 3回目:もうない -> 終了

3.3 コード移動条件

ループ不変命令 s: x = y op z をループ外(preheader)に移動するには以下の条件をすべて満たす必要があります:

条件1:sを含むブロックがループのすべての出口を支配

// 安全な場合:
//   preheader -> [B1: s: x=... ] -> B2(出口)
//   B1が出口を支配するので移動可能

// 危険な場合:
//   preheader -> B1 -> B3(出口)
//                |
//                v
//               B2: s: x=...
//   B2が出口B3を支配しない -> 移動不可
//   (sが実行されない経路が存在)

条件2:xがループ内で他の場所で定義されていない

条件3:ループ内でxを使用するすべての箇所がsの定義にのみ到達

3.4 Preheaderの挿入

コード移動のためにループヘッダの直前にpreheaderブロックを挿入します。

// 移動前:
//   ... -> header -> body -> ...
//           ^               |
//           +---------------+

// preheader挿入後:
//   ... -> preheader -> header -> body -> ...
//                        ^               |
//                        +---------------+

// ループ不変コードをpreheaderに配置:
//   preheader:
//     t1 = n * 4       // 移動されたコード
//     t2 = a[t1]       // 移動されたコード

4. 誘導変数の検出と除去

4.1 基本誘導変数(Basic Induction Variable)

ループ内で i = i + c(cはループ定数)形式でのみ定義される変数iを基本誘導変数と呼びます。

4.2 派生誘導変数(Derived Induction Variable)

基本誘導変数iに対して j = c1 * i + c2(c1、c2はループ定数)で表現される変数jを派生誘導変数と呼びます。

誘導変数の三つ組(triple)表記:(i, c1, c2)j = c1 * i + c2 を意味します。

// 例:
// i = i + 1        // iは基本誘導変数
// t1 = 4 * i       // t1は派生誘導変数:(i, 4, 0)
// t2 = t1 + base   // t2は派生誘導変数:(i, 4, base)

4.3 誘導変数検出アルゴリズム

アルゴリズム:誘導変数検出

ステップ1:基本誘導変数の検出
  - ループ内で i = i + c 形式でのみ定義される変数を見つける

ステップ2:派生誘導変数の検出
  - j = c1 * i(iが基本誘導変数、c1がループ定数)
    -> jの三つ組:(i, c1, 0)
  - j = k + c2(kが誘導変数 (i, a, b)、c2がループ定数)
    -> jの三つ組:(i, a, b+c2)
  - j = c1 * k(kが誘導変数 (i, a, b))
    -> jの三つ組:(i, c1*a, c1*b)

4.4 強度削減の適用

派生誘導変数の乗算を加算に変換します。

// 元のコード:
// i = 0
// loop:
//   t1 = 4 * i       // 乗算
//   a[t1] = 0
//   i = i + 1
//   if i < n goto loop

// 強度削減後:
// i = 0
// t1 = 0             // 初期値:4 * 0 = 0
// loop:
//   a[t1] = 0
//   i = i + 1
//   t1 = t1 + 4      // 乗算 -> 加算
//   if i < n goto loop

4.5 誘導変数の除去

強度削減後、基本誘導変数がループ終了条件でのみ使用される場合、派生誘導変数で置き換えることができます。

// 強度削減後:
// i = 0
// t1 = 0
// loop:
//   a[t1] = 0
//   i = i + 1         // iは比較でのみ使用
//   t1 = t1 + 4
//   if i < n goto loop

// 誘導変数除去後:
// t1 = 0
// limit = 4 * n       // 終了条件変換(ループ外で1回だけ計算)
// loop:
//   a[t1] = 0
//   t1 = t1 + 4
//   if t1 < limit goto loop
//
// iが完全に除去!

5. ループ展開(Loop Unrolling)

5.1 概念

ループ本体を複数回コピーして反復回数を減らす技法です。

5.2 基本ループ展開

// 元(100回反復)
for (i = 0; i < 100; i++) {
    a[i] = 0;
}

// 4倍展開(25回反復)
for (i = 0; i < 100; i += 4) {
    a[i]   = 0;
    a[i+1] = 0;
    a[i+2] = 0;
    a[i+3] = 0;
}

5.3 利点

  1. ループオーバーヘッドの削減:分岐、比較、増分命令の回数削減
  2. 命令スケジューリング機会の増加:独立した命令が増えパイプライン活用度が向上
  3. レジスタ活用の改善:連続した演算間で値の再利用が可能

5.4 欠点と考慮事項

// 反復回数が展開係数の倍数でない場合
// 余り処理が必要

// nが4の倍数でない可能性がある場合
i = 0
// メインループ(4倍展開)
while (i + 3 < n) {
    a[i] = 0; a[i+1] = 0; a[i+2] = 0; a[i+3] = 0;
    i += 4;
}
// 余り処理
while (i < n) {
    a[i] = 0;
    i++;
}
  • コードサイズの増加:キャッシュに悪影響を及ぼし得る
  • レジスタ圧迫:展開が過度だとレジスタspillが発生し得る
  • 一般に2倍から8倍の展開が効果的

6. ループ最適化の総合例

全体のループ最適化過程を一つの例で見てみましょう。

// 元のコード(行列の1行を初期化)
i = 0
loop:
    t1 = i * 8           // 誘導変数(乗算)
    t2 = base + t1       // 誘導変数
    *t2 = 0.0
    i = i + 1
    if i < n goto loop

// ステップ1:ループ不変コード移動
//   (この例では該当なし)

// ステップ2:強度削減
i = 0
t1 = 0                   // 初期値
t2 = base                // 初期値
loop:
    *t2 = 0.0
    i = i + 1
    t1 = t1 + 8          // 乗算 -> 加算
    t2 = t2 + 8          // 乗算 -> 加算
    if i < n goto loop

// ステップ3:誘導変数除去(iとt1を除去)
t2 = base
limit = base + n * 8     // ループ外で1回計算
loop:
    *t2 = 0.0
    t2 = t2 + 8
    if t2 < limit goto loop

// ステップ4:ループ展開(2倍)
t2 = base
limit = base + n * 8
loop:
    *t2 = 0.0
    *(t2 + 8) = 0.0
    t2 = t2 + 16
    if t2 < limit goto loop
// (余り処理コードは省略)

元はループ1回の反復に乗算1回、加算2回、ストア1回、比較1回が必要でしたが、最適化後は加算1回、ストア2回、比較1回に減りました。


まとめ

概念説明
支配者入口点から特定ノードへのすべての経路が通るノード
支配者木支配関係をツリーで表現した構造
後退辺支配者に戻る辺(ループ形成)
自然ループ後退辺によって定義されるループ
ループ不変コード移動毎反復同じ値を計算するコードをループ外に移動
誘導変数ループで一定に増減する変数
強度削減乗算を加算に置換
誘導変数除去不要になった誘導変数の除去
ループ展開ループ本体をコピーして反復回数を削減

ループ最適化はコンパイラ最適化で最大の性能向上をもたらす分野です。支配者分析でループを識別し、不変コード移動と強度削減でループ本体を軽量化し、ループ展開でオーバーヘッドを減らすことが基本戦略です。次の記事では、命令レベル並列性とスケジューリングを扱います。