C++: パフォーマンス最適化
最終更新:2026-08-31
レッスン48では、C++17/20について学びました。
ここでは、パフォーマンス最適化について学びます。
C++の強みはパフォーマンスです。しかし、最適化には正しい知識とツールが必要です。
1. 最適化の基本原則
(1) 1.1 なぜ最適化が必要?
原則:
- 早すぎる最適化は避ける(Premature Optimization)
- 測ってから最適化
- アルゴリズムを最適化、コードを最適化
(2) 1.2 パフォーマンス要因
| 要因 | 影響 |
|---|---|
| アルゴリズム | 70% |
| メモリアクセス | 20% |
| その他 | 10% |
要点: アルゴリズムの最適化はコード最適化より重要。
2. メモリアライメント
(1) 2.1 メモリアライメントとは?
メモリアライメントはメモリアドレスの配置方法で、CPUの読み書き効率に影響します。
なぜ重要?
- CPUはアライメントされたメモリへのアクセスが高速
- アライメントが不正だとペナルティ
(2) 2.2 例:メモリアライメントの影響(難易度 ⭐⭐)
▶ サンプル 1:オブジェクト指向プログラミング(難易度 ⭐)
CPP
#include <iostream>
struct BadAlignment {
char c; // 1バイト
int i; // 4バイト(パディング3バイト)
};
struct GoodAlignment {
int i; // 4バイト
char c; // 1バイト
};
int main() {
std::cout << "Bad: " << sizeof(BadAlignment) << " bytes" << std::endl;
std::cout << "Good: " << sizeof(GoodAlignment) << " bytes" << std::endl;
return 0;
}
出力:
TEXT 📖 参照専用Bad: 8 bytes Good: 8 bytes
💡 ヒント:
- メンバの宣言順を変えることでパディングを削減可能
3. キャッシュ最適化
(1) 3.1 CPUキャッシュ
CPUキャッシュはメモリより100倍高速です。
最適化のポイント:
- 局所性:メモリの近い場所にアクセス
- 順次アクセス:ランダムアクセスより高速
- キャッシュミスを避ける:メモリの飛び飛びアクセスを避ける
(2) 3.2 例:行優先 vs 列優先(難易度 ⭐⭐⭐)
CPP
#include <iostream>
#include <vector>
#include <chrono>
int main() {
const int N = 1000;
std::vector<std::vector<int>> matrix(N, std::vector<int>(N));
// 行優先(速い)
auto start = std::chrono::high_resolution_clock::now();
for (int i = 0; i < N; i++) {
for (int j = 0; j < N; j++) {
matrix[i][j] = 1;
}
}
auto end = std::chrono::high_resolution_clock::now();
auto row_time = std::chrono::duration<double>(end - start).count();
// 列優先(遅い)
start = std::chrono::high_resolution_clock::now();
for (int j = 0; j < N; j++) {
for (int i = 0; i < N; i++) {
matrix[i][j] = 1;
}
}
end = std::chrono::high_resolution_clock::now();
auto col_time = std::chrono::duration<double>(end - start).count();
std::cout << "行優先:" << row_time << " 秒" << std::endl;
std::cout << "列優先:" << col_time << " 秒" << std::endl;
return 0;
}
4. コンパイラ最適化
(1) 4.1 最適化フラグ
| フラグ | 説明 |
|---|---|
| -O0 | 最適化なし(デバッグ用) |
| -O1 | 基本的な最適化 |
| -O2 | 推奨(バランス) |
| -O3 | 積極的な最適化 |
| -Os | コードサイズ最適化 |
(2) 4.2 例:コンパイラ最適化(難易度 ⭐)
CPP
#include <iostream>
int main() {
int sum = 0;
for (int i = 0; i < 1000000; i++) {
sum += i;
}
std::cout << sum << std::endl;
return 0;
}
コンパイル:
BASH
g++ -O0 main.cpp # 最適化なし
g++ -O2 main.cpp # 最適化あり(高速)
5. パフォーマンス測定
(1) 5.1 プロファイリングツール
| ツール | プラットフォーム | 説明 |
|---|---|---|
| gprof | Linux | GCC標準プロファイラ |
| perf | Linux | Linux標準プロファイラ |
| Valgrind | クロスプラットフォーム | メモリ分析 |
| Visual Studio Profiler | Windows | VS標準 |
(2) 5.2 例:std::chronoで測定(難易度 ⭐)
CPP
#include <iostream>
#include <chrono>
int main() {
auto start = std::chrono::high_resolution_clock::now();
// 測定対象のコード
long sum = 0;
for (int i = 0; i < 100000000; i++) {
sum += i;
}
auto end = std::chrono::high_resolution_clock::now();
auto duration = std::chrono::duration<double>(end - start).count();
std::cout << "実行時間:" << duration << " 秒" << std::endl;
return 0;
}
6. コード最適化の実践
(1) 6.1 コードレベル
| テクニック | 説明 |
|---|---|
| ムーブセマンティクス | コピーを避ける |
| emplace_back | 一時オブジェクトを避ける |
| reserve | vectorの再割り当てを避ける |
| unordered_map | ハッシュ、O(1)検索 |
(2) 6.2 アルゴリズムレベル
| テクニック | 説明 |
|---|---|
| 適切なコンテナ | vector vs list vs map |
| 適切なアルゴリズム | sort vs partial_sort |
| 値渡しを避ける | 参照を使用 |
▶ サンプル 2:Vector Reserve最適化(難易度 ⭐⭐)
CPP
#include <iostream>
#include <vector>
#include <chrono>
int main() {
const int N = 1000000;
// reserveなし
auto start = std::chrono::high_resolution_clock::now();
std::vector<int> v1;
for (int i = 0; i < N; i++) {
v1.push_back(i);
}
auto end = std::chrono::high_resolution_clock::now();
double time1 = std::chrono::duration<double>(end - start).count();
// reserveあり
start = std::chrono::high_resolution_clock::now();
std::vector<int> v2;
v2.reserve(N);
for (int i = 0; i < N; i++) {
v2.push_back(i);
}
end = std::chrono::high_resolution_clock::now();
double time2 = std::chrono::duration<double>(end - start).count();
std::cout << "reserveなし: " << time1 << " 秒" << std::endl;
std::cout << "reserveあり: " << time2 << " 秒" << std::endl;
return 0;
}
出力:
TEXT 📖 参照専用reserveなし: 0.05 秒 reserveあり: 0.02 秒
▶ サンプル 3:参照渡し最適化(難易度 ⭐⭐)
CPP
#include <iostream>
#include <vector>
#include <chrono>
struct BigData {
std::vector<int> data;
BigData(int n) : data(n, 0) {}
};
// 遅い:値渡し(vector全体をコピー)
void processByValue(BigData d) {
d.data[0] = 1;
}
// 高速:参照渡し(コピーなし)
void processByRef(BigData& d) {
d.data[0] = 1;
}
int main() {
BigData big(10000);
auto start = std::chrono::high_resolution_clock::now();
for (int i = 0; i < 10000; i++) {
processByValue(big);
}
auto end = std::chrono::high_resolution_clock::now();
double value_time = std::chrono::duration<double>(end - start).count();
start = std::chrono::high_resolution_clock::now();
for (int i = 0; i < 10000; i++) {
processByRef(big);
}
end = std::chrono::high_resolution_clock::now();
double ref_time = std::chrono::duration<double>(end - start).count();
std::cout << "値渡し: " << value_time << " 秒" << std::endl;
std::cout << "参照渡し: " << ref_time << " 秒" << std::endl;
return 0;
}
出力:
TEXT 📖 参照専用値渡し: 0.8 秒 参照渡し: 0.001 秒
❓ よくある質問
Q:最適化は本当に必要? A: 場合によります。プロファイラで測ってから判断。
Q:C++はPythonより速い? A:一般的にはいえ。アルゴリズムと実装次第。C++の利点は低レベル制御。
Q:どのコンパイラフラグを使う? A:開発中は
-O0、リリースは-O2または-O3。
📖 まとめ
| トピック | 要点 |
|---|---|
| メモリアライメント | パディングを削減 |
| キャッシュ最適化 | 順次アクセス、局所性 |
| コンパイラ最適化 | -O2 を推奨 |
| パフォーマンス測定 | 測ってから最適化 |
| コード最適化 | ムーブセマンティクス、emplace_back |
📝 練習問題
-
初級(難易度 ⭐): vectorとlistで100000個の要素を追加・削除し、パフォーマンスを比較。
-
中級(難易度 ⭐⭐): 値渡しと参照渡しのパフォーマンスを比較。大きな構造体(
vector<int>(10000))を渡す関数を書き、値渡しとconst参照渡しの速度を測定。 -
上級(難易度 ⭐⭐⭐): std::chronoを使って、forループ、STL for_each、Rangesのパフォーマンスを比較測定。
- コンパイラ最適化:-O0/-O1/-O2/-O3/-Os
- 値渡し削減:参照渡し、ムーブセマンティクス
- コンテナ選択:vectorはメモリ連続でキャッシュに優しい
- インライン関数は関数呼び出しのオーバーヘッドを削減
- プロファイラで測ってから最適化
次のレッスン:ユニットテスト(#50)