C/C++言語でコンパイラの最適化について紹介します。
この分野は奥が深いので、基本的なところを例で説明します。
コンパイラの最適化が有効な場合は、コードがより高速に実行される可能性があります。
最適化が有効な例
まず、代表的なものを2つ紹介します。(詳細解説は後半で)
例1: ループのアンローリング
int main() { int sum = 0; for (int i = 1; i <= 10; ++i) { sum += i; } std::cout << "Sum: " << sum << std::endl; return 0; }
この簡単なプログラムでは、1から10までの整数を足して合計を計算しています。
最適化が有効な場合、コンパイラはループをアンローリングして、より効率的なコードを生成することができます。
例2: インライン関数
inline int square(int x) { return x * x; } int main() { int result = square(5); std::cout << "Square: " << result << std::endl; return 0; }
ループのアンローリングとは
ループのアンローリング(Loop Unrolling)は、コンパイラがプログラムのループを展開して、ループ内の複数のイテレーション(反復処理)を1回のイテレーションとして処理する最適化手法です。
これにより、ループ制御のオーバーヘッドや分岐の予測ミスを減少させ、プログラムの実行速度を向上させることができます。
具体的には、通常のループは次のように表現されます。
for (int i = 0; i < N; ++i) {
// ループ内の処理
}
これをアンローリングすると、次のようになります。
forのループの回数は半分にして、1つのループの中で i と i+1の処理を行っています。
—
for (int i = 0; i < N; i += 2) {
// ループ内の処理(i)
// ループ内の処理(i + 1)
}
// 残りの処理(iが奇数の場合)
—-
このように、元のループが1回のイテレーションで行っていた処理を、アンローリングによって2回のイテレーションで行うようにします。
これにより、ループの制御文や条件分岐のオーバーヘッドが減り、プログラムの性能が向上します。
ただし、アンローリングはメモリ使用量を増加させる可能性があり、またループ回数が少ない場合やループ内の処理が複雑である場合には逆に効果が薄れることがあります。
コンパイラはこれらの要因を考慮しながら最適なアンローリングを選択します。
インライン関数について
インライン関数で性能を上げることが可能です。
コンパイラは、自動で最適化の1つとして inline宣言なしの関数もインライン化を実施していることがあります。
インライン関数(Inline Function)は、プログラムの中で関数呼び出しのオーバーヘッドを削減し、コードをより効率的に実行できるようにするためのC++言語の機能です。
通常、関数が呼び出されると、実行のために関数の呼び出しと復帰が発生しますが、インライン関数を使用すると、関数の本体がその場に展開され、関数呼び出し自体がなくなります。
インライン関数は通常、短い処理や小さな関数に適しています。以下は、インライン関数の基本的な使い方と特徴です。なんでも「inline」と関数の頭に書くとインライン化される訳ではありません。
インライン関数の定義方法:
// インライン関数の定義 inline int add(int a, int b) { return a + b; } int main() { int result = 0; result = add(3, 4); // インライン関数が呼び出される result = add(9, 8); // インライン関数が呼び出される return 0; }
<インライン化のイメージ>
インライン関数が展開されて埋め込まれます。
こんなイメージです。
result = a + b;
result = a + b ;
・関数呼び出しオーバーヘッドが減り、処理が高速化されます。(Good)
・inline関数を使っているところに直接コードが埋め込まれるので、全体のサイズが大きくなります。
インライン関数の特徴
インライン関数の特徴としては以下があります。
a)コンパクトなコード: インライン関数は関数呼び出しのオーバーヘッドがないため、コードがよりコンパクトになります。
b)高速な実行: 関数の本体が直接展開されるため、関数呼び出しや復帰のコストがなくなり、実行速度が向上します。
c)小さな関数に適している: インライン関数は一般的に小さなサイズの関数に対して有効です。
d)ヘッダーファイルでの定義: インライン関数は通常、ヘッダーファイル内で定義されます。これにより、ヘッダーファイルを複数のソースファイルでインクルードする場合に、関数の本体が各ソースファイルに展開されます。
// ヘッダーファイル内でのインライン関数の定義 // header.h #ifndef HEADER_H #define HEADER_H inline int add(int a, int b) { return a + b; } #endif // HEADER_H -------- // ソースファイル内でのヘッダーファイルのインクルード // main.cpp #include "header.h" int main() { int result = add(3, 4); // インライン関数が呼び出される return 0; }
ただし、インライン関数を多用するとコードサイズが増加し、逆に性能が低下する可能性があるため、適切に使用する必要があります。
他の最適化の例
他の最適化の例も挙げてみましょう。
C++コンパイラは多くの最適化手法を使用して、プログラムの実行性能を向上させます。
コードの不要な計算の削減
int result = square(5) + square(5);
同じ関数を2回実行せずに。 コンパイラはsquare(5)を1回しか計算しないように最適化します。1回計算して、2個を足します。
int square(int x) { return x * x; } int main() { int result = square(5) + square(5); // コンパイラはsquare(5)を1回しか計算しないように最適化 return 0; }
定数畳み込み (Constant Folding)
コンパイラはxとyが定数であることを知っているため、実行時に計算せずにコンパイル時に結果を計算済みにします。
result に計算結果の 15が埋め込まれる。
const int x = 5;
const int y = 10;
int result = x + y; // 5 +10→15 がコンパイル時に埋まる
デッドコード削除
変数resultが使われていないため、コンパイラはfunc(5)の呼び出しを最適化します。
int func(int x) { int y = x * 2; return y; } int main() { int result = func(5); // resultが使われていないため、コンパイラはfunc(5)の呼び出しを最適化 return 0;
ループ不変式コードの移動 (Loop Invariant Code Motion)
sum += 5;
コンパイラはループ内での5の加算をループ外に移動して、効率的なコードを生成します。
ループが100回の固定数なので、ループを使わずに実行可能。
int main() { int sum = 0; for (int i = 0; i < 100; ++i) { sum += 5; // コンパイラはループ内での5の加算をループ外に移動して、効率的なコードを生成 } return 0; }
メモリアクセスの最適化
コンパイラは最適なメモリアクセス方法を選択して、高速なコードを生成する。
1つずつ実行するのではなく、100回分を一度にメモリコピーするイメージ。
int main() { int arr[100]; int sum = 0; for (int i = 0; i < 100; ++i) { sum += arr[i]; // コンパイラは最適なメモリアクセス方法を選択して、高速なコードを生成 } return 0; }
最後に
これらの最適化は、コンパイラがプログラムを解析して最適な形に変換することで、実行速度の向上やメモリの効率的な利用を可能にします。
最適化の程度はコンパイラやコンパイルオプションによって異なります。
こういうことを知っていると自分でコードを書く時に工夫すれば、実行速度が速いコードを書くことができるようになりますね。