Rustのパフォーマンス最適化とプロファイリング
Rustのパフォーマンス最適化をマスターする:LTOによるリリース設定、FlamegraphによるCPUプロファイリング、ヒープ削減、SIMD。
第3部:上級システム開発と実践Rust の第3回(最終回)。Rustはゼロコスト抽象化を基本理念としていますが、最高のスループットと低レイテンシを実現するためには、適切なコンパイラ設定、メモリレイアウトの最適化、ボトルネックの正確なプロファイリングが不可欠です。
本稿では、Rustアプリケーションを体系的に測定・プロファイリング・高速化する手法を解説します。
graph TD
A["Rust最適化ワークフロー"] --> B["1. Cargoリリース設定とLTO"]
A --> C["2. 測定とプロファイリング (Flamegraph, Criterion)"]
A --> D["3. メモリレイアウトの改善 (ヒープ確保の削減)"]
A --> E["4. 低レベル最適化 (SIMD・キャッシュ局所性)"]
10歳でもわかる説明(ELI5):F1レーシングカー#
- 通常のコード:高性能なF1マシンを作ったのに、冬用タイヤを履いてトランクに重い荷物を積んだまま走っている状態(Debugビルドや過剰なヒープ確保)。
- プロファイリング(Flamegraph):全輪にセンサーを取り付け、どのコーナーで速度が落ちているか正確に特定する作業。
- 最適化(LTO、SIMD、Arena):不要な重量を削ぎ落とし、空力性能を高め、ターボ(SIMD)を作動させてサーキットを最高速度で駆け抜けること!
1. Cargoリリースプロファイルの最適化#
初心者に最もありがちなミスは、最適化が無効化された cargo run(Debugモード)でベンチマークを行ってしまうことです。
Cargo.toml で高度なコンパイラ最適化を有効にします:
Cargo.toml
[profile.release]
opt-level = 3 # 最大限の最適化
lto = "fat" # クレート全体をまたぐ完全なリンク時最適化 (LTO)
codegen-units = 1 # コード生成ユニットを1にし、インライン化効率を最大化
panic = "abort" # スタック巻き戻しテーブルを排除しバイナリを軽量化
strip = true # リリースバイナリからデバッグシンボルを削除toml2. CriterionとFlamegraphによる正確な測定#
推測で最適化してはいけません。実データに基づいて改善しましょう!
Criterionによる統計的ベンチマーク#
criterion クレートを使うことで、統計的に正確なマイクロベンチマークを実行できます:
benches/my_benchmark.rs
use criterion::{black_box, criterion_group, criterion_main, Criterion};
fn fibonacci(n: u64) -> u64 {
match n {
0 => 0,
1 => 1,
n => fibonacci(n - 1) + fibonacci(n - 2),
}
}
fn criterion_benchmark(c: &mut Criterion) {
c.bench_function("fib 20", |b| b.iter(|| fibonacci(black_box(20))));
}
criterion_group!(benches, criterion_benchmark);
criterion_main!(benches);rustcargo flamegraph によるCPUボトルネックの可視化#
CPU時間を最も消費している関数呼び出しをヒートマップで確認します:
cargo install flamegraph
cargo flamegraph --bin my_serverbash3. ヒープ割り当ての最小化#
ヒープの動的確保(malloc/free)は、スタック操作や連続した配列走査に比べて大幅に時間を要します。
SmallVec や ArrayVec によるスタック内保持#
要素数が少ないコレクションの場合:
src/main.rs
use smallvec::{smallvec, SmallVec};
// 最大8要素まではスタック上にインライン保持し、8個を超えた場合のみヒープへ退避
let mut v: SmallVec<[i32; 8]> = smallvec![1, 2, 3];rustCow (Clone-on-Write) の活用#
文字列の変更が必要な場合のみメモリ確保を行うスマートポインタです:
src/main.rs
use std::borrow::Cow;
fn sanitize(input: &str) -> Cow<str> {
if input.contains('<') {
Cow::Owned(input.replace('<', "<"))
} else {
Cow::Borrowed(input) // メモリ確保なし!
}
}rust4. SIMDによるハードウェア並列化#
1回のCPUクロックサイクルで複数の数値を並列処理します:
src/main.rs
pub fn add_arrays(a: &[f32; 4], b: &[f32; 4]) -> [f32; 4] {
// opt-level=3 でLLVMコンパイラが自動ベクトル化 (Auto-vectorization) を適用
let mut out = [0.0; 4];
for i in 0..4 {
out[i] = a[i] + b[i];
}
out
}rustまとめ#
- 本番ビルドでは必ず
cargo build --releaseとlto = "fat"を設定します。 criterionとcargo flamegraphでボトルネックを特定してからリファクタリングします。CowやSmallVecを活用し、ヒープ確保の回数を劇的に減らします。- CPUキャッシュに優しいメモリ配置とSIMDベクトル化を意識します。