perfとハードウェアパフォーマンスカウンタ
推測でなく実測でボトルネックを特定できる。CPU内蔵のカウンタでキャッシュミスや分岐予測ミスを数え、低オーバーヘッドで原因に迫る仕組みを原理から解説します。
- 1.PMUはCPUに内蔵された専用ハードウェアで、キャッシュミスや分岐予測ミスなどのイベントを、ソフトの介入なしにレジスタで数える。プログラムを止めずに計測できる。
- 2.計測方式はカウント(区間の合計値)とサンプリング(N回ごとに割り込んで状態を採取)の2つ。PEBSはハードがサンプルを直接メモリへ書き、スキューの少ない正確なプロファイルを取る。
- 3.perf_eventサブシステムがPMUを抽象化し、多重化・スケーリング・仮想カウンタを提供する。観測自体が割り込みやキャッシュ汚染というオーバーヘッドを生む点が原理的な限界。
「推測でなく実測する」ためのハードウェア
性能チューニングで最も危険なのは、ボトルネックを勘で当てに行くことです。「ここが遅いはず」という思い込みは外れることが多く、実際のCPUは キャッシュ階層 や分岐予測、パイプラインといった見えにくい要因に律速されています。これらは普通のプログラムからは観測できません。
そこで現代のCPUは、自分自身の挙動を数える専用ハードウェアを内蔵しています。それが PMU(Performance Monitoring Unit) であり、Linux でそれを使う標準ツールが perf です。核心は「ソフトウェアの計測コードを挟まずに、ハードが直接イベントを数える」点にあります。命令を1つ実行するたびにカウンタを増やすコードを書けば、その計測コード自体が支配的なオーバーヘッドになります。PMU はそれをゲートレベルで行うため、本来の実行をほぼ妨げません。
PMUとパフォーマンスカウンタの構造
PMU の実体は、CPU コアごとに置かれた少数の PMC(Performance Monitoring Counter) レジスタ群です。各カウンタには「何を数えるか」を指定する イベントセレクタ(モデル固有レジスタ、MSR)が紐付き、そこにイベント番号・ユニットマスク・特権レベル(ユーザー/カーネルどちらで数えるか)などを設定します。設定後はハードがそのイベントの発生回数を黙々と加算します。
イベントは大きく2種類あります。
| 種別 | 実体 | 代表例 | 性質 |
|---|---|---|---|
| 固定機能カウンタ | 数えるイベントが固定 | 実行命令数・コアサイクル数・参照TSC | 常に利用可・設定不要 |
| 汎用カウンタ | 任意のイベントを割当可 | L1/LLCキャッシュミス・分岐予測ミス・ストール | 本数が少なく取り合いになる |
ここで効いてくる物理的な制約が カウンタの本数 です。汎用カウンタは1コアあたり数本(世代によりおおむね 4〜8 本)しかありません。固定機能カウンタはサイクル数や命令数といった頻出イベント専用に用意され、汎用カウンタを消費しません。なぜこの区別が重要かというと、IPC(命令数 ÷ サイクル数)のような基礎指標を、限られた汎用カウンタを使わずに常時取れるからです。
perf で使うイベント名には、cache-misses や branch-misses のような 汎用イベント と、mem_load_retired.l3_miss のような 生イベント(raw event) の2層があります。前者はカーネルがマイクロアーキテクチャの差を吸収した抽象名で、後者は特定CPUのMSRに直接対応する名前です。同じ「キャッシュミス」でも、どのレベルのどの条件を数えるかは世代で異なるため、厳密な解析では生イベントを指定します。
カウントとサンプリング:2つの計測モード
PMU の使い方には原理的に異なる2つのモードがあります。この違いを理解することが perf 習得の山です。
カウントモード(counting) は、ある区間でイベントが合計何回起きたかを集計します。perf stat がこれにあたります。区間の開始時にカウンタをゼロにし、終了時に値を読むだけなので、オーバーヘッドが極めて小さく、IPC やミス率といった全体傾向を測るのに向きます。ただし「どこで」起きたかは分かりません。
perf stat -e cycles,instructions,cache-misses,branch-misses ./app
12,345,678,901 cycles
9,876,543,210 instructions # 0.80 insn per cycle
123,456,789 cache-misses
23,456,789 branch-misses # 3.21% of all branches
サンプリングモード(sampling) は、イベントを N 回数えるごとに1回だけ割り込みを上げ、その瞬間のプログラム状態(命令ポインタ、レジスタ、コールスタック)を記録します。perf record がこれです。これにより「キャッシュミスがどの関数・どの命令に集中しているか」という空間的な分布が得られます。
サンプリングの周期は2通りで指定します。period(イベント N 回ごと)を固定する方式と、freq(1秒あたりの目標サンプル数)を指定し、カーネルが周期を自動調整する方式です。後者は負荷の異なる区間でもサンプル数を一定に保てます。
イベント N 回ごとに割り込む方式では、N と相関する周期的な挙動を取りこぼす危険があります。たとえばループの特定の反復だけが常にサンプル点と一致(または常に外れ)すると、分布が歪みます。これを避けるため perf は周期にランダムな揺らぎ(dithering)を加えます。サンプリングは確率的な推定であり、サンプル数が少ない関数の数値は誤差が大きいことを忘れてはいけません。
PEBS:割り込みスキューを潰す仕組み
サンプリングには厄介な根本問題があります。**スキュー(skew)**です。カウンタがオーバーフローしてから、CPU が割り込みを受け付けて状態を記録するまでには遅延があり、その間にパイプラインは次々と命令を実行します。結果、記録される命令ポインタは「実際にイベントを起こした命令」より数命令〜数十命令ずれます。これを skid と呼びます。アウトオブオーダ実行や深いパイプラインほど skid は大きくなります。
これを解決するのが PEBS(Precise Event-Based Sampling) です(AMD では IBS が近い役割を担います)。発想はこうです。割り込みでソフトに状態採取を任せるのではなく、ハードウェアがイベント発生時点の正確なレジスタ状態を、あらかじめ確保したメモリ領域(PEBSバッファ)へ直接書き込む。CPU はバッファが閾値に達したときだけ割り込みを上げ、まとめて回収します。
[従来サンプリング] オーバーフロー → 割り込み → ハンドラが状態採取
↑ この間に命令が進む = skid
[PEBS] イベント発生 → ハードが正確な状態をバッファへ直書き
バッファ満杯時のみ割り込み(回収専用)
PEBS の利点は2つです。第一に、記録地点がイベント発生命令に正確に対応する(precise)こと。perf の :p / :pp / :ppp という精度サフィックスはこの PEBS の利用度を表します。第二に、割り込み頻度が下がり、オーバーヘッドが減ること。さらに PEBS レコードにはデータアドレスやレイテンシなどの付加情報を載せられ、perf mem や perf c2c(キャッシュライン競合の検出)といった高度な解析を支えます。
perf record -e cycles:pp のように付ける :p は precise レベルの要求です。:p で skid を「定数」に抑え、:pp で「ゼロ要求」、:ppp で「タイブレークも含め最大精度」を意味します。指定した精度をハードが満たせなければ、そのイベントは使えないかフォールバックします。原理を知らずに数字だけ見ると、skid のずれを実コードの問題と誤読しがちです。
perf_eventサブシステム:ハードを抽象化する
ユーザーが直接 MSR を叩くわけにはいきません。MSR 操作は特権命令であり、コア間の整合や カーネルとユーザーの境界 の管理が必要だからです。この橋渡しをするのが Linux の perf_event サブシステム で、入口は perf_event_open(2) という システムコール 1本です。
このサブシステムが提供する重要な抽象は次のとおりです。
- イベントの仮想化:プロセスやスレッドに紐付いた「仮想カウンタ」を作る。コンテキストスイッチ のたびにカーネルがカウンタ値を退避・復元するため、対象タスクが他コアへ移っても、そのタスクだけの値を追える。
- 多重化(multiplexing)とスケーリング:要求したイベント数が物理カウンタ本数を超えると、カーネルは時分割でイベントを入れ替えて測る。ただし各イベントは一部の時間しか動かないため、
enabled時間とrunning時間の比で値を**外挿(スケーリング)**する。 - リングバッファ:サンプルや PEBS レコードを、
mmapされた共有リングバッファ経由でユーザー空間へ低コストで渡す。
測定値 × (enabled / running) というスケーリングは、その期間イベント発生率が一定だと仮定した外挿です。短時間で挙動が激しく変わるワークロードでは、たまたま動いていた時間帯の率で全体を引き伸ばすことになり、大きく外れ得ます。一度に張るイベントは物理カウンタ本数以内に収める、あるいはイベントを複数回に分けて測るのが安全です。
観測がもたらすオーバーヘッド
「PMU は本来の実行を妨げない」と述べましたが、これは厳密には正しくありません。観測は必ず対象を乱す(観測者効果)。原理から見て、オーバーヘッドの源は次の3つです。
- 割り込みコスト:サンプリングでは周期ごとに割り込みが入り、ハンドラがスタックを巻き戻し(コールグラフ採取)、レコードをバッファへ書く。周期
Nを小さくするほど精度は上がるが割り込み頻度も上がり、計測対象を遅くする。 - キャッシュ・TLB汚染:ハンドラやコールスタック走査が、対象プログラムのデータを キャッシュ から追い出す。皮肉にも、キャッシュミスを測る行為自体がキャッシュミスを増やす。
- コンテキストスイッチ時の退避コスト:仮想カウンタは切り替えのたびに save/restore が走る。多数のイベントを張るほどこの定数コストが積み上がる。
カウントモードのオーバーヘッドは無視できるほど小さい一方、高頻度サンプリングや深いコールグラフ採取は数%以上の歪みを生み得ます。測りたい現象を、測る行為が壊さないかを常に意識する必要があります。
押さえる軸は3つ。(1) カウント vs サンプリング——前者は区間合計で全体傾向、後者はN回ごとの割り込みで空間分布。(2) skid と PEBS——OoO実行で割り込み地点がずれる問題を、ハードがイベント時点の状態を直接メモリへ書くことで解決する。(3) 多重化とスケーリング——物理カウンタ不足を時分割で補い enabled/running 比で外挿するが、率が一定でないと歪む。「観測自体がオーバーヘッドを生む」という観測者効果も頻出です。
まとめ
perf は CPU 内蔵の PMU を Linux の perf_event サブシステム経由で操り、キャッシュミスや分岐予測ミスといったイベントを実測します。計測は カウント(区間合計) と サンプリング(N回ごとに割り込んで状態採取) の2モード。サンプリングの skid 問題は PEBS がハードによる直接書き込みで潰し、precise なプロファイルを実現します。汎用カウンタは本数が少ないため、超過分は 多重化 で時分割し enabled/running 比で外挿しますが、これは推定であり歪み得ます。そして観測は割り込み・キャッシュ汚染・退避コストという不可避のオーバーヘッドを伴う——勘を捨て実測へ向かう強力な手段であると同時に、その数字がどう作られたかを知って初めて正しく読めます。
OS Article
perfとハードウェアパフォーマンスカウンタを実務で読む
TL;DRは入口です。実際に選ぶ・使う段階では、何を解決するか、何と比較するか、導入後にどこで詰まるかまで見る必要があります。
解決すること
perf
比較で見る軸
難易度: advanced / カテゴリ: OS / タグ数: 6
導入後に効く点
計測方式はカウント(区間の合計値)とサンプリング(N回ごとに割り込んで状態を採取)の2つ。PEBSはハードがサンプルを直接メモリへ書き、スキューの少ない正確なプロファイルを取る。
先に潰すリスク
用語だけ覚えても、設計・実装・運用でどこに効くかを確認しないと判断を誤る。
- 難易度
- advanced
- カテゴリ
- OS
- タグ数
- 6
判断チェックリスト
- 自社の用途が「perf / PMU」に近いか確認する。
- 強みである「PMUはCPUに内蔵された専用ハードウェアで、キャッシュミスや分岐予測ミスなどのイベントを、ソフトの介入なしにレジスタで数える。プログラムを止めずに計測できる。」が本当に評価軸になるか確認する。
- 注意点の「用語だけ覚えても、設計・実装・運用でどこに効くかを確認しないと判断を誤る。」を運用で吸収できるか確認する。
- 公開値や仕様値は、対象プラン・対象機種・対象リージョンまで確認する。
- 既存システム、ID、ネットワーク、監視、バックアップとの接続方法を先に洗い出す。
- 小さく試してから、本番移行、権限設計、障害時手順、コスト監視を決める。