知っておいて損はない「コンパイラ最適化」9選

ある程度のコンパイラ最適化を知っておくと、冗長的なコードの記述を回避できたり、余計なパフォーマンスチューニングや無意味なパフォーマンスチューニングをしなくても済むようになります。これを機会にコンパイラ最適化のバリエーションを把握しておきましょう。

コンパイラ最適化への理解は、富豪的プログラミングへの理解や可読性の高いコードを重視する志向にも繋がります。コンパイラのためのコードではなく、プログラマのためのコードが書けるようになります。コンパイラ最適化はプログラマの教養と言うべきものです。

目次

プログラミング言語やコンパイラの種類、最適化レベル等によって結果は異なります。過信はしないようにしましょう。
スポンサーリンク

定数式の事前計算

定数値からなる式は事前に計算されます。(定数畳み込み)

// Before
int value = 1 + 1;

// After
int value = 2;
// Before
int SECONDS_PER_HOUR = 60 * 60;
int SECONDS_PER_DAY  = 60 * 60 * 24;

// After
int SECONDS_PER_HOUR = 3600; // 1時間は3600秒
int SECONDS_PER_DAY  = 86400;

既存の定数値も置き換えの対象になります。(定数伝播)

int SECONDS_PER_WEEK = SECONDS_PER_DAY * 7;
↓
int SECONDS_PER_WEEK = 60 * 60 * 24 * 7;
↓
int SECONDS_PER_WEEK = 86400 * 7;
↓
int SECONDS_PER_WEEK = 604800;

上記の例のように、定数畳み込みと定数伝播は同時に行われることがあります(この過程で更なる最適化が適用される場合もあります)。

質の良いコンパイラでは、関数の呼び出しの結果をコンパイル時計算してくれる場合もあります。

size_t length = strlen("abc");
↓
size_t length = 3;

計算結果の再利用

冗長的なコードや副作用の無い単純な処理は、計算結果の再利用が行われる場合があります。(冗長性の除去)

// Before
int r = (i * 2) + (i * 2) + (i * 2);

// After
int V = (i * 2);   // キャッシュ
int r = V * V * V; // 再利用

もっとも、先程のコードについては次のような最適化が行われる場合もあります。

int r = (i * 2) + (i * 2) + (i * 2);
↓
int r = (i * 2) * 3;
↓
int r = i * 6;

コードの置き換え

非効率な処理は、より効率的な処理に置き換わる場合があります。

// Before
printf("\n");
printf("Shop %d\n", 99);
printf("%c", '9');

// After
puts("");
puts("Shop 99");
putchar('9');

処理の結果は変わりませんが、より低レベルで効率的な関数に置き換わっています。

ループの展開

次のような繰り返し文はインライン化される場合があります。

for (int i = 0; i < 2; i++)
  printf("%d", i);

以下のように、各ループ毎の処理が展開されます。

printf("%d", 0);
printf("%d", 1);

また上記のコードは、先程紹介した最適化と組み合わさって、さらなる置き換えが行われます。

printf("%d", 0);
printf("%d", 1);
↓
printf("0");
printf("1");
↓
putchar('0');
putchar('1');

ループの回数が極端に多くなる場合は、実行ファイルサイズの肥大化を招くため、ループ展開は行われません。

なお、カウンタを参照しないループ文の場合であれば、以下のような最適化が働くケースもあります。

for (int i = 0; i <= 99; i++) puts("hello");
↓
for (int i = 100; i != 0; --i) puts("hello");

これにより実行時のステップ数が軽減される場合があります。ただし、この辺の最適化は処理系によって程度や内容も異なるため過信はできません。

デッドコードの削除

あからさまに実行されることが無いような処理は、実行ファイルに含まれること無く、消えてなくなります。(デッドコード削除)

到達不能コード/デッドコードの例

// 警告: Code will never be executed
if (false) puts("i was here"); // 消える

goto label;
if (false) label: puts("here i am"); // 普通は消えない

int v = 9;
if (v != 9) puts("私はここにいない"); // 普通に消える
int fn() {
  do {
    break;
    puts("what you see"); // 消える
  } while (false);
  return 0;
  return isdigit('9'); // 消える
}
/*    */ int a = 1;
/*    */ int b = 2;
volatile int c = 3; // volatileで最適化を抑制

while (a != 10) {
  printf("%d", a);
  a++; // 消えない
  b++; // 消える
  c++; // 消えない
}
struct { const char* s; int size; } str = {"ab", 2};
if (str.size != 2) puts("1"); // 大抵は消える

str.size = strlen(str.s);
if (str.size != 2) puts("2"); // 消えることがある

以下の最適化は関数のインライン展開と共に働く事があります。

inline void f(bool b) {
  if (b) puts("a");
  puts("b");
}

f(false); // `if (b)`の判定処理は省略される事が多い

関数のインライン展開については次項以降に紹介します。

中でも以下の最適化は意外と便利で気に入っています。リファクタリングも効くのでコメントやプリプロセッサよりもスマートに扱えます。このケースでは到達不能コードに対する警告はされません。

#ifdef DEBUG
bool isDebug = true;
#else
bool isDebug = false;
#endif

if (isDebug) {
  // リリースビルド時に消えることが多い
  puts("password"); // ただし、パスワードを扱っているため過信は禁物
}

同様に、以下のような記述についてもコンパイラ側の警告は発生しません。なかなか面白い気の利かせ方です。中の人御用達のテクニックだったりするのかもしれません。

return 0;
return isdigit('9'); // 消える

関数のインライン展開

関数の呼び出し箇所に、関数の処理が展開されます。(インライン展開)

// インライン関数
inline void print(int i) { printf("%d" i); }
// Before
int main() {
   print(99);
}

// After
int main() {
   printf("%d" 99); // print関数の処理が展開された
}

これによって関数呼び出しによるオーバロードが回避され、より高速な処理が実現されます。

関数定義時にinlineキーワードを指定することで、コンパイラ側にインライン展開を促す事ができます。ただしinlineを指定してもインライン化が行われない場合もあります。逆にinlineを指定していない関数であっても、インライン展開が行われる場合があります。

コンパイラによっては__attribute__((always_inline))という属性機能を利用することで、関数のインライン化を強制することもできます。ただし、関数のインライン化によって逆に処理効率が悪くなるケースもあるため、インライン関数の強制は無闇に行うべきではありません。

関数のインライン展開と更なる最適化

インライン展開後の処理に対しても「# デッドコードの削除」が働くことがあります。

inline void f(bool b) {
   if (b) puts("a"); else puts("b");
}
f(true);
↓
if (true) puts("a"); else puts("b");
↓
puts("a");
f(false);
↓
if (false) puts("a"); else puts("b");
↓
puts("b");

この組み合わせによる最適化は、冗長的なコードを回避する上で非常に重要な最適化となっています。

この最適化によって、例えば以下のようなコードが躊躇なく書けるようになります。

inline void c(bool flag) {
   puts("begin");
   if (flag) puts("a"); else puts("b");
   puts("end");
}

void a() { c(true);  }
void b() { c(false); }

コンパイラ最適化によってインライン展開が働くと、abの関数はそれぞれ以下のように展開されます。

void a() {
   puts("begin");
   puts("a");
   puts("end");
}

void b() {
   puts("begin");
   puts("b");
   puts("end");
}

オーバーヘッドの無い理想的なコードに変化しました。このような展開後のコードは、冗長的で保守性も悪くなるため、実際の人間が書くべきではありませんが、コンパイラによるインライン展開とデッドコード削除の最適化を活用すれば、このような「冗長的ではあるが効率的な処理」を自動的に生成することもできます。

再帰呼び出しの除去

void print_dot(int n) {
  if (n) {
    printf(".");
    print_dot(n - 1);
  }
}

再帰呼び出しが関数の末尾で行われている場合は、再帰呼び出しが除去され、ループ文に置き換わる場合があります。

void print_dot(int n) {
  while (n--) {
    printf(".");
  }
}

print_dot呼び出し後にprintfを呼び出すような処理を書いてしまうと、最適化の対象にはならないため注意が必要です。

goto文への置き換え

非効率なコードがより効率的なジャンプ命令に置き換わる場合があります。

元コード

for (int i = 1; i <= 9; i++) {
  bool escape = false;
  for (int j = 1; j <= 9; j++) {
    if (i * j == 10) {
      escape = true; // 非効率
      break;
    }
    printf("%d, ", i * j);
  }
  if (escape) break; // 非効率
  printf("\n");
}

fflush(stdout);

最適化後の擬似コード

for (int i = 1; i <= 9; i++) {
  // bool escape = false;
  for (int j = 1; j <= 9; j++) {
    if (i * j == 10) {
      // escape = true; // 非効率
      // break;
      goto label; // 置き換わった
    }
    printf("%d, ", i * j);
  }
  // if (escape) break; // 非効率
  printf("\n");
}

label: // ここにジャンプする
fflush(stdout);

後置インクリメントの値更新タイミング

このコードは、

if (c != *s++) return false;

このように最適化されることがあります。

if (c != *s) return false;
++s;

return false;実行前の余計なインクリメント処理が回避されました。

for文 条件式の最適化

for文の条件式内の関数呼び出しの結果はキャッシュされる場合があります。

for (int i = 0; i < string.length(); i++);

上記のコードは、以下ように最適化される場合があります。

int c = string.length();
for (int i = 0; i < c; i++);

ただし、lengthメソッドが返す内部値は基本的にプリミティブ型の定数である必要があります。またメソッドに副作用があってはなりません。

stringクラスとlengthメソッドの定義例は以下の通りです。

const struct String {
  const char* s;
  const size_t n;
  String(const char* s) : s(s), n(strlen(s)) {}
  size_t length() const { return n; }
} string = "abc";
ミュータブルなオブジェクトに対しても同様の最適化が行われることがあります。

これらの最適化はlength関数の結果がプリミティブ型の定数の場合に限ります。パスカル文字列やポインタへの間接参照は最適化の対象にならない可能性があります。

ですから以下のようなケースは、strlen関数を使用しているため結果は純粋な定数とはみなされず、ループの度にlength関数が呼ばれることになります。

const struct {
  const char* s;
  size_t length() const { return strlen(s); }
} string = {"abc"};

for (size_t i = 0; i < string.length(); i++);

広告