ハードウェア上級 #1 CPU マイクロアーキテクチャと perf — 同じ 100% が違う理由

読了 7分

ハードウェア中級 で 4 つのリソースの指標を読み、ボトルネックを切り分ける診断法をつかみました。上級シリーズはその診断を両方向に広げます。基礎が概念、中級が診断だったとすれば、上級は下へはカーネルとシリコンレベルの観測に降りていき、上へはサーバーが集まるデータセンターへ上がっていきます。

シリーズは全 7 編です。CPU マイクロアーキテクチャと perf (1 編)、eBPF 観測 (2 編)、ページキャッシュ・hugepages・メモリ帯域幅 (3 編)、ZFS 深掘り (4 編) までがカーネルの下側で、データセンターの電力 (5 編)、冷却とラック (6 編)、ファームウェア・BMC とサーバーライフサイクル (7 編) がデータセンターの上側です。

第 1 編の問いはこれです。使用率 100% の 2 台のサーバーで、2 つの CPU は同じ量の仕事をしているでしょうか。答えは「そうとは限らない」で、その差を数字で見せてくれるツールが perf です。perf を含む診断ツールの全体地図は RHEL 上級 #3 が扱うので、この記事はツールの使い方ではなく その出力に並んだ数字をマイクロアーキテクチャとして解釈する方法 に集中します。

使用率 100% の二つの顔 — IPC #

OS が見る CPU 使用率は「コアにタスクが載っていた時間の割合」です。コアがその時間に実際に命令を処理していたのか、それともメモリからデータが届くのを待って空回りしていたのかは区別しません。メモリを待っている間もタスクはコアを占有しているので、使用率にはどちらも 100% と表示されます。

この差をあらわにする指標が IPC (instructions per cycle、クロックサイクルあたりに処理した命令数) です。現代のサーバー CPU はサイクルあたり 4 個以上の命令を処理する能力があるので、おおよそこういう感覚で読みます。

  • IPC が 1.0 を大きく下回るなら、コアが仕事より待ちにサイクルを使っているシグナルです。たいていメモリアクセスが原因です。
  • IPC が 1.0 を大きく上回るなら、コアが演算で埋まっているシグナルです。このときの 100% は本物の演算の飽和です。

同じ 100% でも IPC 0.5 と 2.0 では、コアがこなした仕事の量に 4 倍の差があります。処方も分かれます。前者はコアを増やしても待ちが増えるだけで、後者はコア増設やアルゴリズム改善が正攻法です。

パイプラインとスーパースカラー — コアの中の組み立てライン #

IPC がなぜ揺れるのかを見るには、コアの内部を一層だけ開けてみれば足ります。コアは命令を 1 つ終えてから次を始める方式ではなく、命令の処理を複数の段階に分けて組み立てラインのように重ねて回します。これがパイプライン (pipeline) です。さらにそのラインを複数並べ、サイクルごとに複数の命令を同時に送り込む構造がスーパースカラー (superscalar) です。

組み立てラインの弱点は停止です。次の命令に必要なデータがまだ届いていなかったり、どちらの分岐に進むかわからずラインを間違って埋めてしまったりすると、ラインは空くか丸ごと破棄されます。この停止 (ストール) が積み重なった結果が低い IPC で、停止の二大原因が次の 2 つの節の主題であるキャッシュミスと分岐予測ミスです。

キャッシュ階層 — ミス 1 回のサイクルコスト #

基礎 #2 でキャッシュは「CPU のそばにある小さくて速いメモリ」という概念をつかみました。上級で必要なのは階層ごとのコストの感覚です。数値は世代ごとに違いますが、桁の感覚はこうです。

データの場所おおよその遅延感覚
L1 キャッシュ約 4 サイクル机の上のメモ
L2 キャッシュ約 12 サイクル同じ部屋の本棚
L3 キャッシュ約 40 サイクル廊下の突き当たりのキャビネット
DRAM約 200 サイクル以上別の建物の書庫

核心は L1 と DRAM の間の格差が 50 倍だという点です。DRAM まで降りるミス 1 回は、命令を数百個処理できたはずの時間を燃やします。ミスが頻発するコードでは、コアは働く時間より待つ時間のほうが長くなり、IPC は 1.0 を割り込みます。大きな配列をランダムな順序でかき回すアクセスや、ポインタをたどってメモリのあちこちへジャンプするデータ構造が典型的な原因です。

分岐予測 — 外れるとラインを破棄します #

条件文の前でコアは待ちません。分岐予測器 (branch predictor) が過去のパターンから「こちらへ進むはず」と賭け、その方向の命令でパイプラインを先に埋めます。予測が当たればタダですが、外れると間違って埋めたラインを丸ごと破棄して埋め直さなければなりません。このコストが 1 回あたりおおよそ 15〜20 サイクルです。

現代の予測器は規則的なパターンをほぼすべて当てるので、分岐ミスの比率は普通 1% 以内です。ソートされていないデータに対する条件分岐や、予測不可能な入力に左右されるコードでこの比率が数パーセントに跳ね上がり、その分だけ IPC を削っていきます。

perf stat — 使用率の裏の数字を取り出す #

CPU にはこうした事象を数えるハードウェアカウンタ (PMU) が内蔵されていて、perf stat はその値を読んでくれます。

perf stat
$ perf stat -p 4321 -- sleep 10

 Performance counter stats for process id '4321':

         39,812.43 msec task-clock                #    3.981 CPUs utilized
    98,234,567,890      cycles                    #    2.468 GHz
    49,876,543,210      instructions              #    0.51  insn per cycle
     8,123,456,789      branches                  #  204.043 M/sec
       123,456,789      branch-misses             #    1.52% of all branches
     2,345,678,901      cache-references
       987,654,321      cache-misses              #   42.10% of all cache refs

      10.001234567 seconds time elapsed

読む順番は 3 行です。

  • insn per cycle — IPC です。上の出力の 0.51 は、コアがサイクルの大半を待ちに使っているという意味です。
  • cache-misses の比率 — 42% なら、メモリアクセス 10 回のうち 4 回以上がキャッシュを突き抜けて下まで降りたという意味なので、低い IPC の犯人としてキャッシュミスを名指しできます。
  • branch-misses の比率 — 1.52% は平凡な水準です。この事例では分岐は無罪です。

ひとつ注意が必要です。cycles を時間で割った実効クロック (上の出力の 2.468GHz) がベースクロックより低いなら、中級 #2 で見たスロットリングやガバナーの問題が重なっている可能性があります。マイクロアーキテクチャの解釈はクロックが正常だという前提の上で意味を持つので、実効クロックの確認を先に行います。

perf record とフレームグラフ — どこでそうなっているのか #

perf stat が「このプロセスのボトルネックはメモリ待ち」という性格を教えてくれるなら、コードのどの部分がそうなのかは perf record が答えます。実行中の関数とコールスタックを周期的にサンプリングしておき、perf report でどの関数がサイクルをもっとも多く食ったかを集計する方式です。

このサンプルを 1 枚の絵に広げたものがフレームグラフ (flame graph) です。横幅がその関数 (とその下の呼び出し) が占めた時間の割合で、縦がコールスタックの深さです。広い峰を探せば、そこがサイクルを燃やしているコードです。perf stat でボトルネックの性格をつかみ、フレームグラフで位置をつかむ順番が実戦の 1 サイクルです。

事例 — IPC 0.5 と 2.0 の違う処方 #

同じ使用率 100% の 2 台のサーバーを並べて、解釈を最後まで進めてみます。

  • サーバー A: IPC 0.5、cache-misses 40% — コアは忙しいふりをしていますが、実際には DRAM 往復を待つ時間が大半です。コア増設は待つコアを増やすだけです。処方はメモリアクセス側です。データ構造の局所性の改善、アクセス順序の整理、そして 3 編で扱う hugepages とメモリ帯域幅の点検が候補です。
  • サーバー B: IPC 2.0、cache-misses 3% — コアが演算で満たされています。この 100% は減らしてくれるハードウェアのトリックがない本物の飽和なので、処方はコア増設、アルゴリズム改善、または処理の分散です。

中級までの指標ではこの 2 台は区別できません。使用率も、ロードも、クロックも同じになり得ます。IPC とミス比率がようやく 2 台を切り分け、間違った処方 (サーバー A へのコア増設) に使うお金を防いでくれます。

まとめ #

今回つかんだ絵です。

  • 使用率はコアの占有時間にすぎず、仕事の量ではありません。仕事の密度は IPC が見せてくれます。
  • 低い IPC の二大原因はキャッシュミス (DRAM 往復は 200 サイクル以上) と分岐予測ミス (1 回あたり 15〜20 サイクル) です。
  • perf stat では IPC、キャッシュミス比率、分岐ミス比率の 3 行を読み、実効クロックが正常かを先に確認します。
  • perf record とフレームグラフがボトルネックの位置を、perf stat がボトルネックの性格を教えてくれます。
  • 同じ 100% でも IPC 0.5 はメモリの処方、2.0 は演算の処方に分かれます。

次回 — eBPF 観測 #

次の記事「ハードウェア上級 #2 eBPF 観測」では、観測の対象を CPU 内部からカーネル全体へ広げます。perf がハードウェアカウンタを読んだとすれば、eBPF はカーネルの中に小さなプログラムを差し込み、システムコール、ディスク I/O 遅延、ネットワーク経路を実行中にのぞき込みます。再起動なしで稼働中のサーバーを解剖する方法です。

X