Intel CPUでのハードウェアパフォーマンスカウンタの使い方

November 3, 2020 (Updated on: November 4, 2020)
by Keichi Takahashi

モダンなCPUはハードウェアパフォーマンスカウンタという機能を備えており,CPU内で発生した様々なイベント (クロック,命令のリタイア,キャッシュヒット・ミスなど) をハードウェアでカウントし,性能解析に役立てることができる.ここでは,Intel CPUにおけるハードウェアパフォーマンスカウンタの使用方法を説明する.

概要

IntelプロセッサのハードウェアパフォーマンスカウンタはPerformance Monitoring Counter (PMC) と呼ばれており,Model-Specific Register (MSR)の一部としてアクセスできる.PMCの使用方法はIntelのマニュアル“Intel 64 and IA-32 Architectures Software Developer’s Manual Volume 3 (3A, 3B, 3C & 3D): System Programming Guide”のChapter 18と19に詳しく書かれている.

CPU内にはPMCというユニットが複数存在し,CPU内の様々なユニットで発生するイベントをカウントする.PMCはgeneral-purpose performance countersとfixed-function performance countersに分類される.前者はプログラムすることにより任意のイベントをカウントできる一方,後者はカウンタごとに決まったイベントしかカウントできない.また,イベントの種類はarchitectural performance eventとnon-architectural performance eventに分類される.名前がわかりにくいが,architectural performance eventはマイクロアーキテクチャ間で互換性がある一方,non-architectural performance eventは特定のマイクロアーキテクチャに依存するものらしい.

PMCの数は限られており,1つのPMCでは1種類のイベントしかカウントできない.CPUに搭載されているPMCの具体的な数はマイクロアーキテクチャやモデルによって異なるが,最近のプロセッサならハードウェアスレッド毎にfixed-function performance countersが3つ,general-purpose performance countersが4つある.これ以上のイベントを計測したい場合は,プログラムを複数回走らせなければならない.

PMCの設定・読み出し方法

具体的にPMCを設定し,カウントを読み出す方法を説明する.General-purpose performance counterの実体は,IA32_PMC0IA32_PMC1, …というMSRにある.また,fixed-function performance countersの実体は,IA32_FIXED_CTR0IA32_FIXED_CTR1,…である.各general-purpose performance counterには設定をプログラムするためのMSRIA32_PERFEVTSEL0IA32_PERFEVTSEL1,…が存在する.IA32_PMC0の設定はIA32_PERFEVTSEL0でプログラムするという具合になる.fixed-function performance countersの設定は全てIA32_FIXED_CTR_CTRLを通して行う.最後に,全カウンタの有効・無効を制御するIA32_PERF_GLOBAL_CTRLというMSRがある.

PMCはハードウェアスレッドまたはコアごとに存在しており,あくまでハードウェアレベルのものである.したがって,OSが測定対象のタスクをマイグレーションしてしまうと正しくカウントができない.したがって正しい測定を行うためにはスレッドアフィニティを設定することが必須である.

General-purpose performance counters

General-purpose performance countersの使用方法は次の手順になる:

  1. IA32_PERF_GLOBAL_CTRLで使用するPMCを有効化
  2. IA32_PERFEVTSELxでカウントするイベントを設定
  3. IA32_PMCxからカウントを読む

例えば,LLCミスをPMC0でカウントしたい場合は次のようにMSRを設定する.まず,IA32_PERF_GLOBAL_CTRL(0x38f) でPMC0を有効にする.IA32_PERF_GLOBAL_CTRLのレイアウトは次の図のようになっている (以降の図は全てIntelのマニュアルより抜粋) ので,0ビット目を立てる.

IA32_PERF_GLOBAL_CTRL

次に,IA32_PERFEVTSEL0 (0x186)でPMC0をプログラムする.IA32_PERFEVTSELxのレイアウトは下図のようになっており,各フィールドの意味は次のとおりである.

  • Event Select: 対象とするCPU内のユニットを指定
  • UMASK: ユニット内でカウントするイベントの種類を指定
  • OS: カーネルモード時のみカウントする
  • USR: ユーザモード時のみカウントする
  • EN: カウンタを有効化

IA32_PERFEVTSELx

LLCミスイベントの場合,Event Selectは0x2e,Umaskは0x41を設定する.各イベントのイベント番号とUmaskはIntelのマニュアルのChapter 19に網羅されている.USRフラグ (16ビット目) ・ OSフラグ (17ビット目) のいずれかまたは両方を立てる.さらに ENフラグ (22ビット目) を立ててカウンタを有効にする.

設定は以上である.IA32_PMC0 (0xc1) を読めばLLCミスの回数がわかる.

Fixed-function performance counters

Fixed-function performance counterの設定手順は次のとおりである:

  1. IA32_PERF_GLOBAL_CTRLIA32_FIXED_CTR_CTRLで使用するPMCを有効化
  2. IA32_FIXED_CTRxからカウントを読む

例えば,リタイアした命令数をカウントする場合は次のようにMSRを設定する.リタイアした命令数はCTR0がカウントしているので,このPMCを有効にする.

まず,IA32_PERF_GLOBAL_CTRL (0x38f) でFIX0に対応する32ビット目を立てる.次に,IA32_FIXED_CTR_CTRL (0x38d) の対応するビットを立てる.レイアウトは下図の通りである.0ビット目と1ビット目のいずれかまたは両方を立てる.

IA32_FIXED_CTR_CTRL

以上で設定が完了したので,IA32_FIXED_CTR0 (0x309) を読み出す.

サンプリング

IA32_PERFEVTSELxINTビットを立てることにより,カウンタがオーバーフローした際に割り込みを発生させることができる.割り込みが発生した際のプログラムカウンタをサンプリングすることでイベントの発生頻度が高い命令を調べることができる.ただし,この方法ではパイプライン段数が深い現代のプロセッサでは問題がある.というのも,割り込みが発生した時点でカウンタをオーバーフローさせた命令は既にパイプラインを進んでおり,割り込み発生時のプログラムカウンタは実際にPMCをオーバーフローさせた命令のプログラムカウンタとずれている.

そこで導入されたのがProcessor Event-Based Sampling (PEBS)という仕組みである.PEBSを使用すると,カウンタがオーバーフローした時点でハードウェアがプログラムカウンタ等のレジスタをメモリのバッファに保存しておいてくれる.このバッファが溢れた際に割り込みが発生するので,ソフトウェアはバッファに蓄積された情報を読みに行けば良い.PEBSを利用することによりサンプリング結果が正確になる上,割り込み回数が減るのでオーバーヘッドが削減される.

PMCにアクセスするためのツール・ライブラリ

実際にPMCを使用して自分のアプリケーションを測定する場合は,既存のツールやライブラリを使用すれば良い.簡単と思われる順に書くと,次のようになると思う.普通は1か2で十分で,自分でMSRを操作する必要はないはず.

  1. perfIntel VTune等のプロファイラを使用する.これらのツールはお手軽だが,具体的にどのパフォーマンスカウンタを読んでいるのかわからない場合がある.perfの場合,カーネルのソースコード (arch/x86/events/intel/core.cなど)を読むとわかる.
  2. PAPILIKWID等のフレームワークを使用する.アプリケーションの一部分のみ正確に測定したい場合,これらのフレームワークを使用することになる.アプリケーションからライブラリ関数を呼ぶことで,カウンタの開始・停止を指示できる.使用できるカウンタの種類も1に比べ多い.
  3. msrドライバを使用する.modprobe msrすると,/dev/cpu/n/msrからMSRが見えるようになる.このデバイスを開き,対象のMSR番号だけシークした後,8バイト単位で読み書きする.
  4. wrmsr, rdmsr命令を使用する.カーネルモードでしか実行できないので,カーネルモジュールを書く必要がある.