perf の深い活用

このページの目次

perf は Linux perf_events のユーザーランドツールであり、イベントの総数をカウントするだけでなく、イベント発生時に呼び出しスタックをサンプリングすることもできます。まず明確にすべきは、答えたい質問です。プログラムはどのくらい CPU を消費しているのか、ホットスポットはどこか、特定の命令が遅い理由は何なのか、それともリクエストは何を待っているのか。異なる問題には、異なるイベントと観察範囲が必要です。

以下のコマンド例では、Linux ユーザーランドプログラム ./prog を対象としています。イベントのサポートは CPU、カーネル、仮想化環境、および perf のバージョンに依存します。まず perf list で実際に利用可能な項目を確認してください。権限も perf_event_paranoid、capability、およびシステムポリシーに依存し、すべての操作で root 権限が必要とは限りません。

カウント:再現可能なベースラインの確立

perf stat -r 5 -e task-clock,context-switches,page-faults -- ./prog
perf stat -r 5 -e '{cycles:u,instructions:u}' -- ./prog
perf list

1行目は CPU 時間、スケジューリング、ページフォルトから始めます。2行目はユーザーランドの cycles と instructions を同じグループに配置し、同じ測定ウィンドウでの比較を容易にします。-r 5 はプログラムを5回実際に実行し、入力、データ状態、ウォームアップ戦略を一致させます。プログラムがデータを変更する場合は、再現可能なテストシナリオを事前に準備してください。

指標答えられる質問直接導き出せないこと
task-clockタスクが累積して使用した CPU 時間壁掛け時計(リアルタイム)とは等しくなく、マルチスレッドでは累積値がより大きくなる場合がある
instructions / cyclesこの負荷の IPCCPU や負荷間で統一された閾値を使って良し悪しを判断できない
branch-misses / branchesカウントされた分岐のミス割合ミス率だけを見ても、総消費時間に占める寄与度を特定できない
page-faults発生したページフォルトの数すべてがディスク I/O ではなく、軽微なページフォルトを含む
context-switchesスケジューリングの切り替え頻度切り替えが多いことが必ずしも主要なボトルネックではない

例えば、アルゴリズムを再実装した結果、命令数が半分になり IPC も低下しましたが、総実行時間は依然として短くなりました。IPC は実行時間と完了した作業量と合わせて解釈すべきです。cycles:u の u はユーザーランドのみを意味し、周波数変化を排除するものではありません。CPU 周波数、SMT、コア移動、ハイブリッドコア、およびバックグラウンドの干渉は比較に影響を与え続けます。

汎用の cache-misses の具体的なマッピングは PMU によって決定されるため、デフォルトで L1 ミス率と見なすべきではありません。特定のキャッシュレイヤについて議論する場合は、その CPU のドキュメントで定義されたイベントを選択し、分母を確認してください。

1万件の作業を完了しても、IPC が低下すれば遅くなるわけではない

以下は仮データであり、カウントの解釈を練習するためのものです。特定のマシンの測定結果ではありません。A と B はどちらも 10,000 件の同じ作業を完了し、イベント範囲と実行条件は一致しています。

指標AB解釈
壁掛け時計の消費時間0.50 s0.40 sB の消費時間は 20% 減少
instructions2.0 × 10⁹1.2 × 10⁹作業あたりの命令数が 200,000 から 120,000 に減少
cycles1.0 × 10⁹0.8 × 10⁹ユーザーランドイベントカウントの減少
IPC2.01.5B の IPC は低いものの、総作業コストは節約されている

この場合、正しい結論は B が同じ作業をより速く完了したということです。IPC が 2 から 1.5 に低下したからといって、後退と判断してはいけません。もし B が 8,000 件のみ処理し、検証をスキップしたりエラーを無視したりしていた場合、消費時間の減少を最適化の利益として直接見なすことはできません。

from math import isclose
jobs = 10_000
a = {"seconds": 0.5, "instructions": 2_000_000_000, "cycles": 1_000_000_000}
b = {"seconds": 0.4, "instructions": 1_200_000_000, "cycles": 800_000_000}
assert a["instructions"] / a["cycles"] == 2
assert b["instructions"] / b["cycles"] == 1.5
assert a["instructions"] / jobs == 200_000
assert b["instructions"] / jobs == 120_000
assert isclose(1 - b["seconds"] / a["seconds"], 0.2)
assert isclose(jobs / b["seconds"] / (jobs / a["seconds"]), 1.25)

消費時間が 20% 減少することは、分母が異なるためスループットが 25% 向上することを意味します。task-clock も正しい分母で使用する必要があります。2秒の壁掛け時計の間に 3.2秒の累積 CPU 時間が計測された場合、平均して約 1.6個の CPU の実行時間を使用していることになります。「CPU 利用率が 100% を超えているのでデータが壊れている」わけではありません。この平均値は、ある瞬間のピークや、各コアでの均一性を表すものではありません。

カウンタの再利用:数値はスケーリングされている可能性がある

ハードウェアで同時に利用可能なカウンタは限られており、イベント数が利用可能なリソースを超えると、リソースが共有されることがあります。perf は実際の実行時間に基づいて結果をスケーリングできますが、負荷が段階的に強い場合、特定の段階を短期間観測して全体を推測すると、誤差が生じる可能性があります。

出力結果における実行比率、not counted(カウントされず)、または not supported(サポートされず)に注目してください。イベント数を減らし、関連する比率のイベントをグループ化し、実験を繰り返すことで、結果が安定しているかどうかを判断するのに役立ちます。グループ化もリソース制約により全体としてスケジューリングできない場合があります。perf stat のスケーリングとイベントグループのセマンティクスについては マニュアル を参照してください。

スケーリング補正は時間をカバーするが、観察されなかった段階を復元できない

イベントが 10秒間有効化され、ハードウェアカウンタによって実際にスケジューリングされた時間が 2.5秒、原始カウントが 200 であると仮定します。時間の比率でスケーリングすると 200 × 10 / 2.5 = 800 となります。これは、サンプリングされた期間がサンプリングされていない期間を代表するという仮定を含みます。もしちょうど初期化のピークのみをカバーし、その後の大部分の時間が別の作業フェーズにある場合、800 は実際の総量から大きく外れる可能性があります。

関連する比率は、同じイベントグループと互換性のある範囲から取得するのが最も良いですが、イベントグループは無料の保証ではありません。ハードウェアリソースが不足している場合、グループ全体が実行できないこともあります。not supported に遭遇した場合は、まずイベントと PMU を確認し、not counted に遭遇した場合は、スケジューリングリソースと実際の稼働時間を確認してください。これらの状態を数値のゼロとして除算に使用しないでください。perf stat のスケーリングとグループ化

時間で拡大しても、なぜ誤算が生じる可能性があるのか?

完全な棒グラフはモデルが提供する既知のイベント率であり、実際の測定ではサンプリングされていない区間は通常観察できません。デフォルトでは最初の 2秒のみをカウントし、原始の 200 が 1000 に拡大され、実際の 280 を大幅に超えます。逆に、後の低率の区間に移動すると、過小評価される可能性があります。running=0 の場合、ゼロ除算したり、ゼロイベントと解釈したりしてはいけません。有効なカウントがない場合は報告すべきです。

図を準備しています
時間で拡大しても、なぜ誤算が生じる可能性があるのか?

enabled/running によるスケーリングは、カバーされた比率のみを補正し、観察されなかった段階のイベント率を再構築することはできません。ウィンドウがちょうどピークまたはボトムをカバーする場合、推定値は総量から外れる可能性があります。

サンプリング:イベント発生時の実行位置の特定

perf record -o perf.data -F 99 -e cycles:u --call-graph dwarf -- ./prog
perf report -i perf.data --stdio
perf annotate -i perf.data --symbol=my_function

この一連のコマンドは、ユーザーランドのサイクルイベントを選択し、目標周波数 99 Hz でサンプリングしてユーザースタック情報を収集します。実際のサンプリング周波数は、カーネルによるスロットリングやワークロードの影響を受ける可能性があります。特定のバージョンのデフォルトイベントやデフォルト周波数に依存しないでください。

-F は目標サンプリング周波数を指し、-c はイベントサンプリング周期を指し、両者の意味は異なります。cycles サンプルは、CPU がアクティブな状態でのイベントの分布を示します。ブロック期間中、スレッドは実行されていないため、通常の on-CPU スタックは、待機時間の全体像を直接教えてくれるわけではありません。ロック待機や I/O 遅延を解釈するには、スケジューリングイベントやエントリ/リターントレースと組み合わせる必要があります。

スタックの取得方法にもトレードオフがあります。frame pointer 方式は通常、関連するコードがフレームポインタを保持していることを要求します。DWARF 方式は展開のためにユーザースタックを記録し、対応する展開情報が必要であり、サンプリングスタックのサイズはデータ量と切り捨てに影響します。LBR はハードウェアのサポートに依存します。-g は、任意のバイナリで完全な呼び出しチェーンが得られることを保証するスイッチではありません。perf record

シンボル名、ソースコード行、および呼び出しチェーンは異なる情報です。ビルドされたバイナリ、build-id、およびデバッグシンボルを一致させて保持し、別のコンパイルで生成されたファイルを使って古い perf.data を解釈しないでください。インライン展開、末尾呼び出しの最適化、およびコンパイラの最適化は、ソースコードとの対応関係に影響を与えます。リンクとロード は、これらの生成物の関係について解説しています。

フレイムグラフ:呼び出しスタックの集約

CPU フレイムグラフの例:幅はサンプル重みであり、タイムラインではない main:100% handle_request:60% background:40% parse:40% store:20% 横方向の配置は類似したスタックを集約するために使用されます。縦方向の上は、呼び出し先を示します 親ボックスは子呼び出しのサンプルを含みます。最上位の広いボックスは最適化候補ですが、最適化後もエンドツーエンドの利益を測定する必要があります。

標準的な集約フレイムグラフにおいて、左右の位置には時間の先后の意味はありません。幅は、その呼び出しスタックに帰属されたサンプル量または重みを表します。色は通常、ボックスを区別するためのものであり、温度や消費時間のレベルを自然に表すものではありません。広いボックスの上を見れば深いホットスポットが見つかりますが、ホットスポットは上位の関数が繰り返し呼び出した結果である可能性もあります。Brendan Gregg:Flame Graphs

FlameGraph スクリプトが PATH に存在する場合:

perf script -i perf.data > out.perf
stackcollapse-perf.pl out.perf > out.folded
flamegraph.pl out.folded > flamegraph.svg

これらのスクリプトは FlameGraph プロジェクト に属しており、perf に同梱されているサブコマンドではありません。サンプルの欠落、シンボルの欠如、またはスタック展開の断絶が発生した場合は、まず入力の品質を修正し、生成されたグラフがきれいに見えるかどうかだけを見てはいけません。カウントサンプルの比率と、イベント周期で加重された比率は、レポート設定に応じて区別する必要があります。

self と children は異なる帰属であり、重複して加算してはならない

あるサンプルスタックが main → handle_request → parse であるとします。self 帰属の場合、これは parse の現在の実行位置に属します。children または呼び出しチェーンの累積帰属の場合、handle_request と main もこのサンプルを含みます。したがって、親と子の行のパーセンテージを加算すると 100% を超えることがあり、これは perf の計算が間違っていることを意味するものではありません。

children 累積を有効にした場合と無効にした場合のレポートをそれぞれ確認し、使用されているパーセンテージの基準を記録してください。非常に広い親ボックスは、自分自身のコードをほとんど実行していない可能性があります。それは単に、すべてのホットスポットの共通呼び出し元であるだけです。この場合、親関数のエントリで1行変更しても、子呼び出しのコストが自然に解消されるわけではありません。perf report の帰属

サンプリングが不足している場合は、まず有効な観察量を増やす

99 Hz で、約 20 ms しか実行しないシングルスレッドの CPU 集中フェーズの場合、理想的なサンプル数は約2回程度であり、実際にはイベントとスケジューリングの影響も受けます。この場合、「ある関数が 50% を占める」のは、実際には1つのサンプルに過ぎず、据此に正確なランキングを行う価値はありません。

同じワークロードを1つのプロセス内で十分に繰り返すことで、初期化と定常状態を別々に処理できます。サンプル数を増やすために、入力規模を全く異なるアルゴリズムパスに変更しないでください。周波数を増やすことにもコストがあり、サンプリングデータの拡大、イベントの欠落、または観察対象プログラムの挙動変化を引き起こす可能性があります。まず有効なサンプル数、欠落のヒント、およびスタックの品質を確認し、その後で周波数の向上を検討してください。

親ボックスが非常に広い場合、親関数自体が遅いのか?

デフォルトで handle の幅が 90% を占めていますが、self は 0 です。これらのサンプルの葉はすべて parse または send にあるためです。main の 100%、handle の 90%、および parse の 60% には重複する帰属が含まれており、加算して総消費時間として扱うことはできません。特定の子ボックスを狭めると、親ボックスはサンプル重みとともに変化し、横方向の配置は関数が順次実行されることを示すものではありません。

図を準備しています
親ボックスが非常に広い場合、親関数自体が遅いのか?

フレイムグラフの幅はサンプル重みを表し、親ボックスは子呼び出しを含みます。同じサンプルは複数の祖先に帰属できますが、self はサンプリングされた葉にのみ属します。横軸は時間ではありません。

正確なサンプリング:位置のずれの削減

通常のサンプリングでは、イベントのオーバーフローから命令位置の記録までには、skid(ずれ)が発生する可能性があります。ホットスポットとしてマークされた命令が、必ずしもイベントをトリガーした命令であるとは限りません。正確なサンプリング機能は、特定の PMU やイベントバインディングに依存します。Intel PEBS と AMD IBS は異なるハードウェアメカニズムです。

perf の p 修飾子は、サンプリング IP の精度に対する要件を示します。pp はゼロ skid を要求し、ppp の厳格なサポートはイベントとハードウェアによって決定されます。cycles:ppp を「すべてのマシンで適用可能な最高精度モード」として記述してはいけません。サポートされている正確なイベントをまず照会し、そのイベント定義に基づいて結果を解釈してください。perf list のイベント修飾子

正確な IP であっても、その命令が根本原因であると証明されたわけではありません。キャッシュ階層、データソース、依存チェーン、または上位の制御フローなど、さらに観察が必要な要素が残っています。Top-down 指標は、マイクロアーキテクチャに基づいて実行スロットなどのリソースを分類しますが、サポート範囲と指標の定義は、本机の PMU に基づいて確認する必要があり、「特定の世代以降はすべて同じである」と固定して記述してはいけません。

ダイナミックプロービングと観察範囲

perf probe -x ./prog --add 'work=my_function'
perf probe --list

ツールは実際のイベント名を報告します。その後、その名前を perf record -e に渡します。コマンドのシンボル解析には、対象にプローブ可能な位置が実際に存在する必要があります。ソースコード行とローカル変数の解析には、DWARF 情報が必要になる場合もあります。インライン展開、最適化で削除された変数、または1つのシンボルが複数の位置に対応する場合、「任意の関数、任意の行でプローブできる」と単純化することはできません。

記録パラメータは、対象関数の ABI とプローブ位置を確認する必要があります。x86-64 のレジスタの例をそのまま arm64 に適用してはいけません。終了時は、共有トレース機構をクリアするのではなく、実際のイベントの完全な名前を使って、今回作成した probe を削除してください。perf probe マニュアル

プロセスへのアタッチには -p PID を、スレッドには -t TID を使用できます。システム全体の短時間収集には perf record -a ... -- sleep 10 を使用できますが、他のワークロードも観察されます。CPU、プロセス、ユーザーランド、またはカーネルランドを選択する際は、結論が実際の範囲と一致するようにしてください。

CPU ホットスポットの比率を、リクエストレイテンシの比率として直接見なしてはならない 単一リクエストの直列実行を仮定:CPU 20 ms + 待機 100 ms CPU 20 待機 100 ms parse が CPU の 40% を占める:8 ms。最適化して 4 ms にすると、4 ms 節約 CPU 16 待機は依然として 100 ms 総レイテンシは 120 → 116 ms に減少し、約 3.3% の削減。これは、図に示すように重なりがなく、他の要因が変化しないという仮定にのみ適用されます。

リクエスト時間の勘定で期待される利益を制約する

CPU サンプルが時間的に代表性を持っていると仮定すると、parse は 20 ms の CPU 作業の 40%、つまり 8 ms を占めます。これを2倍に高速化しても、節約できるのは 4 ms に過ぎません。リクエストに 100 ms の直列待機がある場合、全体は 120 ms から 116 ms に減少し、「ホットスポットが2倍に高速化されたので、インターフェースも2倍に高速化した」わけではありません。

from math import isclose
cpu_ms, wait_ms = 20, 100
hot_ms = cpu_ms * 0.4
before = cpu_ms + wait_ms
after = before - hot_ms + hot_ms / 2
assert (before, after) == (120, 116)
assert isclose(1 - after / before, 1 / 30)

マルチスレッドパイプライン、非同期オーバーラップ、およびキューイングシステムでは、各スレッドの CPU 時間を単一リクエストのレイテンシに単純に加算することはできません。高負荷下では、CPU サービス時間の削減がキューイングの減少につながる可能性がありますが、低負荷下ではレイテンシの変化がほとんど見られない可能性があります。負荷レベル、完了スループット、およびレイテンシ分布を同時に記録し、単一リクエストモデルをすべてのシナリオに外挿しないようにしてください。

検証可能な最適化結論の形成

まず入力と完了した作業量を固定し、カウントし、その後サンプリングで候補を特定します。説明可能なメカニズムを提案して修正し、最後に同じエンドツーエンドの指標に戻って検証します。Amdahl の法則は、総消費時間の 10% を占める部分を完全に除去しても、元の消費時間を 10% しか削減できないことを警告しています。

ホットスポットの比率が低下したが、リクエスト時間が変化しない場合、ボトルネックが移動したか、負荷が変化したか、または元の指標が CPU の一部しかカバーしていない可能性があります。コマンド、マシン情報、シンボルバージョン、およびベースラインを保持することで、「速く見える」ことを検証可能な結論にすることができます。

キーワード:perf, perf_events, PMU, IPC, multiplexing, call graph, flame graph, PEBS, IBS, skid, sampling

結論ごとに最小限のエビデンスパッケージを保持する

ソースコードまたはビルド識別子、バイナリの build-id、完全なコマンド、イベント修飾子、入力規模、完了数、実行回数、および測定環境を保持してください。サンプリングについては、スタックの取得方法、データファイル、および欠落/スロットリング状況を別途記録します。カウントについては、イベントの実行比率とサポートされていない項目を別途記録します。

新しいバージョンがベースラインより 1% 速いだけであり、繰り返し実行のばらつきがこの値よりも大きい場合は、小さな差異を修正に帰因する急がず、まず実験を改善するか、代表性のある繰り返し回数を増やしてください。A/B を交互に実行して時間によるドリフトの影響を軽減し、コールドスタートと定常状態を別々に報告します。最終的な結論は、「特定の負荷下でどのくらい時間またはリソースを削減したか」に落とし込むべきであり、perf の指標はメカニズムを説明するために使用します。