rust-performance について
Rust のコーディングエージェント向けに、プロファイリングと最適化を扱うスキルです。計測に基づく変更、ベンチマーク比較、割り当てやコレクションの選択、コンパイラー設定のトレードオフを説明します。
主な機能
- 01最適化前のプロファイリング
- 02Criterion と divan によるベンチマーク
- 03メモリ割り当ての再利用パターン
- 04アクセス方法に応じたコレクション選択
- 05ハッシュとコード生成のトレードオフ
活用シーン
- 01Rust の性能ボトルネック特定
- 02ベンチマーク基準との変更比較
- 03ホットパスのメモリ割り当て削減
インストール
コマンドをコピーしてターミナルで実行します。
npx skills add https:/ / github.com/ rewrite-rs/ skills --skill rust-performanceインストール後、Claude Code で/rust-performanceを入力して読み込まれたか確認します。
同梱ファイルagents/openai.yamlALLOCATION.md
詳細
Rust のパフォーマンス
測定によって課題が特定されていない変更は、ここには取り込みません。プロファイルが出発点であり、コード生成フラグは最後の 5%を詰めるためのもので、最初に手をつけるものではありません。非同期処理のスループットについては /async-rust を、速度向上のための unsafe については /unsafe-rust を参照してください。
測定してから変更する
測定によって課題が特定されていない最適化は取り込みません。これを省くと、何も速くならなかった変更と、何の成果もないままコードを悪化させた変更の両方で、二重のコストがかかります。プロファイルを取れば、時間を要している関数を特定できます。それは、コードを読んでいるときに高コストに見えた関数とは、ほとんどの場合異なります。
意味のあるベンチマーク
保存済みのベースラインと統計的に比較するなら criterion、より軽量なハーネスで十分なら divan を使います。セットアップ周辺ではなく、操作そのものをベンチマークしてください。
これがないと、プロファイラーは関数名の代わりにアドレスを表示します。
[profile.bench]
debug = 1
サーマルスロットリングが発生しているノート PC でベンチマークを実行すると、数値は出ますが、比較可能な数値にはなりません。比較する実行の間は、マシンを落ち着かせておく必要があります。
たいていの答えはメモリ割り当てです
最も効果が出やすく、プロファイルで最も頻繁に原因として挙がるカテゴリです。
- サイズがわかっている場合は
Vec::with_capacityを使います。デフォルトでは容量が拡張され、そのたびにコピーが発生します。 - 反復ごとに割り当てるのではなく、クリアして再利用します。バッファーはループの外で用意し、ループ内では
clear()を呼びます。 - 今後増えることのない所有シーケンスには、
Box<[T]>とBox<str>を使います。 - クローンせずに
&mutから値を取り出すには、mem::takeを使います。 - ホットループ内で
format!を使わないでください。呼び出すたびに新しいStringが割り当てられます。 - 値を借用するか所有するかを選ぶ場合は
Cowを使います。値が変更された場合にのみコピーします。
合計サイズがわかっている場合の、結合処理の完全な例です。
fn joined(parts: &[String]) -> String {
let total = parts.iter().map(|p| p.len() + 1).sum();
let mut out = String::with_capacity(total);
for (i, part) in parts.iter().enumerate() {
if i > 0 {
out.push(' ');
}
out.push_str(part);
}
out
}
再利用パターンとバッファーを渡す方法については、ALLOCATION.md を参照してください。
アクセスパターンに合ったコレクションを選ぶ
小さな n では、漸近計算量ではなくキャッシュの挙動により、Vec は HashMap より優れています。反復順序が重要な場合は BTreeMap を使います。一度構築して何度も読み取る場合は、ソート済みの Vec と binary_search を使います。切り替えどころは測定するものであり、定数として暗記するものではありません。
ハッシュ化
デフォルトのハッシャーは DoS 耐性があるぶん、低速です。これを置き換えても安全で有益なのは、キーが攻撃者によって制御されていない場合に限ります。たとえば、内部 ID、列挙型の判別値、インターン化された文字列などです。キーが攻撃者によって制御される場合、これはサービス拒否攻撃の対象となり、検索速度の向上でそのリスクを埋め合わせることはできません。置き換えを無条件の利点として示すのではなく、リスクを明確に説明してください。
イテレーターと境界チェック
イテレーターチェーンは、インデックスを使うループでは隠れてしまう長さの不変条件をオプティマイザーに伝えます。これによって境界チェックが取り除かれます。1 つのループにある 2 つの [i] アクセスを iter().zip() にすると、オプティマイザーはアクセス範囲内であることを証明できます。慣用的な形式が速い形式でもあるのはここだけです。議論すべきトレードオフはありません。
最後の 5%
LTO、codegen-units = 1、PGO、target-cpu。ADR 0007 に従い、これらはアプリケーションで決めるものであり、ライブラリのデフォルトにはしません。それぞれ、得られる利点とともに移植性へのコストを明記します。設定項目は 1 行ですが、プロファイルの取得には半日かかります。
- コミット済みの設定に
target-cpu=nativeを指定すると、古いマシンでは障害が発生するバイナリが生成されます。 - PGO では代表的なワークロードが必要です。そうでないと、誤った経路に対して最適化されます。
- LTO と
codegen-units = 1で得られるのは数パーセントの改善であり、数倍の改善ではありません。その代わり、ビルド時間が長くなります。
panic = "abort" はパフォーマンス向上のためのデフォルト設定ではありません。これはアプリケーションレベルの選択であり、ライブラリはアンワインドを前提にできず、#[should_panic] の実行時の挙動も変わります。
対象外
非同期処理のスループット、エグゼキューターの停滞、spawn_blocking については /async-rust を参照してください。これはスループットではなく、ランタイムの問題です。チェックを省くために unsafe を使う場合は /unsafe-rust を参照してください。そこでの答えは、ベンチマークではなく、そのブロックの健全性を論証することです。プロファイルで見つかったクローンがそもそも必要かどうかについては /ownership-not-clone を参照してください。
検証
cargo bench # against the recorded baseline where one exists
cargo clippy --all-targets # at the level the repo configures; add -- -D warnings only if it configures none
cargo test --release
保存済みのベースラインがないベンチマークでは、変更の効果を証明できません。変更前を記録し、その後を測定して、ベンチマークが報告するノイズと比較してください。Clippy は対象リポジトリで設定されている lint レベルを維持します。-D warnings は、lint がまったく設定されていないリポジトリでのフォールバックです。