RustではじめるCPUとGPU

実践の性能工学

この章でわかること:

  • レイテンシを分布で扱う方法: 平均と中央値と最悪値は異なる量 (実測: 中央値70ナノ秒、最大33ミリ秒)
  • 計測結果を実際より良く見せる協調的省略(coordinated omission)という誤り
  • 性能の回帰を防ぐ運用: ベンチマークのCI化と安定化
  • 本書全体の知識の全体像: 28章に共通する5つの原則
  • なぜ体系に必須か: 知識は運用されて初めて性能になります。 個人の技能を、チームと時間に耐える仕組みへ変換するのが最終章です

レイテンシは分布で扱う

Webアプリケーションの性能は、ほとんどの場合レイテンシ (2章で導入した言葉の、 サービス応答時間への適用)で評価されます。最初に改めるべき習慣は、 平均だけで評価することです。

実験します。HashMapへの100万件の挿入を、1回ずつ個別に 計測して分布を見ます。

100万回のinsertの、1回ごとの時間
use std::collections::HashMap;
use std::time::Instant;

fn main() {
    // HashMapへの100万件のinsertを「1回ずつ」計測する
    let n = 1_000_000u64;
    let mut map: HashMap<u64, u64> = HashMap::new();
    let mut lat_ns: Vec<u64> = Vec::with_capacity(n as usize);

    for i in 0..n {
        let start = Instant::now();
        map.insert(i, i);
        lat_ns.push(start.elapsed().as_nanos() as u64);
    }

    lat_ns.sort_unstable();
    let pick = |p: f64| lat_ns[((n as f64 - 1.0) * p) as usize];
    let mean = lat_ns.iter().sum::<u64>() as f64 / n as f64;
    println!("平均   : {mean:8.0} ns");
    println!("中央値 : {:8} ns", pick(0.50));
    println!("p99    : {:8} ns", pick(0.99));
    println!("p99.9  : {:8} ns", pick(0.999));
    println!("最大   : {:8} ns  ← 外れ値(主因はリハッシュ=全件コピー)", lat_ns[n as usize - 1]);
}

筆者の実測(Playground)です。

平均   :      152 ns
中央値 :       70 ns
p99    :      260 ns
p99.9  :      470 ns
最大   : 33340370 ns  (33.3ミリ秒)

中央値は70ナノ秒ですが、最大値は33ミリ秒で、中央値の47万倍です。 原因は22章で 学んだリハッシュ(表の作り直し)です。償却すれば安い (7章)操作は、分布の裾を厚くします。 平均152ナノ秒という数字は、この最大値の存在を隠しています。

そのため実務ではパーセンタイル(percentile)で評価します。 p50(中央値)、p99(100回に1回の遅さ)、p99.9などを使います。 1回の操作の内部でm個のリクエストが実行されるなら、いずれかが p99以上の遅さになる確率は1−0.99^mです。m=50で約4割、 数百なら大半のユーザーが毎回その遅さを経験します。 ファンアウトの大きな現代のシステムでSLO(サービスレベル目標)が p99系で書かれるのはこのためです。

協調的省略: 計測が遅さを隠す誤り

負荷試験ツールで「リクエストを送り、応答を待って、次を送る」 方式を使うと、サーバが33ミリ秒停止している間、ツールは 次のリクエストを送らずに待ちます。停止期間に 本来発生したはずの数百件の遅い応答が記録から消え、 p99は実際より大幅に良く見えます。これを協調的省略 (coordinated omission)と呼びます。対策は「予定時刻を基準に」 送り続ける負荷生成(一定レートを保つ)と、それを前提に設計された 計測ツール(HdrHistogram系)を使うことです。

ばらつきの源泉: 本書の総復習

「たまに遅い」現象の原因の特定は、本書で学んだ機構の総復習になります。 p99の異常を見たら、この一覧を上から疑ってください。

源泉 章
リハッシュ・Vecの再確保 7章・22章
大きな構造のdrop(解放の連鎖) 18章
アロケータの遅い経路・断片化 18章
ページフォールト(初回タッチ、起動直後) 14章
asyncランタイムのブロッキング混入 19章
ロック競合・false sharing 5章・17章
CPU周波数の変動・サーマルスロットリング 21章・26章
キャッシュ・TLBの内容の消失(コンテキストスイッチ後) 27章

クラウド環境ではさらに2つの源泉が加わります。1つは、多くのインスタンスで vCPUがSMTの論理コア(5章)であり、 物理コア換算で半分の性能しかない場合があることです(インスタンス 仕様の確認が必要です)。もう1つはノイジーネイバー(noisy neighbor)で、 同じ物理ホストの他のテナントとキャッシュやメモリ帯域 (2章)を共有するため、 同じコードの性能が日によって変わることです。 本書のPlayground実測のばらつきも、この現象によるものです。

性能の回帰を防ぐ: ベンチマークの運用

性能は、機能と同じように回帰します。一見無害なリファクタリングが インライン化(6章)を妨げ、依存関係の更新が アロケーションを増やします。対策も機能と同じで、継続的に検査します。

  • ベンチマークをCIに組み込む: criterion(8章)は 基準値との比較(--save-baseline)の機能を持ち、 critcmpで差分を確認できます
  • 時間ではなく命令数で測る: 共有CIランナーの時間計測は ばらつきが大きすぎます(上の表のとおりです)。 iai-callgrindは 実行命令数・キャッシュミス数(8章の カウンタのシミュレーション版)で測るため、共有環境でも安定して 回帰を検出できます。速さではなく処理量の定点観測です
  • 閾値は緩く、傾向は厳しく設定する: 例えば「1回の5%は雑音として扱い、 数回連続の3%は回帰として調べる」といった運用にします (数字は環境のばらつきに合わせて決めます)
  • プロファイルを定期的に確認する: 月に一度フレームグラフ(8章)を 確認するだけで、気づかないうちに増えた処理を早期に発見できます

そして組織の規則として、性能の主張には数字を付けます。 「速くなったはず」というプルリクエストには、8章の手順 (計測→変更→再計測)の出力を添付します。本書が全章で行ってきた 「筆者の実測では」を、チームの習慣にすることです。

知識の全体像

最後に、基礎編・応用編の28章に共通する5つの原則を整理します。 次の図が本書全体の対応関係です(主要な対応のみ示します)。

① 局所性
データは近くに、まとめて

② 並列度と独立性
依存を切れば速くなる

③ 待ちを隠し、境界を減らす
重ねる・バッチ化する

④ 表現がコストを決める
数・レイアウト・構造の選択

⑤ 計測が最終判断
予測は外れうる

2章 キャッシュ / 10章 コアレッシング
12章 ループ順 / 14章 TLB / 15章 ブロッキング

3章 ILP / 4章 SIMD / 5章 マルチコア
9章 SIMT / 17章 メモリモデル / 23章 粗粒度化

3章 パイプライン / 11章 非同期submit
19章 async / 20章 FFI境界 / 24章 重ね合わせ / 27章 syscall

7章 ゼロコスト / 13章 数の表現
18章 アリーナ / 22章 データ構造 / 25章 混合精度

1章 アセンブリ / 6章 コンパイラ / 8章 計測
16章 top-down / 21章 ビルド / 26章 GPU計測 / 28章 分布

  • ① 局所性: キャッシュ、TLB、コアレッシングに関わる原則です。 速いメモリは小さいので、使うデータを近くに置きます。本書で最も多くの倍率 (92倍、70倍、13倍など)を生んだ原則です
  • ② 並列度と独立性: ILPからSIMD、マルチコア、GPUまで、 現代の計算能力はすべて並列性に基づきます。並列性を引き出す条件は 常に、依存を取り除くことでした
  • ③ 待ちを隠し、境界を減らす: パイプライン、async、 GPUの重ね合わせ、syscallのバッチ化が該当します。待ちは消せなくても 重ねられ、境界は越える回数を減らせます
  • ④ 表現がコストを決める: 同じ意味でも、数の形式・メモリの レイアウト・データ構造の選択で性能は桁単位で変わります。 抽象化は無料でも、表現は無料ではありません
  • ⑤ 計測が最終判断: ①〜④の適用が正しかったかは 計測だけで確認できます。本書の実験でも、定石が覆った場面 (23章、21章のPGO)が何度もありました

この5つは、CPUにもGPUにもOSにも、そして今後現れる新しい ハードウェアにも適用できます。個別の数値(キャッシュのサイズ、 ワープの幅)は世代で変わりますが、原則と、それを検証する方法は 変わりません。

まとめ

  • レイテンシは分布で扱います。平均152nsの裏に33msの最大値が ありました(実測)。p99系のパーセンタイルとSLOが実務の指標です
  • 負荷試験では協調的省略に注意します。応答を待ってから次を送る 計測は、最悪の期間を記録から除外してしまいます
  • 「たまに遅い」原因の一覧(リハッシュ、drop、ページフォールト、 ブロッキング混入、周波数、ノイジーネイバー)は本書の各章の索引でもあります
  • 性能はテストと同じく回帰します。ベンチマークのCI化(criterion/ iai-callgrind)と、性能の主張に数字を添える習慣が対策です
  • 全28章は5つの原則(局所性、並列度、待ちと境界、表現、計測)に 整理できます。個別の数値は世代で変わっても、原則は変わりません

読み終えた読者へ

本書のすべての実測は、リポジトリのsrc/snippets/とexamples/で 再現できます。読者自身の環境で数字がどう変わるかを確認することが、 原則⑤の実践であり、この本の最も重要な課題です。 さらに学ぶにはさらに学ぶにはを、 用語の確認には用語集を参照してください。

計測に基づいた設計を続けてください。