Skip to content
Published on

[コンパイラ] 13. マシン非依存コード最適化の基礎

Authors

概要

マシン非依存コード最適化(Machine-Independent Code Optimization)は、特定のハードウェアに依存せず、中間コードレベルでプログラムの効率性を高める技法です。これらの最適化はプログラムの意味を保存しながら、実行時間やコードサイズを削減します。

この記事では、主要な最適化技法の原理を例とともに見ていき、これらの最適化の基盤となるデータフロー分析を紹介します。


1. 共通部分式の除去(Common Subexpression Elimination)

1.1 概念

同じ被演算子に対して同じ演算が2回以上行われる場合、2回目以降の計算を除去し、以前の結果を再利用します。

1.2 局所的共通部分式の除去

一つの基本ブロック内での共通部分式の除去です。

// 最適化前
a = b + c
d = a - e
f = b + c    // b + cは既に計算済み
g = f - e

// 最適化後
a = b + c
d = a - e
f = a        // b + cの代わりにaを再利用
g = f - e    // またはg = d(a-eも共通部分式)

1.3 大域的共通部分式の除去

複数の基本ブロックにまたがる共通部分式の除去です。これには**利用可能式(Available Expressions)**分析が必要です。

// ブロックB1:
//   t1 = a + b
//   ...
// ブロックB2:(B1から到達可能、aとbが変更されていない)
//   t2 = a + b    // B1のt1と同じ値

// 最適化後
// ブロックB2:
//   t2 = t1       // 再利用

注意点:2つの計算の間に被演算子(aやb)が変更されていないことを確認する必要があります。

1.4 実用的な例

配列アクセスでは共通部分式が頻繁に発生します:

// Cコード
x = a[i][j] + a[i][j+1];
// コンパイル時に生成される中間コード
t1 = i * n       // 行オフセット
t2 = t1 + j      // a[i][j]のインデックス
t3 = a[t2]
t4 = i * n       // 共通部分式!(t1と同じ)
t5 = t4 + j + 1  // a[i][j+1]のインデックス
t6 = a[t5]
x = t3 + t6

// 最適化後
t1 = i * n
t2 = t1 + j
t3 = a[t2]
t5 = t1 + j + 1  // t4の代わりにt1を再利用
t6 = a[t5]
x = t3 + t6

2. デッドコード除去(Dead Code Elimination)

2.1 概念

デッドコードとは計算結果がその後まったく使用されないコードです。これを除去すると不要な演算を減らせます。

2.2 例

// 最適化前
a = b + c       // aがその後使用されなければデッドコード
d = e * f
return d

// 最適化後
d = e * f
return d

2.3 デバッグコードでよく発生

// Cコードの例
int debug = 0;    // デバッグモードオフ

void process() {
    // ... 処理ロジック ...

    if (debug) {    // 条件は常にfalse
        printf("Debug info");  // デッドコード
    }
}

定数伝播後に debug が0であることが分かるので、if ブロック全体がデッドコードになります。

2.4 デッドコードの判定

変数xへの代入 x = expr がデッドコードである条件:

  • xがその代入以降で**生存(live)**でない場合
  • つまり、xの値がプログラムの出力や副作用に影響を与えない場合

これを判定するために**生存変数分析(Live Variable Analysis)**が必要です。


3. 定数畳み込みと定数伝播

3.1 定数畳み込み(Constant Folding)

被演算子がすべて定数の演算をコンパイル時に事前に計算します。

// 最適化前
x = 3 + 5
y = x * 2

// 定数畳み込み後
x = 8
y = x * 2

// 定数伝播 + 定数畳み込み後
x = 8
y = 16

3.2 定数伝播(Constant Propagation)

変数に定数が代入されると、その変数を使用する箇所に定数を直接代入します。

// 最適化前
n = 10
i = 0
limit = n - 1

// 定数伝播後
n = 10
i = 0
limit = 10 - 1  // nを10に置換

// 定数畳み込み後
n = 10
i = 0
limit = 9

3.3 条件付き定数伝播

分岐条件に定数が伝播されると、分岐自体を除去できます。

// 最適化前
x = 1
if x > 0 goto L1
goto L2

// 定数伝播 + 定数畳み込み後
x = 1
if 1 > 0 goto L1  // 常にtrue
goto L2            // 到達不能

// 分岐簡約後
x = 1
goto L1

4. ループ不変コード移動(Loop-Invariant Code Motion)

4.1 概念

ループ内で毎回同じ値を計算するコードをループの外に移動します。

4.2 例

// 最適化前
while (i < limit) {
    // t = n * 4 はループ不変
    t = n * 4
    a[i] = t + i
    i = i + 1
}

// 最適化後
t = n * 4          // ループ外に移動
while (i < limit) {
    a[i] = t + i
    i = i + 1
}

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

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

  • yとzの定義がすべてループ外にあるか
  • yとzが定数であるか
  • yとzを定義する命令が既にループ不変と判定されている場合

4.4 移動条件

ループ不変コードでも無条件に移動できるわけではありません。移動が安全な条件:

命令 s: x = y op z をループ外に移動できる条件:

1. sが含まれるブロックがループのすべての出口を支配(dominate)
2. xがループ内で他の場所で定義されていない
3. ループ内でxを使用するすべての箇所がsの定義にのみ到達

5. 誘導変数と強度削減

5.1 誘導変数(Induction Variable)

ループで毎回一定量ずつ変化する変数を誘導変数と呼びます。

// iは基本誘導変数(毎反復+1)
// jは派生誘導変数(j = 4*iなので毎反復+4)
for (i = 0; i < n; i++) {
    j = 4 * i
    a[j] = 0
}

5.2 強度削減(Strength Reduction)

誘導変数に対するコストの大きい演算(乗算)をコストの小さい演算(加算)に置き換えます。

// 最適化前
i = 0
L1: if i >= n goto L2
    j = 4 * i        // 乗算(高コスト)
    a[j] = 0
    i = i + 1
    goto L1
L2:

// 強度削減後
i = 0
j = 0              // jの初期値
L1: if i >= n goto L2
    a[j] = 0
    i = i + 1
    j = j + 4       // 乗算を加算に置換
    goto L1
L2:

5.3 誘導変数の除去

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

// 強度削減後
i = 0
j = 0
L1: if i >= n goto L2
    a[j] = 0
    i = i + 1
    j = j + 4
    goto L1
L2:

// 誘導変数除去後(iをjに置換)
j = 0
limit = 4 * n      // 終了条件の変換
L1: if j >= limit goto L2
    a[j] = 0
    j = j + 4
    goto L1
L2:

乗算がループ外に移動して1回だけ実行され、ループ内では加算と比較のみが行われます。


6. コピー伝播(Copy Propagation)

6.1 概念

コピー命令 x = y の後で、xの代わりにyを直接使用します。

// 最適化前
x = y
z = x + 1     // xの代わりにy使用可能

// コピー伝播後
x = y          // この行はデッドコードになり得る
z = y + 1

コピー伝播自体はコードを減らしませんが、その後のデッドコード除去と組み合わせると効果的です。

6.2 共通部分式除去との関係

// 共通部分式除去後
a = b + c
...
f = a          // b + cの代わりにコピー

// コピー伝播後(fの使用箇所にaを代入)
// fが不要になればデッドコードとして除去

7. データフロー分析の紹介

上で説明した最適化を安全に適用するには、プログラムのデータフローを分析する必要があります。

7.1 データフロー分析とは

データフロー分析(Data-Flow Analysis)は、プログラムの各地点で特定の性質が成立するかを判定する技法です。

主なデータフロー分析:

分析質問用途
到達定義(Reaching Definitions)この地点で変数xの値はどこで定義されたか?定数伝播、共通部分式除去
生存変数(Live Variables)この地点の変数値がその後使用されるか?デッドコード除去、レジスタ割り当て
利用可能式(Available Expressions)この式が既に計算されて使用可能か?大域的共通部分式除去

7.2 基本概念:genとkill集合

各基本ブロックBに対して:

  • gen(B):ブロックBで新たに生成される情報
  • kill(B):ブロックBで無効化される情報
// ブロックB:
//   d1: x = a + b    // gen: d1, kill: xの他のすべての定義
//   d2: y = c - d    // gen: d2, kill: yの他のすべての定義

7.3 反復アルゴリズムの予告

データフロー方程式を反復的に解いて安定状態(fixed point)に到達します:

// 到達定義分析の伝達関数
OUT[B] = gen(B) U (IN[B] - kill(B))

// IN[B]はすべての先行ブロックのOUTの和集合
IN[B] = U OUT[P]  (PはBのすべての先行者)

詳細は次の記事で扱います。


8. 最適化の適用順序

最適化を適用する順序も重要です。一般的な順序:

1. 定数畳み込み / 定数伝播
   -> 定数を先に確定して他の最適化に貢献

2. 共通部分式の除去
   -> 重複計算の除去

3. コピー伝播
   -> 不要なコピーの除去準備

4. デッドコード除去
   -> 上記の最適化によって発生したデッドコードの整理

5. ループ不変コード移動
   -> ループ外へのコード移動

6. 誘導変数 / 強度削減
   -> ループ内のコスト削減

7. 反復(収束するまで)

まとめ

最適化技法効果必要な分析
共通部分式の除去重複計算の防止利用可能式分析
デッドコード除去不要なコードの削除生存変数分析
定数畳み込み/伝播コンパイル時に事前計算到達定義分析
ループ不変コード移動ループ内反復計算の除去到達定義、支配者分析
誘導変数/強度削減乗算を加算に置換誘導変数検出
コピー伝播不要なコピーの除去到達定義分析

マシン非依存最適化は、コンパイラが生成するコードの品質を大幅に向上させます。特にループ関連の最適化(ループ不変コード移動、強度削減)は、プログラム実行時間の大部分を占めるループの効率を改善するため、実質的な性能向上効果が大きいです。次の記事では、これらの最適化の基盤となるデータフロー分析フレームワークを詳しく見ていきます。